1. 従来のコレクションにおける順序の問題
Java には、要素の順序を保証するコレクション(例: ArrayList、LinkedList、LinkedHashSet、LinkedHashMap)と、順序を保証しないコレクション(例: HashSet、HashMap)が昔から存在します。しかし、これらすべてのコレクションに共通の欠点がありました。つまり、いくつかのコレクションは特定の順序で要素を保持しているにもかかわらず、標準インターフェース(List、Set、Map)は先頭・末尾要素へのアクセスや順序の反転に関する汎用メソッドを提供していなかったのです。
例えば、List<String> があれば list.get(0) で先頭要素を取得できますが、Set や Map ではその手は使えません。イテレータを使ったり、余分なコードを書く必要がありました。これはあまり便利とは言えず、可読性も落ちます。
たとえるなら、引き出しのある棚があるのに、最初や最後の引き出しを取り出すたびに毎回すべて数え直さなければならないようなものです。専用の「最初」「最後」の取っ手があったらうれしいですよね。
SequencedCollection と関連インターフェースの登場
Java 21 で新しいコレクション・インターフェースが導入されました:
- SequencedCollection<E>
- SequencedSet<E>
- SequencedMap<K, V>
これらのインターフェースは標準コレクションを拡張し、要素の順序を扱うための統一的なアプローチを導入します。これにより、順序が重要なあらゆるコレクションに対して、具体的な実装を気にせず汎用的なコードを書けるようになりました。
これはどういうものか
- SequencedCollection — 要素に明確な順序があり、先頭・末尾の要素取得や順序の反転が簡単にできるコレクション。
- SequencedSet — 同様だが集合(重複なし)に対するもの。
- SequencedMap — 同様だがマップ(キーと値)に対するもの。
どのコレクションが実装しているか
Java 21 では、次の標準コレクションが新しいインターフェースを実装しています:
- ArrayList、LinkedList → SequencedCollection
- LinkedHashSet、TreeSet → SequencedSet
- LinkedHashMap、TreeMap → SequencedMap
つまり、すでにこれらのコレクションを使っているなら、何もしなくても新しい機能を自動的に利用できます。
2. SequencedCollection、SequencedSet、SequencedMap の主なメソッド
SequencedCollection のメソッド
E getFirst(); // 先頭要素を取得
E getLast(); // 末尾要素を取得
SequencedCollection<E> reversed(); // 逆順のビューを取得
SequencedSet のメソッド
SequencedCollection と同じメソッドに加え、Set の機能すべてを備えます。
SequencedMap のメソッド
Map.Entry<K, V> firstEntry(); // 先頭のエントリ(キーと値)
Map.Entry<K, V> lastEntry(); // 末尾のエントリ(キーと値)
SequencedMap<K, V> reversed(); // 逆順の Map
3. 新しいインターフェースの使い方の例
例 1: 先頭と末尾の取得
import java.util.*;
public class SequencedDemo {
public static void main(String[] args) {
SequencedCollection<String> sc = new ArrayList<>();
sc.add("Java");
sc.add("Python");
sc.add("Kotlin");
// 先頭と末尾の要素を取得
String first = sc.getFirst(); // "Java"
String last = sc.getLast(); // "Kotlin"
System.out.println("先頭: " + first);
System.out.println("末尾: " + last);
}
}
出力:
先頭: Java
末尾: Kotlin
例 2: コレクションの反転
SequencedCollection<String> sc = new LinkedList<>();
sc.add("A");
sc.add("B");
sc.add("C");
SequencedCollection<String> reversed = sc.reversed();
System.out.println(reversed); // [C, B, A]
注意: reversed() はコレクションを逆順に見せるビューを返します。元のコレクションを変更すると、reversed ビューも変更されます!
例 3: SequencedSet の利用
SequencedSet<Integer> set = new LinkedHashSet<>();
set.add(100);
set.add(200);
set.add(300);
System.out.println("先頭要素: " + set.getFirst()); // 100
System.out.println("末尾要素: " + set.getLast()); // 300
SequencedSet<Integer> reversedSet = set.reversed();
System.out.println(reversedSet); // [300, 200, 100]
例 4: SequencedMap の利用
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("apple", 5);
map.put("banana", 3);
map.put("cherry", 7);
Map.Entry<String, Integer> first = map.firstEntry();
Map.Entry<String, Integer> last = map.lastEntry();
System.out.println("先頭: " + first.getKey() + " = " + first.getValue()); // apple = 5
System.out.println("末尾: " + last.getKey() + " = " + last.getValue()); // cherry = 7
SequencedMap<String, Integer> reversedMap = map.reversed();
System.out.println(reversedMap); // {cherry=7, banana=3, apple=5}
4. これがアプリケーションとどう関係するのか?
たとえば学習用アプリでログインしたユーザーの一覧を保持しており、「最初にログインした人」「最後にログインした人」を素早く取得したいとします。以前は次のように書く必要がありました:
List<String> users = new ArrayList<>();
// ... ユーザーを追加する
String first = users.get(0);
String last = users.get(users.size() - 1);
しかし、コレクションがリストではなく、例えば順序を保つ LinkedHashSet(要素は一意)である場合、この手は使えません:
Set<String> users = new LinkedHashSet<>();
// ... ユーザーを追加する
// 先頭をどう取得する? イテレータでしかできない:
String first = users.iterator().next();
// では末尾は? 全要素を走査するしかない!
今では、もっと簡単かつ汎用的に書けます:
SequencedSet<String> users = new LinkedHashSet<>();
// ... ユーザーを追加する
String first = users.getFirst();
String last = users.getLast();
これによりコード量が減るだけでなく、より読みやすく安全なコードになります。
5. 新しいインターフェースの関係図
classDiagram
Collection <|-- SequencedCollection
List <|-- SequencedCollection
Set <|-- SequencedSet
Map <|-- SequencedMap
SequencedCollection <|-- SequencedSet
SequencedSet <|-- LinkedHashSet
SequencedSet <|-- TreeSet
SequencedCollection <|-- ArrayList
SequencedCollection <|-- LinkedList
SequencedMap <|-- LinkedHashMap
SequencedMap <|-- TreeMap
6. 役立つポイント
SequencedCollection の実用上の利点
- 順序操作のための統一インターフェース: どこで get(0) が使えて、どこでイテレータが必要で、どこではそもそも先頭が取れないのか—覚える必要がなくなります。
- キューやスタックの操作が容易: 先頭(head)と末尾(tail)を簡単に取得できます。
- 安全性と可読性: コレクションの誤用によるバグが減り、コード自体が自己文書化的になります。
- コレクションの素早い反転: reversed() により手作業なしで簡単に逆順が得られます。
- 保守や拡張が容易: 将来、例えば ArrayList を LinkedHashSet に置き換えても、SequencedCollection を使うコードは書き換え不要です。
実装上の注意点
- すべてのコレクションが SequencedCollection を実装するわけではない: 例えば HashSet や HashMap は順序を保証しないため、新しいインターフェースを実装しません。
- メソッドは例外を投げることがある: コレクションが空のときに getFirst() や getLast() を呼ぶと NoSuchElementException が発生します。空でないことを確認しましょう!
- reversed() はビューでありコピーではない: 元のコレクションの変更は逆順ビューにも反映され、逆もまた同様です。
- 互換性: 新しいインターフェースは Java 21 以降でのみ利用できます。より古い Java を使っている場合はまだ利用できません(これを機にアップデートを検討しましょう)。
- Generics: 新しいインターフェースは Generics を完全にサポートしており、あらゆるデータ型で利用できます。
7. SequencedCollection 利用時のよくあるミス
エラー1: 順序を保証しないコレクションに順序を期待してしまう。 HashSet を SequencedSet にキャストしようとするとコンパイルエラーになります。HashSet には順序がなく、このインターフェースを実装していないためです。
エラー2: 空のコレクションを無視する。 空のコレクションで getFirst() や getLast() を呼ぶと例外が投げられます。これらのメソッドを呼ぶ前に、コレクションが空でないことを確認してください:
if (!sc.isEmpty()) {
String first = sc.getFirst();
}
エラー3: reversed() に関する誤解。 reversed() はビューでありコピーではありません。reversed ビューを変更すると元のコレクションも変更され(その逆も同様)、想定外の結果を生むことがあります。
エラー4: 古い Java バージョンで新しいインターフェースを使おうとする。 プロジェクトのコンパイル対象が Java 21 より古い場合、コンパイラはこれらのインターフェースを見つけられません。プロジェクト設定で JDK のバージョンを確認しましょう!
GO TO FULL VERSION