背景
Javaを書き始めたころから List<String> list = new ArrayList<>() という書き方をしていた。
なぜ左辺が ArrayList ではなく List なのか、当時は「そういうものだ」と受け流していた。最近になって後輩から「なんでArrayListじゃダメなんですか」と聞かれ、うまく答えられなかった。聞かれた側が一番理解できていなかったという話で、改めて整理することにした。
ListはインタフェースでArrayListは実装クラス
まず前提として確認しておく。
List はインタフェースなので、それ単体からオブジェクトを作ることはできない。実際に動くオブジェクトを作るには、List を実装したクラスが必要になる。その代表が ArrayList と LinkedList だ。
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
class Main {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
}
}
右辺で new ArrayList<>() として実際のオブジェクトを生成しつつ、左辺の型を List として宣言する。この形が「インタフェース型で扱う」ということの実体だった。
ArrayListとLinkedListの違い
どちらも List インタフェースを実装しているが、内部の構造が異なる。
ArrayList はサイズ変更可能な配列として実装されていて、インデックスを使った要素へのアクセスが高速。その一方で、リストの途中への挿入や削除は、後続要素をずらす処理が発生するため時間がかかる。
LinkedList は各要素がノードとして存在し、前後のノードへの参照をつなぐことで構成されている。挿入・削除は参照の張り替えだけで済むので高速だが、特定インデックスへのアクセスは先頭から順にたどる必要があり遅くなる。
まとめるとこういう使い分けになる。
- 要素へのランダムアクセスが多い →
ArrayList - 先頭・末尾への挿入・削除が多い →
LinkedList
余談だが、実際の業務ではほとんど ArrayList しか使っていない。LinkedList が有利になる場面が思ったより少ないか、あるいは気づかずに ArrayList で代替している可能性がある。
なお Vector も可変長配列の実装クラスだが、スレッドセーフのための同期処理がオーバーヘッドになるため、マルチスレッドが不要な場面では ArrayList のほうがパフォーマンス面で優れている。現在は特別な理由がなければ使わない方向が一般的なようだ。
よく使うメソッド
List インタフェースには要素を操作するメソッドがいくつか用意されている。
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
System.out.println(list.get(1)); // "Banana"
list.remove(2);
System.out.println(list); // "[Apple, Banana]"
list.set(0, "Mango");
System.out.println(list); // "[Mango, Banana]"
}
}
add(index, element) でインデックスを指定して挿入、set(index, element) で置換、remove(index) で削除ができる。System.out.println(list) でそのままリストの内容が表示されるのは、AbstractCollection で実装された toString() が呼ばれているためだ。
拡張for文が使える理由
List で拡張for文が使えるのは、継承関係をたどると理由がわかる。
List は Collection を継承しており、その Collection が Iterable を継承している。Iterable インタフェースを実装していることが拡張for文を使える条件なので、List の実装クラスはすべてfor-eachで反復処理できる。
import java.util.ArrayList;
import java.util.List;
class Main {
public static void printList(List<String> list) {
for (String s : list) {
System.out.println(s);
}
}
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("orange");
printList(fruits);
}
}
通常のfor文より拡張for文のほうがパフォーマンス面でも有利とされている。インデックスを使う必要がない場合は積極的に使ったほうがよい。
本題:List型で宣言する意味
冒頭の話に戻る。なぜ ArrayList<String> list = new ArrayList<>() ではなく List<String> list = new ArrayList<>() と書くのか。
この printList メソッドを見ると、引数の型が List<String> になっている。
public static void printList(List<String> list) {
for (String s : list) {
System.out.println(s);
}
}
もし引数が ArrayList<String> であれば、LinkedList を渡したいときにこのメソッドを修正しなければならない。List<String> にしておけば、ArrayList でも LinkedList でも、List を実装したクラスであれば何でも受け取れる。
// ArrayListでもLinkedListでもprintListに渡せる
List<String> fruits1 = new ArrayList<>();
List<String> fruits2 = new LinkedList<>();
printList(fruits1);
printList(fruits2);
実装クラスを ArrayList から LinkedList に切り替えたとしても、printList の中身には手を入れる必要がない。変更の影響が new している1行だけに収まる。これが「インタフェース型で宣言するメリット」の実体だった。
見落としていたこと
List 型で変数を宣言する書き方は「そういう慣習」として受け入れていたが、実際には「実装の詳細を隠して、使う側のコードを安定させる」という設計上の理由があった。
後輩に聞かれたときに「そういうふうに書くんだよ」としか答えられなかったのは、この背景を把握できていなかったからだった。
ArrayList を直接型として使うと、後から LinkedList に切り替えたいときに変更箇所が増える。List で統一しておけば、実装の選択をあとから変えやすい。それだけのことだが、コードの保守性という観点からすると意外と大事な話だと感じた。
ArrayList と LinkedList のパフォーマンス差については、データ量や操作の種類によって結果が変わるため、必要があればベンチマークを取って確認するのが確実だと思っている。そこまで詰めたことがないので、機会を見て試してみたい。
この記事を書いた人について
株式会社Flexibilityでエンジニアをしています。
DX推進・システム開発を軸に、エンジニアが自律的に動ける環境を大事にしている会社です。
技術的に面白いことをやっていきたい方や、働き方に柔軟さを求めている方は、
よかったら一度のぞいてみてください。
- 会社サイト: https://www.flexi-inc.com/
- Qiita Organization: https://qiita.com/organizations/flexi-inc