Javaプログラミングにおいて、データの集合を扱う際に最も頻繁に利用されるのがList(動的配列)と配列(固定長配列)です。

柔軟に要素を追加・削除できるListと、メモリ効率が良くAPIの引数などで求められることが多い配列は、開発の現場で頻繁に相互変換が必要になります。

しかし、Javaには長い歴史があるため、書き方はバージョンによって多岐にわたり、最適な方法を選択しないとパフォーマンスの低下や思わぬバグを招くことがあります。

本記事では、Javaの最新仕様に基づいたListと配列の相互変換メソッドを網羅的に解説します。

Java 11以降の推奨される書き方から、プリミティブ型を扱う際の注意点、さらには実行時のパフォーマンスや副作用についても深く掘り下げていきます。

これを読めば、プロジェクトの要件に合わせて最適な変換コードを迷わず記述できるようになるでしょう。

Listから配列へ変換する方法

Listから配列への変換は、フレームワークのAPIを呼び出す際や、データの順序を固定してメモリ消費を抑えたい場合によく行われます。

Javaには古くからある方法と、Java 11で導入されたモダンな書き方の2種類が存在します。

1. Java 11以降の推奨:toArray(IntFunction)

Java 11以降を使用している場合、最も簡潔かつ推奨されるのがtoArray(IntFunction<T[]> generator)を使用する方法です。

このメソッドは、メソッド参照を利用して配列のコンストラクタを渡すことができます。

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

public class ListToArrayExample {
    public static void main(String[] args) {
        List<String> fruitList = new ArrayList<>();
        fruitList.add("Apple");
        fruitList.add("Banana");
        fruitList.add("Orange");

        // Java 11以降の推奨される書き方
        // メソッド参照 String[]::new を使用して変換
        String[] fruitArray = fruitList.toArray(String[]::new);

        // 出力して確認
        for (String fruit : fruitArray) {
            System.out.println(fruit);
        }
    }
}

この書き方のメリットは、「型安全であり、かつ記述が非常にシンプルである」という点にあります。

内部的には適切なサイズの配列が自動的に生成されるため、開発者がサイズを計算して指定する必要はありません。

2. 従来の方法:toArray(T[] a)

Java 8以前や、古いライブラリとの互換性を保つ必要がある場合は、引数に空の配列を渡す方法が一般的です。

Java
// 従来の方法
String[] fruitArrayOld = fruitList.toArray(new String[0]);

ここで、new String[0]のようにサイズ0の配列を渡すことに違和感を覚える方もいるかもしれません。

かつては「あらかじめ適切なサイズの配列を渡す方が高速である」と考えられていましたが、現代のJVMにおいては、サイズ0の空配列を渡す方が最適化されやすく、パフォーマンスが向上することが知られています。

そのため、サイズを指定せずに空配列を渡す書き方が定石となっています。

3. 注意点:Object配列への変換

引数なしのtoArray()を使用すると、戻り値の型はObject[]になります。

これを特定の型(例えば String[])にキャストしようとすると、実行時に ClassCastException が発生するため注意が必要です。

Java
// 誤った例:コンパイルは通るが実行時にエラーになる
// String[] array = (String[]) fruitList.toArray();

特定の型として配列を取り出したい場合は、必ず前述した引数付きのメソッドを使用するようにしてください。

配列からListへ変換する方法

配列からListへの変換も、データの加工やフィルタリングを行う際によく発生する処理です。

変換後のListに対して「後から要素を追加したいかどうか」によって、使用すべきメソッドが異なります。

1. 変更可能なListを作成する:new ArrayList<>(Arrays.asList())

変換後に要素の追加や削除を行いたい場合は、java.util.ArrayListのコンストラクタに変換後のビューを渡す方法が一般的です。

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

public class ArrayToListExample {
    public static void main(String[] args) {
        String[] animalArray = {"Dog", "Cat", "Bird"};

        // 配列から変更可能なListを作成
        List<String> animalList = new ArrayList<>(Arrays.asList(animalArray));

        // 要素の追加が可能
        animalList.add("Hamster");

        System.out.println(animalList); // [Dog, Cat, Bird, Hamster]
    }
}

この方法は、一度元の配列からデータをコピーして新しいリストオブジェクトを作成するため、「元の配列との繋がりが断たれる」という特徴があります。

リストを操作しても元の配列は変化しません。

2. 不変(イミュータブル)なListを作成する:List.of()

Java 9以降で導入されたList.of()は、内容を変更できない不変リストを作成する際に最適です。

Java
// Java 9以降:不変リストの作成
List<String> immutableList = List.of(animalArray);

// immutableList.add("Snake"); // 実行時に UnsupportedOperationException が発生

不変リストは、読み取り専用のデータを扱う際の安全性を高めるために非常に有効です。

また、メモリ効率も通常のArrayListより優れているため、変更の必要がない場合は積極的に利用しましょう。

3. 固定サイズのListを作成する:Arrays.asList()

Arrays.asList()を使用すると、配列をリストのように扱える「ラッパー(ビュー)」が返されます。

Java
List<String> fixedList = Arrays.asList(animalArray);

// 要素の書き換えは可能(元の配列も変わる)
fixedList.set(0, "Wolf"); 
System.out.println(animalArray[0]); // Wolf と表示される

// 要素の追加・削除は不可能
// fixedList.add("Fox"); // エラー

注意点として、Arrays.asList() で生成されたリストは元の配列とメモリ空間を共有しています。

そのため、リストの要素を書き換えると元の配列の内容も書き換わります。

また、サイズを増減させることはできません。

この挙動を知らずに使うと思わぬ不具合の原因となるため、注意が必要です。

プリミティブ型配列の変換(int, doubleなど)

Javaのコレクション(Listなど)は参照型(オブジェクト)しか扱えないため、int[] などのプリミティブ型の配列を扱う場合は少し工夫が必要です。

1. Stream APIを使用した変換

プリミティブ型配列をListに変換するには、Java 8から導入されたStream APIを使用するのが最も効率的です。

Java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

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

        // int[] から List<Integer> へ変換
        List<Integer> numberList = Arrays.stream(numbers)
                                         .boxed() // int を Integer に変換
                                         .collect(Collectors.toList());

        System.out.println(numberList);
    }
}

boxed() メソッドを呼び出すことで、プリミティブな int 型をラッパークラスである Integer 型にボクシング(包む)しています。

2. List<Integer> から int[] への変換

逆に、List<Integer> から int[] に戻す際もStream APIが便利です。

Java
// List<Integer> から int[] へ変換
int[] backToArray = numberList.stream()
                              .mapToInt(Integer::intValue) // アンボクシング
                              .toArray();

mapToInt を使用して各要素をプリミティブ型に戻してから toArray() を呼び出します。

この際、前述した Object[] 版の toArray() とは異なり、プリミティブ型専用の配列が返されます。

変換方法の比較まとめ

各変換方法の特徴を整理して比較表にまとめました。

用途に応じて適切な方法を選択してください。

変換方向方法特徴・制限
List → 配列toArray(T[]::new)推奨。 Java 11以降で最も簡潔。
toArray(new T[0])Java 8以前や互換性重視の場合。
配列 → Listnew ArrayList<>(Arrays.asList(a))変更可能なListを作成。コピーが発生。
List.of(a)不変リスト。Java 9以降。
Arrays.asList(a)固定サイズ。元の配列と同期。
プリミティブ変換Stream API (boxed()など)ボクシングが必要なため、若干のコストあり。

パフォーマンスと注意点:大規模データを扱う場合

データ量が数件〜数百件程度であれば、どの方法を選んでも体感できる差はありません。

しかし、数百万件規模のデータを扱う場合には、変換に伴うメモリ消費と計算コストに注意を払う必要があります。

1. メモリの二重持ち

配列から new ArrayList<>(Arrays.asList(array)) を作成する場合、元の配列とは別に新しいメモリ領域を確保してデータをコピーします。

つまり、一時的にメモリを2倍消費することになります。

非常に大きな配列を変換する際は、メモリ不足(OutOfMemoryError)のリスクを考慮し、可能な限り変換を避けるか、Arrays.asList() のようなビューの使用を検討してください。

2. ボクシング・オーバーヘッド

プリミティブ型(intなど)とラッパークラス(Integerなど)の変換には、オブジェクト生成のコストがかかります。

頻繁に変換を行うループ内などでStream APIを乱用すると、ガベージコレクション(GC)の負荷が高まる可能性があります。

パフォーマンスがクリティカルな箇所では、プリミティブ型に特化したサードパーティ製ライブラリ(Eclipse Collectionsなど)の使用も視野に入ります。

3. Arrays.asList() の落とし穴:基本データ型配列

初心者によくある間違いとして、int[] をそのまま Arrays.asList() に渡してしまうケースがあります。

Java
int[] nums = {1, 2, 3};
List<int[]> list = Arrays.asList(nums); 
System.out.println(list.size()); // 1

期待した結果はサイズ3のリストかもしれませんが、実際には「int[] というオブジェクトを1つ保持するリスト」になってしまいます。

ジェネリクスは参照型しか扱えないため、int[] 全体が1つのオブジェクトとして認識されるためです。

前述したStream APIを使用する方法を使いましょう。

まとめ

JavaにおけるListと配列の変換は、言語の進化とともに洗練されてきました。

現代的なJava開発においては、以下の3点を意識することが重要です。

  1. Listから配列へは、Java 11の toArray(String[]::new) を使用する。
  2. 配列からListへは、変更が必要なら new ArrayList<>(Arrays.asList())、不要なら List.of() を選択する。
  3. プリミティブ型の場合は Stream API を活用してボクシング処理を行う。

これらの方法を正しく使い分けることで、コードの可読性が向上するだけでなく、予期せぬ実行時エラーやパフォーマンス劣化を防ぐことができます。

Javaのバージョンアップにより便利なメソッドは増えていますが、それぞれの内部挙動(コピーかビューか)を理解して、安全で効率的なプログラミングを心がけましょう。