Javaプログラミングにおいて、複数のデータを管理する際に「配列」は欠かせない存在です。

その中でも、行と列という2つの軸を持つ2次元配列は、表形式のデータ管理や行列演算、ゲームのマップデータ作成など、幅広いシーンで活用されます。

しかし、初心者のうちは宣言方法やメモリ上での構造、多重ループによる操作に戸惑うことも少なくありません。

本記事では、Javaにおける2次元配列の基礎から、実務で役立つ応用的な操作、最新のStream APIを用いた処理までを網羅的に詳しく解説します。

Javaの2次元配列とは

Javaにおける2次元配列は、一言で言えば「配列を要素として持つ配列」です。

数学的な行列のように「行」と「列」の概念を持ち、matrix[行番号][列番号]という形式でデータにアクセスします。

一般的なプログラミング言語の中には、多次元配列を連続した一つのメモリ領域として扱うものもありますが、Javaの2次元配列は「配列の配列 (Arrays of Arrays)」という構造をとっています。

これは、親となる配列の各要素が、子となる配列への参照(アドレス情報)を保持していることを意味します。

この構造を理解しておくことは、後述する「非対称な配列(ジャグ配列)」やメモリ効率を考える上で非常に重要です。

2次元配列の宣言と生成

2次元配列を使用するためには、まず変数の宣言と、メモリ領域の確保(インスタンス化)を行う必要があります。

Javaではいくつかの記述方法が用意されています。

基本的な宣言と領域確保

最も標準的な方法は、データ型の後に [][] を付け、new 演算子でサイズを指定する方法です。

Java
public class Main {
    public static void main(String[] args) {
        // 3行4列のint型2次元配列を宣言・生成
        int[][] matrix = new int[3][4];

        // 宣言と生成を分ける場合
        double[][] values;
        values = new double[2][5];
    }
}

このとき、数値型の配列は 0 で、参照型(Stringなど)の配列は null で自動的に初期化されるというJavaの仕様に注意してください。

初期値を指定した宣言

あらかじめ格納するデータが決まっている場合は、中括弧 {} を使用して、宣言と同時に初期値を代入するのが効率的です。

Java
public class Main {
    public static void main(String[] args) {
        // 初期値を指定して2次元配列を生成
        int[][] scores = {
            {80, 90, 70}, // 0行目
            {65, 75, 85}, // 1行目
            {95, 100, 90} // 2行目
        };
    }
}

この記述方法を用いると、行数や列数を明示的に指定する必要はありません。

コンパイラが中括弧内の要素数から自動的にサイズを判断します。

要素の代入と取得

2次元配列の各要素にアクセスするには、インデックス(添字)を使用します。

Javaのインデックスは常に 0から始まる ことに留意してください。

値の代入

特定の行と列を指定して値を書き換える例を以下に示します。

Java
public class Main {
    public static void main(String[] args) {
        String[][] board = new String[3][3];

        // 0行目0列目に値を代入
        board[0][0] = "〇";
        // 1行目2列目に値を代入
        board[1][2] = "×";

        System.out.println("代入完了");
    }
}

値の取得

取得する場合も同様にインデックスを指定します。

範囲外のインデックス(例:3行しかない配列に [3] でアクセスする)を指定すると、実行時に ArrayIndexOutOfBoundsException が発生するため注意が必要です。

Java
public class Main {
    public static void main(String[] args) {
        int[][] data = {
            {10, 20},
            {30, 40}
        };

        int val = data[1][0]; // 1行目0列目(値は30)
        System.out.println("取得した値: " + val);
    }
}

配列の長さ(lengthプロパティ)の理解

2次元配列において length プロパティを使用する場合、それが「行数」を指すのか「列数」を指すのかを正しく区別する必要があります。

コード意味
array.length配列全体の「行数」を取得する
array[i].length指定した「i行目の要素数(列数)」を取得する

Javaの2次元配列は「配列の配列」であるため、array.length は「外側の配列が保持している要素(=各行の配列)の数」を返します。

Java
public class Main {
    public static void main(String[] args) {
        int[][] matrix = new int[3][5];

        System.out.println("行数: " + matrix.length);       // 出力: 3
        System.out.println("0行目の列数: " + matrix[0].length); // 出力: 5
    }
}

2次元配列のループ処理

配列内のすべての要素を順に処理する場合、ループ処理(繰り返し処理)が必要になります。

主に「for文」「拡張for文」「Stream API」の3つの方法があります。

for文による多重ループ

インデックスを直接操作できるため、特定の場所を書き換えたり、インデックス番号を利用した計算を行ったりする場合に適しています。

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

        // 外側のループで行を制御
        for (int i = 0; i < table.length; i++) {
            // 内側のループで列を制御
            for (int j = 0; j < table[i].length; j++) {
                System.out.print(table[i][j] + " ");
            }
            System.out.println(); // 行の終わりで改行
        }
    }
}
実行結果
1 2 3 
4 5 6

拡張for文(foreach)

インデックスを使わずに要素を直接取り出せるため、コードが簡潔になり、読み間違いを防ぐことができます。

要素の参照のみを行う場合に非常に有効です。

Java
public class Main {
    public static void main(String[] args) {
        String[][] colors = {
            {"Red", "Green"},
            {"Blue", "Yellow"}
        };

        for (String[] row : colors) {
            for (String color : row) {
                System.out.println("Color: " + color);
            }
        }
    }
}

Stream APIによる処理

Java 8以降では、Stream APIを用いて2次元配列を処理することも可能です。

特に、多次元構造を1次元に平坦化(フラット化)してフィルタリングや集計を行う際に威力を発揮します。

Java
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[][] numbers = {
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
        };

        // すべての要素の合計値を計算
        int sum = Arrays.stream(numbers)      // Stream<int[]>
                        .flatMapToInt(Arrays::stream) // IntStreamに平坦化
                        .sum();

        System.out.println("合計値: " + sum);
    }
}
実行結果
合計値: 45

ジャグ配列(非対称な多次元配列)

Javaの2次元配列の大きな特徴として、各行の要素数が異なっていても良いという点があります。

これをジャグ配列(不規則配列)と呼びます。

ジャグ配列の生成

まず行数だけを指定して生成し、後から各行に対して個別に配列を割り当てます。

Java
public class Main {
    public static void main(String[] args) {
        // 行数だけを先に指定
        int[][] jagged = new int[3][];

        // 各行に異なる長さの配列を生成
        jagged[0] = new int[2];
        jagged[1] = new int[4];
        jagged[2] = new int[1];

        // 初期化を同時に行う場合
        int[][] irregular = {
            {10, 20, 30},
            {40},
            {50, 60}
        };
        
        System.out.println("1行目の長さ: " + irregular[0].length);
        System.out.println("2行目の長さ: " + irregular[1].length);
    }
}

ジャグ配列は、データの存在する領域だけを確保するため、メモリの節約につながる場合があります。

一方で、ループ処理の際には必ず array[i].length を参照して境界チェックを行わないと、エラーの原因となります。

Arraysクラスを用いた便利なメソッド

2次元配列をデバッグのために出力したり、比較したりする際、通常の Arrays.toString() では期待した結果が得られません(中身ではなく、内側の配列の参照値が表示されてしまうため)。

Arrays.deepToString() による一括出力

多次元配列の内容を文字列として整形して表示するには、Arrays.deepToString() を使用します。

Java
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[][] data = {{1, 2}, {3, 4}};
        
        // 通常のtoString(期待通りにならない)
        System.out.println(Arrays.toString(data));
        
        // deepToString(多次元に対応)
        System.out.println(Arrays.deepToString(data));
    }
}
実行結果
[[I@6504ad2f, [I@555590] // ハッシュ値が表示される
[[1, 2], [3, 4]]         // 階層を辿って中身が表示される

Arrays.deepEquals() による比較

2つの2次元配列の中身が同一であるかを判定する場合、equals() メソッドや Arrays.equals() では不十分です。

階層の深い要素まで比較する Arrays.deepEquals() を使用しましょう。

Java
import java.util.Arrays;

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

        System.out.println("== での比較: " + (arr1 == arr2));
        System.out.println("deepEquals での比較: " + Arrays.deepEquals(arr1, arr2));
    }
}
実行結果
== での比較: false
deepEquals での比較: true

2次元配列の実践的な活用例:成績管理システム

ここでは、これまでに学んだ内容を統合し、生徒3人の4教科のテスト結果を管理し、それぞれの平均点を算出するプログラムを作成してみます。

Java
public class GradeManager {
    public static void main(String[] args) {
        // 生徒名
        String[] students = {"田中", "佐藤", "鈴木"};
        // 各科目の得点(国語, 数学, 英語, 理科)
        int[][] scores = {
            {85, 70, 90, 80},
            {60, 55, 75, 65},
            {95, 100, 85, 90}
        };

        System.out.println("--- 成績一覧 ---");
        
        for (int i = 0; i < scores.length; i++) {
            int sum = 0;
            System.out.print(students[i] + "さんの得点: ");
            
            for (int j = 0; j < scores[i].length; j++) {
                int score = scores[i][j];
                System.out.print(score + " ");
                sum += score;
            }
            
            double average = (double) sum / scores[i].length;
            System.out.println("| 合計: " + sum + " | 平均: " + average);
        }
    }
}
実行結果
--- 成績一覧 ---
田中さんの得点: 85 70 90 80 | 合計: 325 | 平均: 81.25
佐藤さんの得点: 60 55 75 65 | 合計: 255 | 平均: 63.75
鈴木さんの得点: 95 100 85 90 | 合計: 370 | 平均: 92.5

このように、2次元配列を使うことで「誰の」「何のデータか」を構造的に保持できるようになり、ループ処理で一括して計算を行うことが可能になります。

2次元配列を使用する際の注意点とベストプラクティス

2次元配列は便利ですが、使いどころを誤るとコードの可読性や保守性を下げてしまう原因になります。

多次元化の限界を考える

3次元、4次元と次元を増やすことは技術的に可能ですが、3次元を超えると人間が構造をイメージしづらくなり、バグの温床になります。

複雑なデータ構造が必要な場合は、カスタムクラス(DTOなど)を定義し、そのオブジェクトをListで管理する手法を検討してください。

不変性の考慮

配列は一度生成するとサイズを変更できません。

実行時に要素数が頻繁に増減する場合は、ArrayList<ArrayList<T>> のようなコレクションクラスの使用が適しています。

パフォーマンスとメモリ

Javaの2次元配列はオブジェクトの参照を介するため、巨大な配列を扱う場合はキャッシュ効率が悪くなる(メモリアクセスが局所化されない)ことがあります。

科学計算などの極めて高いパフォーマンスが求められる場面では、1次元配列を2次元的に扱う(index = row * cols + col)といった工夫が行われることもあります。

まとめ

Javaの2次元配列は、複数のデータを「行」と「列」という直感的な構造で管理するための強力なツールです。

宣言と初期化

new int[行][列]{{...}} を活用する。

メモリ構造

Javaの多次元配列は「配列の配列」であり、各行の長さが異なるジャグ配列も作成可能。

ループ操作

基本のfor文に加えて、読み取り専用なら拡張for文、高度な処理ならStream APIを使い分ける。

ユーティリティ

デバッグには Arrays.deepToString() が必須。

まずは基本的な2重ループによる操作をマスターし、慣れてきたら実際の業務ロジックに合わせて、配列とコレクション(List)のどちらが最適かを判断できるようになりましょう。

本記事がJavaでのデータ操作スキル向上の助けになれば幸いです。