この記事を書いた理由
ArrayList は日常的に使っているが、改めて「ちゃんとわかっているか」と問われると自信がなかった。
特に Arrays.asList() との組み合わせ、toArray() の使い方、List 型と ArrayList 型で宣言したときの違い。どれも「なんとなく動かしていた」レベルで止まっていたので、一度手を動かして確認した記録を残しておく。
ArrayListは動的に増減できる配列
通常の配列はサイズが固定で、宣言時に長さを決める必要がある。ArrayList はその制約がなく、要素の追加・削除に応じてサイズが変わる。
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Steve");
names.add("Alice");
names.add("Bob");
System.out.println(names); // [Steve, Alice, Bob]
names.remove("Alice");
System.out.println(names); // [Steve, Bob]
names.set(0, "Smith");
System.out.println(names); // [Smith, Bob]
}
}
System.out.println(names) でそのままリストの中身が表示されるのは、AbstractCollection クラスが持つ toString() が呼ばれているから。素通りで表示できるので便利だが、なぜ表示されるかを把握していなかった時期があった。
型パラメータの <String> の部分はジェネリクスで、このリストが扱える型を指定している。右辺の <> はダイアモンド演算子といい、左辺で型が指定されていれば省略できる。
Arrays.asList()との組み合わせで詰まったこと
初期値ありでリストを作りたいとき、Arrays.asList() を使うことがある。ただ、これには落とし穴がある。
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
// Arrays.asList()だけでは要素の追加・削除ができない
List<Integer> list = Arrays.asList(1, 2, 3);
System.out.println(list.getClass().getName()); // java.util.Arrays$ArrayList
// list.add(4); // UnsupportedOperationException が発生する
// ArrayListでラップすることで追加・削除が可能になる
List<Integer> listWrapped = new ArrayList<>(Arrays.asList(1, 2, 3));
System.out.println(listWrapped.getClass().getName()); // java.util.ArrayList
listWrapped.add(4);
System.out.println(listWrapped); // [1, 2, 3, 4]
}
}
Arrays.asList() が返すのは java.util.Arrays$ArrayList という内部クラスで、java.util.ArrayList とは別物だ。固定サイズのリストなので、要素の置換(set)はできても追加・削除はできない。
Arrays.asList() の戻り値をそのまま使い、後から add() や remove() を呼ぶと UnsupportedOperationException が発生する。new ArrayList<>(Arrays.asList(...)) でラップするのを忘れないようにしたい。
以前、この違いを知らずに add() がなぜか動かないと首を傾げた記憶がある。クラス名を確認すれば一発でわかったのだが、そこに気づくまで少し時間を使った。
ArrayListを固定長配列に変換する
逆方向、ArrayList から通常の配列に変換したい場面もある。方法は2つある。
forループで1つずつコピーする方法(プリミティブ型向け)
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
int[] array = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
array[i] = list.get(i); // Integerからintへオートアンボクシングされる
}
for (int v : array) {
System.out.println(v);
}
}
}
Integer から int への変換はオートアンボクシングによって自動で行われる。toArray() はプリミティブ型の配列には対応していないので、int[] が欲しい場合はこの方法になる。
toArray()を使う方法(オブジェクト型向け)
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("orange");
String[] array = list.toArray(new String[list.size()]);
for (String s : array) {
System.out.println(s);
}
}
}
toArray() に変換先の型の配列インスタンスを渡す。指定したサイズが足りない場合は新しい配列が作られるが、list.size() を渡しておくのが無難だ。
余談だが、Java 11以降では toArray(String[]::new) というメソッド参照を使った書き方もできる。こちらのほうが簡潔に書けるので、プロジェクトのバージョンが対応していれば使ってみる価値がある。
List型とArrayList型、どちらで宣言すべきか
// ArrayList型で宣言
ArrayList<Integer> list1 = new ArrayList<>();
// List型で宣言
List<Integer> list2 = new ArrayList<>();
ArrayList 型で宣言すると ArrayList 固有のメソッドも使えるが、List 型で宣言しておくほうが柔軟性が高い。
具体的には、List<Integer> を引数に取るメソッドがあれば、ArrayList でも LinkedList でも Stack でも渡すことができる。実装クラスを後から変えたくなったとき、変更箇所は new している1行だけに収まる。
// LinkedListに切り替えたい場合、ここだけ変えればよい
List<Integer> list = new LinkedList<>();
ArrayList 型で宣言していると、渡せる先がそのクラスに縛られる。実際には ArrayList 固有のメソッドを直接使いたい場面はほとんどないので、迷ったら List 型で宣言しておくほうが保守しやすい。
整理して気づいたこと
Arrays.asList() の罠と、toArray() がプリミティブ型に使えない点は、それぞれ一度詰まって覚えた知識だった。今回改めて理由を確認して、「なぜそうなるか」まで言語化できた気がする。
List 型で宣言する意味については以前の記事でも触れたが、今回 ArrayList 固有のメソッドとの対比で改めて整理できた。ArrayList 固有のメソッドが必要なケースが実際にどれくらいあるか、もう少し意識して見てみようと思っている。
この記事を書いた人について
株式会社Flexibilityでエンジニアをしています。
DX推進・システム開発を軸に、エンジニアが自律的に動ける環境を大事にしている会社です。
技術的に面白いことをやっていきたい方や、働き方に柔軟さを求めている方は、
よかったら一度のぞいてみてください。
- 会社サイト: https://www.flexi-inc.com/
- Qiita Organization: https://qiita.com/organizations/flexi-inc