Javaの二次元配列とは、「配列を要素に持つ配列」のことで、int[][] arr = new int[3][4]; のように角かっこを2つ並べて宣言します。 行と列を持つ表のようなデータを、arr[行][列] の形で読み書きできます。行数は arr.length、ある行の列数は arr[i].length で取得します。

この記事では、宣言と初期化の書き方4通り、length の意味、for・拡張for・Arrays.deepToString を使った出力、行ごとに長さが違うジャグ配列、例外の再現、コピーの落とし穴、合計・転置・ソート、三次元配列までを扱います。コードはすべて OpenJDK 21.0.10 で実行し、出力をそのまま掲載しています。ファイル名を合わせて java ファイル名.java で動かせます。

二次元配列の早見表

やりたいこと 書き方
値を並べて初期化する int[][] a = { {1, 2, 3}, {4, 5, 6} };
行数と列数を決めて作る(中身は0) int[][] a = new int[2][3];
行数だけ決めて作る int[][] a = new int[2][]; のあと a[0] = new int[3];
要素を読む・書く int v = a[1][2]; / a[1][2] = 10;
行数を調べる a.length
i行目の列数を調べる a[i].length
二重ループで回す for (int i = 0; i < a.length; i++) for (int j = 0; j < a[i].length; j++)
拡張forで回す for (int[] row : a) for (int v : row)
全体を1行で出力する System.out.println(Arrays.deepToString(a));
行ごとに出力する for (int[] row : a) System.out.println(Arrays.toString(row));
深いコピーを作る Arrays.stream(a).map(int[]::clone).toArray(int[][]::new)
中身を比べる Arrays.deepEquals(a, b)
行単位で並べ替える Arrays.sort(a, Comparator.comparingInt(row -> row[0]));
全要素の合計を出す Arrays.stream(a).flatMapToInt(Arrays::stream).sum()
三次元配列を作る int[][][] c = new int[2][3][4];

Arrays を使うコードでは、先頭に import java.util.Arrays; を書きます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java二次元配列の使い方|初期化・ループ・lengthをコード付きで解説」

二次元配列を宣言して初期化する4通りの書き方

初期化の書き方は4通りあります。1つのプログラムで並べて実行します。

// Init.java
import java.util.Arrays;

public class Init {
    public static void main(String[] args) {
        // 1. 値を並べて初期化(宣言と同時のときだけ使える書き方)
        int[][] a = { {1, 2, 3}, {4, 5, 6} };

        // 2. new付きの初期化子(代入や引数に直接渡すときはこちら)
        int[][] b = new int[][] { {1, 2, 3}, {4, 5, 6} };

        // 3. 行数と列数を指定して生成(中身は既定値の0)
        int[][] c = new int[2][3];

        // 4. 行数だけ指定し、各行はあとから生成
        int[][] d = new int[2][];
        d[0] = new int[3];
        d[1] = new int[] {7, 8};

        System.out.println(Arrays.deepToString(a));
        System.out.println(Arrays.deepToString(b));
        System.out.println(Arrays.deepToString(c));
        System.out.println(Arrays.deepToString(d));
    }
}

実行結果:

[[1, 2, 3], [4, 5, 6]]
[[1, 2, 3], [4, 5, 6]]
[[0, 0, 0], [0, 0, 0]]
[[0, 0, 0], [7, 8]]

4通りの使い分け

書き方 向いている場面 補足
{ {1, 2}, {3, 4} } 最初から値が決まっている 宣言と同時のときだけ書ける
new int[][] { {1, 2}, {3, 4} } 宣言のあとで代入する、メソッドの引数に直接渡す 角かっこの中に要素数は書かない
new int[2][3] 大きさだけ決まっていて、値はあとで入れる 全要素が既定値で埋まる
new int[2][] 行ごとに長さを変えたい 各行は null なので、使う前に行を生成する

既定値は要素の型で決まる

new で作った配列の中身は、型ごとの既定値で埋まります。次の値は実際に Arrays.deepToString で出力して確かめたものです。

要素の型 既定値 new 型[1][2] の出力
int(long・short・byte も同じ) 0 [[0, 0]]
double(float も同じ) 0.0 [[0.0, 0.0]]
boolean false [[false, false]]
char 空文字ではなく文字コード0の文字 画面には見えない文字が2つ並ぶ
String などの参照型 null [[null, null]]
行を省略した new int[2][] 各行が null Arrays.toString で [null, null]

初期化で出やすいコンパイルエラー

次の2つは書きたくなる形ですが、コンパイルエラーになります。

int[][] a;
a = { {1, 2}, {3, 4} };          // 宣言と別の行では { } だけの形は使えない
int[][] b = new int[][3];        // 行数を省いて列数だけ書くことはできない

コンパイル結果(抜粋):

InitErr.java:4: error: illegal start of expression
        a = { {1, 2}, {3, 4} };
            ^
InitErr.java:5: error: ']' expected
        int[][] b = new int[][3];
                              ^
エラーになる書き方 直し方
a = { {1, 2}, {3, 4} }; a = new int[][] { {1, 2}, {3, 4} };
new int[][3] new int[2][3] または new int[2][](左側の要素数は必ず書く)

lengthの意味:行数と列数を取り出す

二次元配列で最初につまずきやすいのが length です。arr.length は行数、arr[i].length はi行目の列数を表します。

図で見る二次元配列の構造

二次元配列は「行を表す配列への参照を並べた配列」です。3行4列の配列は次のような構造になっています。

arr ──▶ [ arr[0] , arr[1] , arr[2] ]          ← arr.length = 3(行数)
            │        │        │
            ▼        │        │
          [ 1,  2,  3,  4 ]   │               ← arr[0].length = 4(0行目の列数)
                     ▼        │
                   [ 5,  6,  7,  8 ]          ← arr[1].length = 4
                              ▼
                            [ 9, 10, 11, 12 ] ← arr[2].length = 4

arr[1][2] は「1行目(上から2番目)の、2列目(左から3番目)」= 7

表として見ると次のようになります。添字は行も列も0から始まります。

列0 列1 列2 列3
行0 arr[0][0] = 1 arr[0][1] = 2 arr[0][2] = 3 arr[0][3] = 4
行1 arr[1][0] = 5 arr[1][1] = 6 arr[1][2] = 7 arr[1][3] = 8
行2 arr[2][0] = 9 arr[2][1] = 10 arr[2][2] = 11 arr[2][3] = 12

実行して確かめる

// Len.java
public class Len {
    public static void main(String[] args) {
        int[][] arr = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12}
        };
        System.out.println("arr.length    = " + arr.length);      // 行数
        System.out.println("arr[0].length = " + arr[0].length);   // 0行目の列数
        System.out.println("arr[1][2]     = " + arr[1][2]);       // 1行目・2列目
        System.out.println("最後の要素     = " + arr[arr.length - 1][arr[arr.length - 1].length - 1]);

        int total = 0;
        for (int[] row : arr) total += row.length;
        System.out.println("全要素数       = " + total);
    }
}

実行結果:

arr.length    = 3
arr[0].length = 4
arr[1][2]     = 7
最後の要素     = 12
全要素数       = 12

lengthの早見表

書き方 意味 上の例の値
arr.length 行数(外側の配列の要素数) 3
arr[0].length 0行目の列数 4
arr[i].length i行目の列数 4
arr[arr.length - 1] 最後の行 {9, 10, 11, 12}
行ごとの length の合計 全要素数 12

length はかっこを付けないフィールドです。文字列の length() やリストの size() とは書き方が違います。

ループで全要素を処理・出力する

二次元配列の出力方法を7通り並べます。用途に合うものをそのまま使えます。

// Loop.java
import java.util.Arrays;

public class Loop {
    public static void main(String[] args) {
        int[][] arr = { {1, 2, 3}, {4, 5, 6} };

        System.out.println("--- 1. 二重for(添字が必要なとき)");
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr[i].length; j++) {
                System.out.print(arr[i][j] + " ");
            }
            System.out.println();
        }

        System.out.println("--- 2. 拡張for(読むだけのとき)");
        for (int[] row : arr) {
            for (int v : row) {
                System.out.print(v + " ");
            }
            System.out.println();
        }

        System.out.println("--- 3. 行ごとにArrays.toString");
        for (int[] row : arr) {
            System.out.println(Arrays.toString(row));
        }

        System.out.println("--- 4. Arrays.deepToString(1行で全体)");
        System.out.println(Arrays.deepToString(arr));

        System.out.println("--- 5. printfで桁をそろえる");
        for (int[] row : arr) {
            for (int v : row) System.out.printf("%4d", v);
            System.out.println();
        }

        System.out.println("--- 6. Streamで出力");
        Arrays.stream(arr).map(Arrays::toString).forEach(System.out::println);

        System.out.println("--- 7. そのままprintlnやArrays.toStringに渡した場合");
        System.out.println(arr.getClass().getName());
        System.out.println(Arrays.toString(arr).startsWith("[[I@"));
    }
}

実行結果:

--- 1. 二重for(添字が必要なとき)
1 2 3 
4 5 6 
--- 2. 拡張for(読むだけのとき)
1 2 3 
4 5 6 
--- 3. 行ごとにArrays.toString
[1, 2, 3]
[4, 5, 6]
--- 4. Arrays.deepToString(1行で全体)
[[1, 2, 3], [4, 5, 6]]
--- 5. printfで桁をそろえる
   1   2   3
   4   5   6
--- 6. Streamで出力
[1, 2, 3]
[4, 5, 6]
--- 7. そのままprintlnやArrays.toStringに渡した場合
[[I
true

出力方法の選び方

方法 向いている場面
二重for 添字 i・j を使う、要素を書き換える、一部だけ処理する
拡張for 全要素を順に読むだけ
行ごとに Arrays.toString 行と列の形が分かるように確認したい
Arrays.deepToString デバッグで全体をすばやく確認したい
printf("%4d") 桁をそろえて表のように見せたい
Stream 加工や集計と組み合わせたい

7番目の結果のとおり、二次元配列をそのまま println に渡したり Arrays.toString に渡したりすると、[[I@1b6d3586 のような「型名+ハッシュ値」の文字列になります(@ 以降は実行のたびに変わります)。二次元配列の中身を見たいときは Arrays.deepToString を使う、と覚えておくと便利です。

内側のループ条件はarr[i].lengthにする

内側のループの条件を arr[0].length に固定すると、行ごとに長さが違う配列で範囲外になります。内側は必ず arr[i].length と書くと、どんな形の配列でも正しく回せます。

拡張forでは要素を書き換えられない

拡張forの変数は「要素のコピー」です。基本型の値を代入しても、元の配列は変わりません。

// ForEachTrap.java
import java.util.Arrays;

public class ForEachTrap {
    public static void main(String[] args) {
        int[][] arr = { {1, 2}, {3, 4} };

        for (int[] row : arr) {
            for (int v : row) {
                v = 0;              // コピーされた値を書き換えているだけ
            }
        }
        System.out.println(Arrays.deepToString(arr));

        for (int[] row : arr) {
            row[0] = 0;             // rowは行そのものを指すので反映される
        }
        System.out.println(Arrays.deepToString(arr));

        for (int[] row : arr) {
            Arrays.fill(row, 9);    // 行をまとめて埋める
        }
        System.out.println(Arrays.deepToString(arr));
    }
}

実行結果:

[[1, 2], [3, 4]]
[[0, 2], [0, 4]]
[[9, 9], [9, 9]]
書き方 元の配列に反映されるか
for (int v : row) v = 0; 反映されない
row[0] = 0;(row は拡張forの変数) 反映される
Arrays.fill(row, 9); 反映される
arr[i][j] = 0;(二重for) 反映される

行ごとに長さが違うジャグ配列

Javaの二次元配列は「配列の配列」なので、行ごとに列数を変えられます。このような配列をジャグ配列(不規則配列)と呼びます。

// Jagged.java
import java.util.Arrays;

public class Jagged {
    public static void main(String[] args) {
        int[][] jag = { {1}, {2, 3}, {4, 5, 6} };
        for (int i = 0; i < jag.length; i++) {
            System.out.println("jag[" + i + "].length = " + jag[i].length);
        }
        System.out.println(Arrays.deepToString(jag));

        // 三角形の表(パスカルの三角形)を作る
        int n = 5;
        int[][] tri = new int[n][];
        for (int i = 0; i < n; i++) {
            tri[i] = new int[i + 1];
            tri[i][0] = tri[i][i] = 1;
            for (int j = 1; j < i; j++) {
                tri[i][j] = tri[i - 1][j - 1] + tri[i - 1][j];
            }
        }
        for (int[] row : tri) System.out.println(Arrays.toString(row));
    }
}

実行結果:

jag[0].length = 1
jag[1].length = 2
jag[2].length = 3
[[1], [2, 3], [4, 5, 6]]
[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]

ジャグ配列は、月ごとの日数、段ごとに個数が違う表、三角形の表など、行によって必要な列数が違うデータに向いています。使わない要素を確保せずに済むのが利点です。

例外を再現して原因を確かめる

二次元配列でよく出る例外を、実際に発生させてメッセージを確認します。

ArrayIndexOutOfBoundsException:範囲外の添字

ループの条件を <= にした例です。

// Oob.java
public class Oob {
    public static void main(String[] args) {
        int[][] arr = new int[2][3];
        for (int i = 0; i <= arr.length; i++) {      // <= になっている
            for (int j = 0; j < arr[i].length; j++) {
                arr[i][j] = i * 10 + j;
            }
        }
    }
}

実行結果:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
    at Oob.main(Oob.java:5)

メッセージの Index 2 out of bounds for length 2 は「長さ2の配列に、添字2でアクセスした」という意味です。長さ2の配列で使える添字は0と1なので、i <= arr.length を i < arr.length に直すと解消します。

次は、ジャグ配列で内側の条件を jag[0].length に固定した例です。

// Oob2.java
public class Oob2 {
    public static void main(String[] args) {
        int[][] jag = { {1, 2, 3}, {4} };
        for (int i = 0; i < jag.length; i++) {
            for (int j = 0; j < jag[0].length; j++) {   // 0行目の長さで固定している
                System.out.println(jag[i][j]);
            }
        }
    }
}

実行結果:

1
2
3
4
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
    at Oob2.main(Oob2.java:6)

1行目(長さ1)に対して添字1でアクセスしたため、途中で止まりました。jag[0].length を jag[i].length に直すと最後まで動きます。

NullPointerException:行を生成していない

new int[2][] で作った配列は、各行がまだ null です。

// Npe.java
public class Npe {
    public static void main(String[] args) {
        int[][] d = new int[2][];   // 各行はまだnull
        d[0][0] = 1;
    }
}

実行結果:

Exception in thread "main" java.lang.NullPointerException: Cannot store to int array because "<local1>[0]" is null
    at Npe.main(Npe.java:4)

"<local1>[0]" is null は、ローカル変数の0番目(ここでは d[0])が null だという意味です。今回は java Npe.java で直接実行したため変数名が <local1> と表示されています。d[0] = new int[3]; のように行を生成してから要素に代入すると解消します。

例外と直し方の一覧

例外・メッセージ 原因 直し方
ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2 添字が長さ以上(<= の条件、行と列の取り違えなど) 条件を < にする。行は arr.length、列は arr[i].length
ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1 ジャグ配列で列数を固定して回している 内側の条件を arr[i].length にする
NullPointerException: Cannot store to int array because ... is null new int[n][] のあと行を生成していない arr[i] = new int[m]; を先に実行する
NegativeArraySizeException: -1 要素数に負の値を指定した 要素数の計算結果を事前に確認する
ClassCastException: class [I cannot be cast to class java.lang.Comparable Arrays.sort(二次元配列) を比較方法なしで呼んだ Comparator を渡す(後述)

NegativeArraySizeException と ClassCastException も、それぞれ new int[-1][3] と Arrays.sort(data) を実行して上記のメッセージが出ることを確認しています。

コピーの落とし穴:cloneは浅いコピーになる

二次元配列を clone() や Arrays.copyOf でコピーすると、外側の配列だけが新しく作られ、各行は元の配列と共有されます(浅いコピー)。コピー先を書き換えると、元の配列も変わります。

// Copy.java
import java.util.Arrays;

public class Copy {
    public static void main(String[] args) {
        int[][] src = { {1, 2}, {3, 4} };

        // 浅いコピー:外側の配列だけが新しくなり、行は共有される
        int[][] shallow = src.clone();
        shallow[0][0] = 99;
        System.out.println("src     = " + Arrays.deepToString(src));
        System.out.println("shallow = " + Arrays.deepToString(shallow));
        System.out.println("外側は別物か: " + (src != shallow));
        System.out.println("0行目は同じ実体か: " + (src[0] == shallow[0]));

        // Arrays.copyOfも同じく浅いコピー
        int[][] copyOf = Arrays.copyOf(src, src.length);
        System.out.println("copyOfの0行目は同じ実体か: " + (src[0] == copyOf[0]));

        // 深いコピー1:行ごとにcloneする
        src[0][0] = 1;
        int[][] deep1 = new int[src.length][];
        for (int i = 0; i < src.length; i++) {
            deep1[i] = src[i].clone();
        }

        // 深いコピー2:Streamで1行にまとめる
        int[][] deep2 = Arrays.stream(src).map(int[]::clone).toArray(int[][]::new);

        deep1[0][0] = -1;
        deep2[1][1] = -2;
        System.out.println("src   = " + Arrays.deepToString(src));
        System.out.println("deep1 = " + Arrays.deepToString(deep1));
        System.out.println("deep2 = " + Arrays.deepToString(deep2));

        // 比較:==、equals、Arrays.equals、Arrays.deepEquals
        int[][] x = { {1, 2}, {3, 4} };
        int[][] y = { {1, 2}, {3, 4} };
        System.out.println("x == y            : " + (x == y));
        System.out.println("x.equals(y)       : " + x.equals(y));
        System.out.println("Arrays.equals     : " + Arrays.equals(x, y));
        System.out.println("Arrays.deepEquals : " + Arrays.deepEquals(x, y));
    }
}

実行結果:

src     = [[99, 2], [3, 4]]
shallow = [[99, 2], [3, 4]]
外側は別物か: true
0行目は同じ実体か: true
copyOfの0行目は同じ実体か: true
src   = [[1, 2], [3, 4]]
deep1 = [[-1, 2], [3, 4]]
deep2 = [[1, 2], [3, -2]]
x == y            : false
x.equals(y)       : false
Arrays.equals     : false
Arrays.deepEquals : true

shallow[0][0] = 99 と書いただけで、src の中身も99に変わっています。一方、行ごとに clone() した deep1 と deep2 は、書き換えても src に影響しません。

コピー方法の比較表

方法 コピーの深さ 元の配列への影響
int[][] b = a; コピーではなく同じ配列を指す すべての変更が共有される
a.clone() 浅い(外側だけ) 要素の書き換えが共有される
Arrays.copyOf(a, a.length) 浅い(外側だけ) 要素の書き換えが共有される
行ごとに a[i].clone() 深い 影響しない
Arrays.stream(a).map(int[]::clone).toArray(int[][]::new) 深い 影響しない

二次元配列を独立させたいときは、行ごとにコピーすると覚えておきましょう。要素が String のような変更できない型であれば、行ごとのコピーで十分に独立します。要素が変更できるオブジェクトの場合は、そのオブジェクトも1つずつコピーする必要があります。

中身の比較はArrays.deepEquals

上の結果のとおり、==・equals・Arrays.equals はどれも false になりました。Arrays.equals は行同士を参照で比べるためです。二次元配列の中身を比べるときは Arrays.deepEquals を使います。

Arrays.fillで行を埋めるときの注意

同じ理由で、Arrays.fill(grid, new int[2]) と書くと、同じ1行がすべての位置に入ります。

// FillTrap.java
import java.util.Arrays;

public class FillTrap {
    public static void main(String[] args) {
        int[][] grid = new int[3][];
        Arrays.fill(grid, new int[2]);       // 同じ1行を3か所に入れている
        grid[0][0] = 7;
        System.out.println(Arrays.deepToString(grid));

        int[][] ok = new int[3][2];
        for (int[] row : ok) Arrays.fill(row, -1);   // 行ごとに埋める
        ok[0][0] = 7;
        System.out.println(Arrays.deepToString(ok));

        int[][] ok2 = new int[3][];
        Arrays.setAll(ok2, i -> new int[2]);  // 行ごとに新しい配列を作る
        ok2[0][0] = 7;
        System.out.println(Arrays.deepToString(ok2));
    }
}

実行結果:

[[7, 0], [7, 0], [7, 0]]
[[7, -1], [-1, -1], [-1, -1]]
[[7, 0], [0, 0], [0, 0]]

1つ目は grid[0][0] だけを書き換えたのに、3行すべてが変わっています。0以外の値で全体を埋めたいときは、行ごとに Arrays.fill(row, 値) を呼ぶのが確実です。

行・列の合計、検索、転置をコードで

3人×4科目の点数表を使って、よく使う処理をまとめて実行します。

// Calc.java
import java.util.Arrays;

public class Calc {
    public static void main(String[] args) {
        // 3人×4科目の点数表
        int[][] scores = {
            {80, 65, 90, 70},
            {55, 75, 60, 85},
            {95, 88, 72, 64}
        };
        int rows = scores.length;
        int cols = scores[0].length;

        // 行ごとの合計と平均
        for (int i = 0; i < rows; i++) {
            int sum = 0;
            for (int v : scores[i]) sum += v;
            System.out.printf("行%d 合計=%d 平均=%.2f%n", i, sum, (double) sum / cols);
        }

        // 列ごとの合計
        int[] colSum = new int[cols];
        for (int[] row : scores) {
            for (int j = 0; j < cols; j++) colSum[j] += row[j];
        }
        System.out.println("列合計 = " + Arrays.toString(colSum));

        // 全体の合計・最大・最小(Stream)
        int total = Arrays.stream(scores).flatMapToInt(Arrays::stream).sum();
        int max = Arrays.stream(scores).flatMapToInt(Arrays::stream).max().getAsInt();
        int min = Arrays.stream(scores).flatMapToInt(Arrays::stream).min().getAsInt();
        System.out.println("総合計=" + total + " 最大=" + max + " 最小=" + min);

        // 行ごとの合計をStreamで
        int[] rowSum = Arrays.stream(scores).mapToInt(r -> Arrays.stream(r).sum()).toArray();
        System.out.println("行合計 = " + Arrays.toString(rowSum));

        // 値を探して位置を返す
        int target = 88;
        search:
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (scores[i][j] == target) {
                    System.out.println(target + " は [" + i + "][" + j + "] にあります");
                    break search;
                }
            }
        }

        // 転置(行と列を入れ替える):3×4 → 4×3
        int[][] t = new int[cols][rows];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                t[j][i] = scores[i][j];
            }
        }
        for (int[] row : t) System.out.println(Arrays.toString(row));

        // 1次元に平らにする
        int[] flat = Arrays.stream(scores).flatMapToInt(Arrays::stream).toArray();
        System.out.println(Arrays.toString(flat));
    }
}

実行結果:

行0 合計=305 平均=76.25
行1 合計=275 平均=68.75
行2 合計=319 平均=79.75
列合計 = [230, 228, 222, 219]
総合計=899 最大=95 最小=55
行合計 = [305, 275, 319]
88 は [2][1] にあります
[80, 55, 95]
[65, 75, 88]
[90, 60, 72]
[70, 85, 64]
[80, 65, 90, 70, 55, 75, 60, 85, 95, 88, 72, 64]

処理ごとのポイント

処理 ポイント
行の合計 外側のループの中で合計用の変数を0に戻す
平均 (double) sum / cols のように、割る前に double へ変換する(整数同士の割り算は小数が切り捨てられる)
列の合計 列数ぶんの配列を用意し、colSum[j] += row[j] で足し込む
全体の集計 flatMapToInt(Arrays::stream) で1本の IntStream にしてから sum max min
検索 見つけたらラベル付き break で二重ループをまとめて抜ける
転置 大きさを new int[cols][rows] にして t[j][i] = scores[i][j]

時計回りに90度回転させたいときは、代入先を r[j][rows - 1 - i] = m[i][j] にします。{ {1, 2, 3}, {4, 5, 6} } で実行すると [4, 1] [5, 2] [6, 3] の3行になりました。

行単位でソートする

二次元配列のソートには「行の中を並べ替える」と「行そのものを並べ替える」の2種類があります。

// Sort.java
import java.util.Arrays;
import java.util.Comparator;

public class Sort {
    public static void main(String[] args) {
        // {ID, 点数}
        int[][] data = { {3, 70}, {1, 90}, {2, 70}, {4, 55} };

        // 1. 各行の中身を昇順に(行の中を並べ替える)
        int[][] a = { {3, 1, 2}, {9, 7, 8} };
        for (int[] row : a) Arrays.sort(row);
        System.out.println("行の中を並べ替え : " + Arrays.deepToString(a));

        // 2. 行単位で並べ替え:0列目(ID)の昇順
        int[][] byId = Arrays.stream(data).map(int[]::clone).toArray(int[][]::new);
        Arrays.sort(byId, Comparator.comparingInt(row -> row[0]));
        System.out.println("0列目の昇順     : " + Arrays.deepToString(byId));

        // 3. 1列目(点数)の降順
        int[][] byScoreDesc = Arrays.stream(data).map(int[]::clone).toArray(int[][]::new);
        Arrays.sort(byScoreDesc, (x, y) -> Integer.compare(y[1], x[1]));
        System.out.println("1列目の降順     : " + Arrays.deepToString(byScoreDesc));

        // 4. 点数の降順、同点ならIDの昇順
        int[][] multi = Arrays.stream(data).map(int[]::clone).toArray(int[][]::new);
        Arrays.sort(multi, Comparator.<int[]>comparingInt(row -> row[1]).reversed()
                                     .thenComparingInt(row -> row[0]));
        System.out.println("点数降順→ID昇順 : " + Arrays.deepToString(multi));

        // 5. 文字列の二次元配列を名前順に
        String[][] members = { {"sato", "tokyo"}, {"abe", "osaka"}, {"kato", "nagoya"} };
        Arrays.sort(members, Comparator.comparing(row -> row[0]));
        System.out.println("名前順          : " + Arrays.deepToString(members));
    }
}

実行結果:

行の中を並べ替え : [[1, 2, 3], [7, 8, 9]]
0列目の昇順     : [[1, 90], [2, 70], [3, 70], [4, 55]]
1列目の降順     : [[1, 90], [3, 70], [2, 70], [4, 55]]
点数降順→ID昇順 : [[1, 90], [2, 70], [3, 70], [4, 55]]
名前順          : [[abe, osaka], [kato, nagoya], [sato, tokyo]]

ソートの書き方早見表

やりたいこと 書き方
各行の中身を昇順に for (int[] row : a) Arrays.sort(row);
0列目の昇順で行を並べ替え Arrays.sort(a, Comparator.comparingInt(row -> row[0]));
1列目の降順で行を並べ替え Arrays.sort(a, (x, y) -> Integer.compare(y[1], x[1]));
複数の列で並べ替え Comparator.<int[]>comparingInt(row -> row[1]).reversed().thenComparingInt(row -> row[0])
文字列の列で並べ替え Arrays.sort(s, Comparator.comparing(row -> row[0]));

3番目の結果では、同じ70点の行が元の順番(ID 3 → ID 2)のまま並んでいます。オブジェクトの配列に対する Arrays.sort は、同じ順位の要素の並びを保つ(安定な)ソートだからです。同点のときの順番も決めたい場合は、4番目のように thenComparingInt を続けます。

比較方法を渡さずに Arrays.sort(data) と書くと、行(int[])同士の大小が決められず、次の例外になります。

Exception in thread "main" java.lang.ClassCastException: class [I cannot be cast to class java.lang.Comparable ([I and java.lang.Comparable are in module java.base of loader 'bootstrap')
    (中略)
    at java.base/java.util.Arrays.sort(Arrays.java:1042)
    at SortErr.main(SortErr.java:5)

[I は int[] を表す内部の型名です。

三次元配列と多次元配列

角かっこを3つにすると三次元配列になります。考え方は二次元と同じで、「二次元配列を要素に持つ配列」です。

// ThreeD.java
import java.util.Arrays;

public class ThreeD {
    public static void main(String[] args) {
        // 2階 × 3行 × 4列
        int[][][] cube = new int[2][3][4];
        System.out.println(cube.length);          // 2
        System.out.println(cube[0].length);       // 3
        System.out.println(cube[0][0].length);    // 4

        int n = 0;
        for (int i = 0; i < cube.length; i++) {
            for (int j = 0; j < cube[i].length; j++) {
                for (int k = 0; k < cube[i][j].length; k++) {
                    cube[i][j][k] = n++;
                }
            }
        }
        System.out.println(Arrays.deepToString(cube));
        System.out.println(cube[1][2][3]);

        int[][][] lit = { { {1, 2}, {3, 4} }, { {5, 6}, {7, 8} } };
        for (int[][] layer : lit) {
            for (int[] row : layer) {
                System.out.print(Arrays.toString(row) + " ");
            }
            System.out.println();
        }
    }
}

実行結果:

2
3
4
[[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], [[12, 13, 14, 15], [16, 17, 18, 19], [20, 21, 22, 23]]]
23
[1, 2] [3, 4] 
[5, 6] [7, 8] 
次元 宣言 lengthの取り方 ループの深さ
一次元 int[] a a.length 1重
二次元 int[][] a a.length、a[i].length 2重
三次元 int[][][] a a.length、a[i].length、a[i][j].length 3重

Arrays.deepToString と Arrays.deepEquals は、三次元以上でもそのまま使えます。次元が増えるほど添字の意味が読み取りにくくなるので、三次元以上になるときは、クラスやレコードで意味のある名前を付けて表す方法も検討すると、読みやすいコードになります。

二次元配列とListのListの使い分け

行や列の数があとから変わる場合は、List<List<Integer>>(リストのリスト)が向いています。

// Lists.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Lists {
    public static void main(String[] args) {
        // 行も列もあとから増やせる
        List<List<Integer>> table = new ArrayList<>();
        table.add(new ArrayList<>(List.of(1, 2, 3)));
        table.add(new ArrayList<>(List.of(4, 5)));
        table.get(1).add(6);                 // 1行目に列を追加
        table.add(new ArrayList<>());        // 空の行を追加
        table.get(2).add(7);

        System.out.println(table);
        System.out.println("行数=" + table.size() + " 0行目の列数=" + table.get(0).size());
        System.out.println("table.get(1).get(2) = " + table.get(1).get(2));

        // 二次元配列 → List<List<Integer>>
        int[][] arr = { {1, 2}, {3, 4} };
        List<List<Integer>> fromArr = Arrays.stream(arr)
                .map(row -> Arrays.stream(row).boxed().toList())
                .toList();
        System.out.println(fromArr);

        // List<List<Integer>> → 二次元配列
        int[][] toArr = table.stream()
                .map(row -> row.stream().mapToInt(Integer::intValue).toArray())
                .toArray(int[][]::new);
        System.out.println(Arrays.deepToString(toArr));
    }
}

実行結果:

[[1, 2, 3], [4, 5, 6], [7]]
行数=3 0行目の列数=3
table.get(1).get(2) = 6
[[1, 2], [3, 4]]
[[1, 2, 3], [4, 5, 6], [7]]

(Stream の toList() は Java 16以降で使えます。)

使い分けの比較表

比べる点 二次元配列 int[][] List<List<Integer>>
大きさ 作成時に決まり、あとから変えられない 行も列もあとから増減できる
要素の型 基本型(int など)をそのまま持てる Integer などのラッパー型
要素の読み書き a[i][j] list.get(i).get(j) / list.get(i).set(j, v)
行数・列数 a.length / a[i].length list.size() / list.get(i).size()
出力 Arrays.deepToString(a) System.out.println(list) でそのまま読める
中身の比較 Arrays.deepEquals(a, b) list.equals(other)
メモリと速度 基本型をそのまま持つので軽い 要素ごとにオブジェクトを持つ
向いている用途 盤面、画像、行列計算など大きさが固定の表 行や列の数が実行時に決まるデータ

1行が「ID・名前・点数」のように意味の違う値の集まりなら、クラスや record を作ってその List にすると、さらに読みやすくなります。

よくある質問

Q. arr.lengthとarr[0].lengthはどう違いますか? A. arr.length は行数(外側の配列の要素数)、arr[0].length は0行目の列数です。3行4列の配列なら、それぞれ3と4になります。行ごとに長さが違う場合に備えて、ループでは arr[i].length を使うのがおすすめです。

Q. 二次元配列をprintlnで表示すると、記号のような文字列になります。 A. 配列をそのまま println に渡すと、型名とハッシュ値を組み合わせた文字列が表示されます。中身を見たいときは System.out.println(Arrays.deepToString(arr)); を使ってください。Arrays.toString は一次元用なので、二次元配列には deepToString を使います。

まとめ

  • 二次元配列は「配列の配列」。int[][] a = { {1, 2}, {3, 4} }; または new int[行数][列数] で作る
  • arr.length が行数、arr[i].length がi行目の列数。内側のループ条件は arr[i].length にする
  • 出力は Arrays.deepToString(arr) が手軽。行ごとに見たいときは Arrays.toString(row)
  • 行ごとに長さを変えられる(ジャグ配列)。new int[n][] のあと行を生成してから使う
  • Index 2 out of bounds for length 2 は 「長さ2の配列に添字2でアクセスした」 という意味
  • clone() と Arrays.copyOf は浅いコピー。独立させるには行ごとにコピーし、比較は Arrays.deepEquals
  • 行単位のソートは Arrays.sort(a, Comparator.comparingInt(row -> row[0]))。大きさが変わるデータは List<List<Integer>> を選ぶ

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java二次元配列の使い方|初期化・ループ・lengthをコード付きで解説」

株式会社RJCでは、Javaの配列やコレクションの扱い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?