Javaの配列は、プログラム内で複数のデータを効率的に管理するための基本的なデータ構造です。

しかし、Javaを学び始めたばかりの方や、他の動的言語に慣れている方が最初に直面する壁の一つが、「一度作成した配列のサイズを変更できない」という仕様です。

Javaにおける配列は、メモリ上に連続した領域を確保する固定長(Static)の仕組みを採用しているため、後から要素を自由に追加することは物理的に不可能です。

そのため、Javaで「配列に要素を追加する」という目的を達成するには、いくつかのテクニックを使い分ける必要があります。

具体的には、新しい配列を確保してデータをコピーする方法や、可変長をサポートする ArrayList クラスを利用する方法などが一般的です。

本記事では、初心者から中級者までが実務で活用できるよう、標準APIを用いたコピー処理から、モダンなStream API、さらには外部ライブラリを活用した方法まで、Javaで配列に要素を追加するためのあらゆる手法を徹底的に解説します。

Javaにおける配列の基本仕様と制限

Javaの配列を扱う上で、まず理解しておかなければならないのが、その「固定長」という性質です。

この性質を理解していないと、要素の追加処理を実装する際に思わぬバグを生む可能性があります。

配列のサイズは不変である

Javaの配列は、インスタンス化された時点でそのサイズ(長さ)が確定し、後から変更することはできません

例えば、int[] numbers = new int[5]; と宣言した場合、この numbers は常に5つの要素しか格納できません。

6つ目の要素を代入しようとすれば、実行時に ArrayIndexOutOfBoundsException が発生します。

「追加」の正体は「再作成とコピー」

Javaで配列に要素を追加したい場合、実際には以下の3つのステップを踏んでいます。

  1. 現在の配列よりも大きいサイズの「新しい配列」を作成する。
  2. 元の配列のデータを新しい配列に「コピー」する。
  3. 新しい要素を空いたスペースに「代入」する。

このプロセスは、パフォーマンスの観点から見ると O(n) の計算量(要素数に比例した時間)が必要になるため、頻繁に要素を追加する場合は注意が必要です。

方法1:System.arraycopy を使用した高速コピー

Javaの標準ライブラリの中で、最も低レイヤーで高速な配列コピー方法が System.arraycopy() です。

このメソッドはネイティブコードで実装されており、大量のデータをコピーする際に非常に高いパフォーマンスを発揮します

System.arraycopy の基本構文

このメソッドは、以下の引数を取ります。

  • src:コピー元の配列
  • srcPos:コピー元の開始位置
  • dest:コピー先の配列
  • destPos:コピー先の開始位置
  • length:コピーする要素数

実装サンプル

Java
public class ArrayAddExample {
    public static void main(String[] args) {
        // 元の配列
        String[] originalArray = {"Java", "Python", "C++"};
        String newItem = "Rust";

        // 1. 新しいサイズの配列を作成 (元のサイズ + 1)
        String[] newArray = new String[originalArray.length + 1];

        // 2. データをコピー
        // System.arraycopy(元配列, 元開始, 先配列, 先開始, 個数)
        System.arraycopy(originalArray, 0, newArray, 0, originalArray.length);

        // 3. 新しい要素を追加
        newArray[newArray.length - 1] = newItem;

        // 結果の確認
        for (String s : newArray) {
            System.out.println(s);
        }
    }
}
実行結果
Java
Python
C++
Rust

System.arraycopy は非常に強力ですが、引数が多いため可読性が低下しやすいというデメリットがあります。

単純な末尾への追加であれば、後述する Arrays.copyOf を使用する方がスマートです。

方法2:Arrays.copyOf を使用した簡潔な実装

java.util.Arrays クラスには、配列操作を簡略化するための便利なメソッドが多数用意されています。

その中でも Arrays.copyOf() は、「配列の拡張とコピーを一気に行う」ことができるため、要素の追加には最適です。

Arrays.copyOf のメリット

内部的には System.arraycopy を呼び出していますが、プログラマがコピー先のインスタンスを明示的に生成する必要がないため、コードが非常にシンプルになります。

実装サンプル

Java
import java.util.Arrays;

public class ArraysCopyOfExample {
    public static void main(String[] args) {
        int[] numbers = {10, 20, 30};
        int newNumber = 40;

        // 配列のサイズを1つ増やしてコピーを作成
        numbers = Arrays.copyOf(numbers, numbers.length + 1);

        // 最後に新しい値を代入
        numbers[numbers.length - 1] = newNumber;

        System.out.println("追加後の配列: " + Arrays.toString(numbers));
    }
}
実行結果
追加後の配列: [10, 20, 30, 40]

Arrays.copyOf は、「現在の配列をベースにサイズだけ変えたい」というケースで最も推奨される方法です。

方法3:ArrayList への変換による動的追加

実務において最も頻繁に利用されるのが、配列を一度 java.util.ArrayList に変換し、要素を追加した後に再び配列に戻す手法です。

この方法は、「要素の追加や削除が頻繁に行われる」場合に非常に有効です。

手順の詳細

  1. Arrays.asList()List.of() を使い、配列をリスト形式にする。
  2. new ArrayList<>() のコンストラクタに渡し、可変(Mutable)なリストを生成する。
  3. add() メソッドで要素を追加する。
  4. toArray() メソッドで配列に戻す。

実装サンプル

Java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

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

        // 1. 配列を可変リストに変換
        // 注意: Arrays.asList(colors) だけでは固定長リストなので new ArrayList が必要
        List<String> colorList = new ArrayList<>(Arrays.asList(colors));

        // 2. 要素を追加
        colorList.add(addColor);

        // 3. 配列に戻す
        String[] newColors = colorList.toArray(new String[0]);

        System.out.println("List経由での追加: " + Arrays.toString(newColors));
    }
}
実行結果
List経由での追加: [Red, Green, Blue, Yellow]

注意点として、Arrays.asList() が返すリストは「元の配列のビュー」であるため、サイズを変更しようとするとエラーになります。

必ず new ArrayList<>() でラップして、新しいリストインスタンスを作成してください。

方法4:Java 8以降の Stream API を活用する

モダンなJava開発(Java 8以降)では、Stream API を用いて宣言的に記述することも可能です。

複数の配列を連結したり、条件に応じた追加を行ったりする場合に、コードの可読性が大幅に向上します

実装サンプル

Java
import java.util.Arrays;
import java.util.stream.Stream;

public class StreamApiExample {
    public static void main(String[] args) {
        String[] first = {"A", "B", "C"};
        String newItem = "D";

        // Stream.concat を使用して配列と単一要素(Stream化)を結合
        String[] result = Stream.concat(Arrays.stream(first), Stream.of(newItem))
                                .toArray(String[]::new);

        System.out.println("Streamでの追加: " + Arrays.toString(result));
    }
}
実行結果
Streamでの追加: [A, B, C, D]

Stream API は非常に柔軟で、例えば「特定の条件に一致するものだけを追加する」といったフィルタリング処理との組み合わせも容易です。

ただし、プリミティブ型(int, doubleなど)の場合は、専用のStream(IntStreamなど)を使用する必要がある点に注意しましょう。

方法5:Apache Commons Lang の ArrayUtils を使う

プロジェクトで外部ライブラリの利用が許容されている場合、Apache Commons Lang というライブラリに含まれる ArrayUtils クラスを使うのが最も簡単です。

ArrayUtils.add の利便性

このライブラリを使えば、これまで説明した複雑なコピー処理や変換処理を、わずか1行で記述できます。

実装サンプル

Java
// Maven等の依存関係に追加が必要
// import org.apache.commons.lang3.ArrayUtils;

public class ApacheCommonsExample {
    public static void main(String[] args) {
        int[] data = {1, 2, 3};
        
        // ArrayUtils.add メソッドで直接追加
        // 内部で新しい配列の作成とコピーが行われる
        // int[] newData = ArrayUtils.add(data, 4);
        
        // System.out.println(Arrays.toString(newData));
    }
}

外部ライブラリへの依存が発生しますが、「車輪の再発明」を避け、バグの混入を防ぐという意味では非常に合理的な選択肢です。

各手法の比較と使い分け

どの方法を選択すべきかは、パフォーマンス要件や開発環境によって異なります。

以下の表に各手法の特徴をまとめました。

手法メ削り特徴・メリットデメリット
System.arraycopy高速ネイティブ実装で最も速い。引数が多く、記述が煩雑。
Arrays.copyOf中速標準機能で最もバランスが良い。配列の末尾追加にしか向かない。
ArrayList変換低速動的な操作(削除、挿入)が容易。変換コスト(オーバーヘッド)がある。
Stream API低速宣言的で美しいコードが書ける。処理速度は劣り、記述も少し特殊。
ArrayUtils中速1行で記述可能。最も直感的。外部ライブラリの導入が必要。

状況別のおすすめ

  • パフォーマンスが最優先: System.arraycopy
  • コードのシンプルさ重視: Arrays.copyOf
  • 追加だけでなく削除も多い: ArrayList
  • モダンなスタイルで書きたい: Stream API

パフォーマンスに関する考察

Javaで配列に要素を追加する際、常に意識すべきなのが「メモリの再確保」です。

例えば、ループの中で1つずつ要素を追加し、その度に Arrays.copyOf を実行すると、計算量は O(n^2) に跳ね上がります

非効率な例

Java
String[] result = new String[0];
for (int i = 0; i < 10000; i++) {
    result = Arrays.copyOf(result, result.length + 1);
    result[result.length - 1] = "Data" + i;
}

このコードは、ループの度に巨大な配列をメモリ上にコピーし続けるため、要素数が増えると急激に重くなります。

このような場合は、最初から ArrayList を使い、最後に一度だけ配列に変換するのが鉄則です。

よくある間違いとトラブルシューティング

配列操作の実装中によく遭遇する問題とその対策を解説します。

1. ArrayIndexOutOfBoundsException

これは「配列のインデックスが範囲外」のときに発生します。

原因

配列のサイズを増やすのを忘れている、あるいは length 以上のインデックスを指定している。

対策

新しい配列のサイズが original.length + 1 になっているか再確認してください。

2. null 要素の混入

オブジェクト型の配列(String[] など)を拡張した際、新しい要素を代入し忘れると、その箇所は null になります。

大量データ処理における注意点

大量データを扱う場合、ボクシング処理がメモリ消費量やパフォーマンスに悪影響を及ぼす可能性があります。

その際は、Trovefastutil といったプリミティブ特化のコレクションライブラリの利用を検討してください。

3. 基本データ型とラッパークラス

ArrayList を使う場合、int などの基本型は Integer などのラッパークラスにオートボクシングされます。

大量データ処理における注意点

ボクシング処理はメモリ消費量やパフォーマンスに影響を与える可能性があります。

この問題に対処するためには、Trovefastutil といったプリミティブ特化のコレクションライブラリの利用を検討してください。

まとめ

Javaにおける「配列への要素追加」は、言語仕様上、直接的な手段が提供されていません。

しかし、今回紹介した以下の手法を状況に応じて使い分けることで、安全かつ効率的に実装することが可能です。

Arrays.copyOf

基本となる手法であり、最もシンプルで一般的な用途に最適です。

System.arraycopy

速度重視の手法であり、大量のデータ移動が必要な場合に選択します。

ArrayList

操作性重視の手法であり、追加や削除が頻繁な場合は一度リストに変換するのが正解です。

Stream API

可読性重視の手法であり、関数型プログラミングのスタイルを取り入れたい場合に有効です。

Javaの配列は固定長であるという原則を忘れず、「新しい箱を用意して中身を移し替える」というイメージを持って実装に取り組んでください。

もし、要素数が頻繁に変わるようなデータ構造を扱っているのなら、最初から配列ではなく ArrayList を利用することも検討しましょう。

適切なデータ構造の選択こそが、保守性の高い高品質なコードへの第一歩となります。