この記事を書いた理由
List も Set も Map も、なんとなく「Javaでデータをまとめて扱うやつ」という認識のまま数年使ってきた。
ある日、コードレビューで「ここはListよりDequeのほうが意図が伝わりやすい」と指摘をもらった。そのとき「DequeってQueueと何が違うんでしたっけ」と聞き返してしまい、自分でも驚いた。使えてはいるが、整理されていない。そう気づいたので、改めて構造から確認することにした。
まず全体像を把握する
コレクションフレームワークは大きく2つのグループに分けられる。
java.util.Collection を頂点とするグループと、java.util.Map のグループだ。
Map は Collection を継承していないため、厳密にはコレクションではない。ただし keySet()、values()、entrySet() という3つのコレクションビューを持っており、それを通じてコレクションとして扱うことができる。
Collection 配下のインタフェースはこのような構造になっている。
Iterable
└── Collection
├── List
├── Set
├── Queue
│ └── Deque
Collection 自体は Iterable を継承しているため、そのサブクラスはすべて拡張for文で反復処理できる。この継承関係を意識しておくと、「なぜこのクラスでfor-eachが使えるのか」が自然に理解できる。
各インタフェースの特徴と使い分け
List:順序とインデックス
インデックスで要素にアクセスできる順序付きリスト。実装クラスとしてよく使うのは ArrayList と LinkedList。
import java.util.List;
import java.util.ArrayList;
class Main {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
System.out.println(list); // [1, 2, 3]
System.out.println(list.get(1)); // 2
for (Integer i : list) {
System.out.println(i);
}
}
}
Vector と Stack も List の実装クラスだが、現在は推奨されていない。スレッドセーフが必要な場面では Collections.synchronizedList、スタック操作が必要な場合は Deque を使うのが現在の主流のようだ。
Set:重複なし、順序なし
重複を許さないコレクション。順序は保持しない。HashSet はハッシュテーブルで要素を管理し、TreeSet は自然順序(昇順)で保持する。
内部実装が HashMap や TreeMap に処理を委譲しているというのは知らなかった。Set と Map が独立した別物に見えて、実は深いところで繋がっているのは少し意外だった。
Queue:FIFOの一方向キュー
先入れ先出し(FIFO)のデータ構造。offer() でエンキュー、poll() でデキュー、peek() で先頭の参照だけ行う。
import java.util.ArrayDeque;
import java.util.Queue;
class Main {
public static void main(String[] args) {
Queue<String> queue = new ArrayDeque<>();
queue.offer("Alice");
queue.offer("Bob");
queue.offer("Charlie");
System.out.println(queue.peek()); // "Alice"(削除しない)
while (queue.peek() != null) {
System.out.println(queue.poll()); // 順番に取り出す
}
}
}
LinkedList も Queue の実装クラスだが、ArrayDeque のほうがパフォーマンスが良いとされている。
Deque:両端から操作できるキュー
Queue を継承したインタフェースで、先頭・末尾の両端から追加・削除ができる。FIFOにもLIFOにも使える柔軟さがある。
import java.util.ArrayDeque;
import java.util.Deque;
class Main {
public static void main(String[] args) {
Deque<String> deque = new ArrayDeque<>();
deque.addFirst("Alice");
deque.addLast("Bob");
deque.addLast("Charlie");
System.out.println(deque.peekFirst()); // "Alice"
System.out.println(deque.peekLast()); // "Charlie"
while (deque.peekFirst() != null) {
System.out.println(deque.pollFirst());
}
}
}
スタックとして使いたい場面では Stack クラスではなく Deque が推奨されている理由は、Deque のほうが操作の表現が明示的で、パフォーマンスも優れているから。レビューで指摘を受けたのはまさにここだった。
余談だが、Deque の読み方を長らく「ディーキュー」と思っていたが、正しくは「デック」らしい。気にしないようにしていたが、口頭で出てくるたびに少し迷う。
Map:キーと値のペア
Collection を継承していないため独立した扱いになるが、コレクションフレームワークの一員ではある。
import java.util.HashMap;
import java.util.Map;
class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 100);
map.put("Bob", 90);
map.put("Charlie", 80);
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
}
}
entrySet() を使うと、キーと値をまとめて取り出せる。keySet() でキーを取り出してから get() で値を引くより、entrySet() を使ったほうが内部的なアクセスが1回で済む分効率がよい。
理解が浅かった部分
Queue と Deque の使い分けができていなかったのが一番の盲点だった。どちらも「キュー」という認識のまま、なんとなく LinkedList を使い続けていた。
Deque は Queue を継承しているため Queue として使うこともできる。つまり「先入れ先出しだけでいいなら Queue、両端の操作が必要なら Deque」という判断が正しい。実装クラスとして ArrayDeque を選べば、どちらの用途にも対応できる。
また、Map が Collection を継承していない点を意識せずにいたため、Collection のメソッドがそのまま使えると誤解していた時期があった。Map に iterator() が直接ないのはそのためで、反復処理したい場合は entrySet() や keySet() を経由してIteratorを得る必要がある。
整理して気づいたこと
今回改めて確認できたのは、コレクションフレームワークは継承構造ごと把握すると迷いが減るという点だった。
どのクラスがどのインタフェースを実装しているかを知っておくと、「なぜこのクラスでfor-eachが使えるのか」「なぜこのメソッドが使えないのか」が構造から説明できるようになる。使えるかどうかを都度調べるより、継承関係を頭に入れておくほうが長い目で見て楽だと感じた。
TreeSet や TreeMap の自然順序の仕組み、Comparator を使ったカスタムソートについてはまだ曖昧なところが残っているので、次の機会に整理したい。
この記事を書いた人について
株式会社Flexibilityでエンジニアをしています。
DX推進・システム開発を軸に、エンジニアが自律的に動ける環境を大事にしている会社です。
技術的に面白いことをやっていきたい方や、働き方に柔軟さを求めている方は、
よかったら一度のぞいてみてください。
- 会社サイト: https://www.flexi-inc.com/
- Qiita Organization: https://qiita.com/organizations/flexi-inc