Javaプログラミングの学習において、データを効率的に扱うための基礎となるのが「配列」です。

配列は、同じ型のデータを複数まとめて管理するための基本的なデータ構造であり、多数の変数を個別に宣言する手間を省き、効率的なデータ処理を可能にします。

この記事では、Java配列の基本的な宣言方法から、実際の操作、さらにListなどの他のデータ構造との連携、多次元配列の活用法までを徹底的に解説します。Java配列をマスターして、より堅牢で効率的なプログラムを作成しましょう!

Java配列の宣言と初期化方法

配列の宣言と作成の基本

Javaで配列を使用する最初のステップは「宣言」と「作成(インスタンス化)」です。

まず、宣言では配列が格納するデータの型と配列名を指定します。基本構文は データ型[] 配列名; あるいは データ型 配列名[]; となります。

特に データ型[] 配列名; の形式は、変数が配列であることをより明確に示すため、一般的に推奨されています。(参考情報)

宣言しただけでは配列としてメモリ領域が確保されないため、次に「配列の作成」、つまりインスタンス化を行います。ここでは、配列が保持する要素の数を指定し、メモリを割り当てます。

基本構文は 配列名 = new データ型[要素数]; です。例えば、整数を5つ格納できる配列を作成するには、int[] numbers = new int[5]; と記述します。(参考情報)

このように、宣言と作成を一行で同時に行うことも可能です。配列の要素数は、一度作成されると変更できない固定長である点に注意しましょう。

配列要素へのアクセスと代入

配列の各要素にアクセスしたり、値を代入したりするには、インデックス(添え字)番号を使用します。

Javaの配列インデックスは常に 0から 始まります。つまり、n個の要素を持つ配列の場合、インデックスは0からn-1までとなります。(参考情報)

値の代入は 配列名[インデックス番号] = 値; の形式で行います。例えば、先ほど作成した numbers 配列の最初の要素(インデックス0)に 10 を代入するには、numbers[0] = 10; と記述します。

同様に、配列から値を取得する場合は データ = 配列名[インデックス番号]; の形式を使います。これにより、特定の場所にあるデータを読み出すことができます。(参考情報)

もし存在しないインデックスにアクセスしようとすると、ArrayIndexOutOfBoundsException が発生し、プログラムが停止してしまいます。そのため、インデックスの範囲には常に注意を払う必要があります。

初期化の多様な方法

配列を作成する際には、同時に初期値を設定して初期化することが可能です。これにより、コードの記述量を減らし、配列をより簡潔に定義できます。

最も基本的な方法は、宣言と同時に要素数を指定して配列を作成し、その後に各要素へ個別に値を代入していくものです。

int[] numbers = new int[5];
numbers[0] = 10;
numbers[1] = 20;
// ...

しかし、あらかじめ格納する値が決まっている場合は、より簡潔な方法として「宣言と同時に初期値を指定して作成する」ことができます。この方法では、波括弧 {} を使って初期値を列挙します。

int[] numbers = {10, 20, 30, 40, 50};

この記述方法では、配列の要素数を明示的に指定する必要はありません。コンパイラが自動的に初期値の数から配列の要素数を判断してくれます。(参考情報)

これにより、要素数の変更があった場合でも、初期値のリストを修正するだけでよいため、メンテナンス性が向上します。

Java配列の要素数とループ処理

lengthプロパティで要素数を取得

Javaの配列は、その要素数を簡単に取得できる便利なプロパティを持っています。それが length プロパティです。

配列名に .length を続けることで、配列が保持している要素の総数を整数値で取得できます。(参考情報)

int[] scores = {85, 90, 78, 92, 88};
int numberOfElements = scores.length; // numberOfElements は 5 になる
System.out.println("配列の要素数: " + numberOfElements);

この length プロパティは、配列のサイズが一度作成されると変更できない「固定長」であることの証拠でもあります。

ループ処理で配列の要素を順に処理する際に、この length プロパティは非常に頻繁に利用されます。例えば、最後の要素のインデックスは scores.length - 1 となります。

forループを使った全要素処理

配列の全要素を一つずつ処理する際に最も一般的に使われるのが、伝統的な for ループです。

このループは、インデックス変数を0から配列の要素数-1まで順に増加させながら、各要素にアクセスします。length プロパティと組み合わせることで、配列の全要素を確実に処理できます。

String[] fruits = {"Apple", "Banana", "Cherry"};
for (int i = 0; i < fruits.length; i++) {
    System.out.println("インデックス " + i + ": " + fruits[i]);
}

この形式のループは、要素の値だけでなく、そのインデックスも同時に利用したい場合に非常に有効です。例えば、配列の特定の場所の要素を更新したり、特定の条件を満たす要素とその位置を特定したりする際に重宝します。

インデックスに基づいた複雑な処理や、配列の途中から処理を開始・終了したい場合にも、for ループは柔軟に対応できます。

拡張forループ(for-each)による簡易処理

Java 5から導入された「拡張 for ループ(for-eachループ)」は、配列やコレクションの全要素を順に処理する際に、よりシンプルで読みやすいコードを提供します。

このループはインデックスを意識する必要がなく、各要素の値を直接取得できます。構文は for (データ型 要素変数 : 配列名) { ... 要素変数 ... } です。

double[] prices = {100.50, 250.75, 50.00};
for (double price : prices) {
    System.out.println("価格: " + price + "円");
}

拡張 for ループの大きな利点は、コードの簡潔さと可読性の向上です。しかし、いくつか注意点があります。

このループではインデックスにアクセスできないため、要素の位置情報が必要な処理には向きません。また、ループ内で 要素変数 に値を代入しても、元の配列の要素は変更されません。これは、要素変数 が配列の要素のコピーを受け取るためです。

読み取り専用の処理や、全要素への単純なアクセスには非常に便利ですが、配列の構造自体を変更するような操作には伝統的な for ループを使用する必要があります。

Java配列の便利な操作:ソート・コピー・結合

Arrays.sort()によるソート

Javaには、配列を操作するための非常に便利なユーティリティクラス java.util.Arrays が用意されています。このクラスの中でも特によく使われるのが、配列をソート(並べ替え)する Arrays.sort() メソッドです。

Arrays.sort() メソッドを使用すると、数値型の配列や文字列の配列などを昇順に簡単に並べ替えることができます。(参考情報)

int[] numbers = {5, 2, 8, 1, 9, 3};
Arrays.sort(numbers); // numbers は {1, 2, 3, 5, 8, 9} になる
System.out.println("ソート後の配列: " + Arrays.toString(numbers));

このメソッドは、プリミティブ型(int, doubleなど)の配列だけでなく、オブジェクトの配列(Stringなど)もソートできます。オブジェクトの配列をソートする場合、そのオブジェクトが Comparable インターフェースを実装しているか、Comparator を指定する必要があります。

非常に効率的なアルゴリズムが内部で使われているため、大規模な配列のソートでも高速に処理が行われます。

配列のコピーと部分コピー

Javaで配列の要素をコピーする方法はいくつかあり、用途に応じて使い分けることが重要です。主に Arrays.copyOf() メソッドと System.arraycopy() メソッドが利用されます。

Arrays.copyOf(original, newLength) メソッドは、指定された配列を新しい長さでコピーします。新しい配列は、元の配列の要素を指定された長さまでコピーして作成されます。もし newLength が元の配列より短い場合、その長さまでコピーされ、長い場合は残りがデフォルト値(数値型なら0、参照型ならnull)で埋められます。(参考情報)

int[] original = {10, 20, 30};
int[] copied = Arrays.copyOf(original, 5); // copied は {10, 20, 30, 0, 0}

一方、System.arraycopy(src, srcPos, dest, destPos, length) メソッドは、指定されたソース配列の特定の位置から、指定されたデスティネーション配列の特定の位置へ、指定された数の要素をコピーします。これはより低レベルで、非常に高速なコピーが可能です。(参考情報)

int[] source = {1, 2, 3, 4, 5};
int[] destination = new int[5];
System.arraycopy(source, 1, destination, 0, 3); // destination は {2, 3, 4, 0, 0}

これらのコピー操作は、参照型の配列の場合、要素そのものではなく、要素への参照をコピーする「シャローコピー」である点に注意が必要です。つまり、コピー元の配列とコピー先の配列が同じオブジェクトを参照することになります。

配列の結合とArrays.binarySearch()

Javaの標準ライブラリには、複数の配列を直接結合する単一のメソッドは提供されていません。配列を結合するには、新しい配列を作成し、そこに元の配列の要素を順番にコピーしていくか、Java 8以降で導入されたStream APIを利用する方法があります。

例えば、二つの配列 arr1arr2 を結合するには、まず arr1.length + arr2.length のサイズの新しい配列を作成し、そこに System.arraycopy() を使って両方の配列の要素をコピーします。

一方、配列内の特定の要素を効率的に検索したい場合は、java.util.Arrays クラスの Arrays.binarySearch() メソッドが非常に強力です。このメソッドは、必ずソート済みの配列に対して使用する必要があります。ソートされていない配列に適用すると、正しい結果が得られない可能性があります。(参考情報)

int[] sortedNumbers = {1, 3, 5, 7, 9};
int index = Arrays.binarySearch(sortedNumbers, 5); // index は 2
int notFound = Arrays.binarySearch(sortedNumbers, 4); // notFound は負の値(挿入ポイント)

binarySearch() メソッドは、要素が見つかればそのインデックスを返し、見つからなければ負の値を返します。この負の値は、その要素が配列に存在した場合に挿入されるべき位置を示すもので、(-(挿入ポイント) - 1) の形式で表現されます。

Java配列とListの相互変換

Arrays.asList()で配列をListへ

Javaでは、配列を柔軟なコレクションである List へと変換する方法がいくつか提供されています。その中でも手軽に利用できるのが java.util.Arrays.asList() メソッドです。

このメソッドを使うと、既存の配列を List オブジェクトとして扱うことができるようになります。(参考情報)

String[] array = {"Apple", "Banana", "Cherry"};
List<String> list = Arrays.asList(array);
System.out.println(list); // 出力: [Apple, Banana, Cherry]

しかし、このメソッドには重要な注意点があります。Arrays.asList() が返す List は、元の配列を「ラップ」しているだけであり、固定サイズです。 (参考情報)

これは、add()remove() といった要素の追加や削除を伴う操作を試みると、UnsupportedOperationException が発生することを意味します。

ただし、既存の要素の値を変更することは可能です。例えば、list.set(0, "Apricot"); のようにすると、元の配列の要素も同時に変更されます。このため、リストのサイズ変更が必要ない場合にのみ利用を検討すべきです。

柔軟なListへの変換:ArrayList

もし配列から、要素の追加や削除が可能な、より柔軟な List(例えば ArrayList)を作成したい場合は、Arrays.asList() を介して新しい ArrayList を生成するのが一般的です。

具体的には、new ArrayList(Arrays.asList(配列名)) のように記述します。これにより、元の配列の要素をコピーした、独立した可変サイズのリストが作成されます。

Integer[] numbersArray = {1, 2, 3};
List<Integer> mutableList = new ArrayList<>(Arrays.asList(numbersArray));
mutableList.add(4); // 要素の追加が可能
System.out.println(mutableList); // 出力: [1, 2, 3, 4]

Java 8以降では、Stream APIを利用して配列から List を生成する方法もあります。これは特に、変換の途中で何らかの処理(フィルタリングやマッピングなど)を加えたい場合に強力な選択肢となります。

String[] fruitsArray = {"Apple", "Banana"};
List<String> streamList = Arrays.stream(fruitsArray).collect(Collectors.toList());

この方法を使えば、コードがより関数型プログラミングのスタイルに近づき、簡潔に記述できることが多いです。

Listから配列への変換

List オブジェクトから再び配列に戻したい場合も、Javaは便利なメソッドを提供しています。それが List インターフェースの toArray() メソッドです。

toArray() メソッドには引数なしのものと、引数に型情報を持つ配列を受け取るものの二種類があります。

引数なしの toArray()Object[] 型の配列を返します。この場合、個々の要素を使用する際にダウンキャストが必要になるため、型安全ではありません。

List<String> myList = Arrays.asList("Red", "Green", "Blue");
Object[] objectArray = myList.toArray(); // Object[] が返される
// String firstElement = (String) objectArray[0]; // ダウンキャストが必要

より推奨されるのは、引数に配列の型を指定する toArray(T[] a) メソッドです。このメソッドには、返したい型と同じ型の空の配列(または要素数分の配列)を渡します。通常、効率のために要素数0の配列を渡すことが多いです。

String[] stringArray = myList.toArray(new String[0]); // 型安全にString[]が返される
System.out.println(stringArray[0]); // Red

これにより、ダウンキャストなしで目的の型の配列を得ることができ、コードの安全性が高まります。特にジェネリクスを使用している場合は、この方法で型安全な変換を行うことが非常に重要です。

多次元配列(2次元・3次元)の活用法

2次元配列の宣言とアクセス

Javaの配列は、一次元だけでなく、複数の次元を持つことができます。最も一般的に使われるのが「2次元配列」です。

2次元配列は、概念的には「配列の配列」と考えることができ、表形式のデータやグリッド、行列などを表現するのに非常に適しています。

2次元配列の宣言は データ型[][] 配列名; の形式で行い、インスタンス化は new データ型[行数][列数]; と記述します。

int[][] matrix = new int[3][4]; // 3行4列の2次元配列

要素へのアクセスは、一次元配列と同様にインデックスを使用しますが、次元の数だけインデックスを指定します。例えば、matrix[行インデックス][列インデックス] の形式です。

matrix[0][0] = 10; // 1行1列目の要素にアクセス
int value = matrix[1][2]; // 2行3列目の要素を取得

宣言と同時に初期化することも可能で、ネストした波括弧を使って表現します。例えば、int[][] identityMatrix = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; のように単位行列を定義できます。

2次元配列は、ゲームのマップ、画像処理のピクセルデータ、スプレッドシートのような表データを扱う場面で広く活用されます。

不規則な多次元配列(ジャグ配列)

Javaの多次元配列の柔軟な側面の一つに、「不規則な多次元配列」、通称「ジャグ配列(jagged array)」を作成できる点があります。

通常の2次元配列では、全ての行が同じ数の列を持つ必要がありますが、ジャグ配列では、行ごとに列の数を変えることができます。これは、2次元配列が実際には「配列の配列」であるというJavaの特性から来ています。

ジャグ配列を作成するには、まず外側の配列のサイズ(行数)のみを指定し、内側の配列(各行)は後から個別に初期化します。

int[][] jaggedArray = new int[3][]; // 3行の配列を宣言、各行の列数は未定
jaggedArray[0] = new int[2]; // 1行目は2列
jaggedArray[1] = new int[4]; // 2行目は4列
jaggedArray[2] = new int[1]; // 3行目は1列

この柔軟性により、メモリをより効率的に使用したり、異なるサイズのデータセットを単一の構造で管理したりすることが可能になります。

ただし、ジャグ配列を扱う際には、各行の長さが異なることを常に意識し、NullPointerExceptionArrayIndexOutOfBoundsException を避けるために、要素にアクセスする前に各行の配列が適切に初期化されているか、またインデックスが有効範囲内にあるかを確認する必要があります。

3次元配列以上の活用と注意点

Javaでは、2次元配列にとどまらず、3次元、4次元、さらにはそれ以上の次元の配列を定義することも技術的には可能です。3次元配列は、例えば空間内の3D座標データ、あるいは時間軸を持つデータセットなどを表現するのに用いられます。

宣言は データ型[][][] 配列名; のように次元の数だけブラケットを増やします。

int[][][] cube = new int[2][3][4]; // 2x3x4の3次元配列
cube[0][0][0] = 1; // 要素へのアクセス

しかし、次元が増えるにつれて、配列の構造を理解し、要素にアクセスするためのコードは非常に複雑になり、可読性やメンテナンス性が著しく低下します。

多くの場合は、3次元を超える配列が必要となる状況では、配列ではなく、より構造化されたデータ(例えば、カスタムクラスのオブジェクトをリストに格納するなど)や、List の中に List をネストさせるような、オブジェクト指向的なデータ構造を検討することが推奨されます。

これにより、コードはより直感的で理解しやすくなり、複雑なデータも効率的に扱えるようになります。多次元配列は非常に強力ですが、その複雑さと引き換えに得られるメリットを慎重に評価することが重要です。