【Java 21/25 LTS対応】今こそ学びたい、Sequenced Collections入門!コレクション操作の新しい常識で開発効率を上げる

はじめに:コレクションAPIの進化とJava 25時代の基礎

Java 25 LTSがリリースされた現在、Java開発は新たなステージに入りました。
しかし、最新の機能を最大限に活用するためには、その基盤となっている重要な改善を理解することが不可欠です。

本記事では、その代表例として、今なおモダンなコレクション操作に不可欠な、Java 21 LTSで導入された「Sequenced Collections (JEP 431)」を改めて掘り下げます。

Java 21以前、順序を持つコレクションの操作は必ずしも一貫していませんでした。
この課題を解決したのがSequenced Collectionsです。

本稿を通じて、この機能がJava 25時代においてもなぜ重要なのか、その基本から実践的な注意点までを解説します。


対象読者

  • Javaの基本的なプログラミング(特にListMapの使用経験)に慣れている方
  • Java 8や11などの古いバージョンから、Java 21や25への移行を検討している開発者
  • Javaのコレクションフレームワークに関する知識をアップデートし、よりモダンで効率的なコードを書きたいと考えているすべてのJavaエンジニア

目次


動作検証環境

この記事は、以下の環境を想定して執筆しています。

  • OS: macOS Tahoe 26.1
  • ハードウェア: MacBook Air (M3, 2024)
  • IDE: Visual Studio Code (最新版)
  • Java: OpenJDK 25.0.1 LTS (Temurin)
  • VS Code拡張機能: Extension Pack for Java (最新版)

1. Sequenced Collectionsとは?:新しいインターフェース群

まずは、今回導入されたインターフェースが、既存のコレクション・フレームワークの中でどのように位置づけられるのか、以下の図で全体像を確認しましょう。

コレクション・フレームワークの中でのSequenced Collectionsの位置づけ

Sequenced Collectionsは、要素の順序が定義されているコレクション(List, Deque, SortedSet, LinkedHashSet, LinkedHashMapなど)に対して、先頭・末尾へのアクセスや逆順でのイテレーションといった「順序付き操作」を標準化するためのものです。
これは既存のコレクションAPIに新しいメソッドを追加するのではなく、以下の3つの新しいインターフェースを導入し、既存のコレクションがこれらを実装するように変更することで実現されています。

  • SequencedCollection<E>
  • SequencedSet<E>
  • SequencedMap<K, V>

これらのインターフェースは、それぞれCollection, Set, Mapインターフェースを拡張しており、要素の順序に関する操作の統一的なビューを提供します。


1-1. SequencedCollection<E>

最も基本的なインターフェースで、要素の追加、取得、削除、逆順でのイテレーションなど、順序付きコレクションの共通操作を定義します。

主要メソッド:

  • E getFirst(): 最初の要素を取得します。
  • E getLast(): 最後の要素を取得します。
  • E removeFirst(): 最初の要素を削除して返します。
  • E removeLast(): 最後の要素を削除して返します。
  • void addFirst(E e): 先頭に要素を追加します。
  • void addLast(E e): 末尾に要素を追加します。
  • SequencedCollection<E> reversed(): 逆順のビューを返します(元のコレクションは変更されません)。

1-2. SequencedSet<E>

SequencedCollectionを拡張し、Setの特性(重複を許さない)を持つ順序付きセットの操作を定義します。
LinkedHashSetなどがこれに該当します。


1-3. SequencedMap<K, V>

SequencedCollectionを拡張し、キーと値のペアに対する順序付き操作を定義します。LinkedHashMap, TreeMapなどがこれに該当します。

主要メソッド (Map関連):

  • K firstKey(): 最初のキーを取得します。
  • K lastKey(): 最後のキーを取得します。
  • Map.Entry<K, V> firstEntry(): 最初のエントリを取得します。
  • Map.Entry<K, V> lastEntry(): 最後のエントリを取得します。
  • Map.Entry<K, V> pollFirstEntry(): 最初のエントリを削除して返します。
  • Map.Entry<K, V> pollLastEntry(): 最後のエントリを削除して返します。
  • V putFirst(K k, V v): 最初の位置にキーと値のペアを追加します。
  • V putLast(K k, V v): 最後の位置にキーと値のペアを追加します。
  • SequencedMap<K, V> reversed(): 逆順のビューを返します。

2. 従来コードとの比較:何がどう改善されたか?

Sequenced Collectionsの導入で、これまで冗長だったり、実装クラスに依存していたりしたコードが、どれほど簡潔で統一的になったかを見てみましょう。

操作Java 21以前 (煩雑な例)Java 21以降 (Sequenced I/F)ポイント
最初の要素取得list.get(0)list.getFirst()意図が明確。空の場合の例外もIndexOutOfBoundsExceptionからNoSuchElementExceptionに。
最後の要素取得list.get(list.size() - 1)list.getLast()煩雑なサイズ計算が不要に。
逆順ビューCollections.reverse(new ArrayList<>(list))list.reversed()元のリストを変更せず、コピーも不要なため効率的。

3. 具体的なコード例で見るSequenced Collectionsの活用

ここでは、Sequenced Collectionsの導入によって、どのようにコードが簡潔になり、可読性が向上するかを具体的なコード例で見ていきましょう。


3-1. リスト操作の簡潔化

import java.util.ArrayList;
import java.util.List;
import java.util.SequencedCollection;

public class SequencedListExample {
    public static void main(String[] args) {
        // SequencedCollectionはListを実装しているコレクションが実装
        List<String> fruits = new ArrayList<>();
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Cherry");

        // Java 21以前
        // String firstFruitOld = fruits.get(0);
        // String lastFruitOld = fruits.get(fruits.size() - 1);
        // ((LinkedList<String>)fruits).addFirst("Grape"); // ArrayListにはない

        // Java 21以降: SequencedCollectionとして操作可能
        SequencedCollection<String> sequencedFruits = (SequencedCollection<String>) fruits;

        // 最初と最後の要素に統一的にアクセス
        System.out.println("最初のフルーツ: " + sequencedFruits.getFirst()); // Apple
        System.out.println("最後のフルーツ: " + sequencedFruits.getLast());  // Cherry

        // 先頭・末尾に要素を追加
        // 注意: ArrayListでのaddFirstはO(n)のコストがかかり、要素数が多いと低速になる可能性があります。
        sequencedFruits.addFirst("Grape");
        sequencedFruits.addLast("Date");
        System.out.println("追加後のフルーツ: " + sequencedFruits); // [Grape, Apple, Banana, Cherry, Date]

        // 最初の要素を削除
        String removedFirst = sequencedFruits.removeFirst();
        System.out.println("削除された最初のフルーツ: " + removedFirst); // Grape
        System.out.println("削除後のフルーツ: " + sequencedFruits); // [Apple, Banana, Cherry, Date]

        // 逆順でイテレーション
        System.out.println("フルーツ (逆順):");
        sequencedFruits.reversed().forEach(System.out::println);
        // Date
        // Cherry
        // Banana
        // Apple
    }
}

実行結果例:

最初のフルーツ: Apple
最後のフルーツ: Cherry
追加後のフルーツ: [Grape, Apple, Banana, Cherry, Date]
削除された最初のフルーツ: Grape
削除後のフルーツ: [Apple, Banana, Cherry, Date]
フルーツ (逆順):
Date
Cherry
Banana
Apple

3-2. Map操作の改善

LinkedHashMapTreeMapのような順序付きマップでも、キーやエントリへのアクセスが統一されます。

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.SequencedMap;

public class SequencedMapExample {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> scores = new LinkedHashMap<>();
        scores.put("Alice", 90);
        scores.put("Bob", 85);
        scores.put("Charlie", 92);

        // Java 21以降: SequencedMapとして操作可能
        SequencedMap<String, Integer> sequencedScores = scores; // LinkedHashMapはSequencedMapを実装

        // 最初と最後のキー/エントリに統一的にアクセス
        System.out.println("最初のキー: " + sequencedScores.firstEntry().getKey()); // Alice
        System.out.println("最後のキー: " + sequencedScores.lastEntry().getKey());  // Charlie
        System.out.println("最初のエントリ: " + sequencedScores.firstEntry()); // Alice=90

        // 先頭・末尾にエントリを追加
        sequencedScores.putFirst("David", 95);
        sequencedScores.putLast("Eve", 88);
        System.out.println("追加後のスコア: " + sequencedScores); 
        // {David=95, Alice=90, Bob=85, Charlie=92, Eve=88}

        // 最後のエントリを削除
        Map.Entry<String, Integer> removedLast = sequencedScores.pollLastEntry();
        System.out.println("削除された最後のエントリ: " + removedLast); // Eve=88
        System.out.println("削除後のスコア: " + sequencedScores);
        // {David=95, Alice=90, Bob=85, Charlie=92}

        // キーを逆順でイテレーション
        System.out.println("スコアのキー (逆順):");
        sequencedScores.reversed().keySet().forEach(System.out::println);
        // Charlie
        // Bob
        // Alice
        // David
    }
}

実行結果例:

最初のキー: Alice
最後のキー: Charlie
最初のエントリ: Alice=90
追加後のスコア: {David=95, Alice=90, Bob=85, Charlie=92, Eve=88}
削除された最後のエントリ: Eve=88
削除後のスコア: {David=95, Alice=90, Bob=85, Charlie=92}
スコアのキー (逆順):
Charlie
Bob
Alice
David

4. Java 21以降の動向

Java 22から25にかけて、Sequenced Collections自体への直接的な変更はありませんでした。
これは、この機能がJava 21の時点で安定し、完成度の高いものであったことを示しています。

一方で、Stream Gatherers (JEP 461) のような、データ処理をさらに柔軟にする機能がプレビューとして導入されるなど、Javaのデータ操作エコシステムは進化を続けています。
しかし、コレクションの基本的な順序操作においては、Java 21で確立されたSequenced Collectionsが引き続き中核的な役割を担っています。


4-1. 実践ユースケース:シンプルなLRUキャッシュの実装

SequencedMap、特にLinkedHashMapの特性を活かすと、シンプルなLRU(Least Recently Used)キャッシュを驚くほど簡単に実装できます。

LinkedHashMapは、アクセス順(access-order)で要素を保持する特別なコンストラクタを持っており、最も長くアクセスされていない要素が自動的に特定されます。

pollFirstEntry()と組み合わせることで、この最も古いエントリを効率的に削除できますが、removeEldestEntryメソッドをオーバーライドするとさらに簡潔に実装できます。

import java.util.LinkedHashMap;
import java.util.Map;

public class LruCacheExample {

    // LinkedHashMapを継承してLRUキャッシュを実装
    static class LruCache<K, V> extends LinkedHashMap<K, V> {
        private final int capacity;

        public LruCache(int capacity) {
            // accessOrderをtrueに設定するのがLRUキャッシュの鍵
            super(capacity, 0.75f, true);
            this.capacity = capacity;
        }

        // 最も古いエントリを削除するかどうかを判断する
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            // サイズが容量を超えた場合にtrueを返し、最も古いエントリを削除
            return size() > capacity;
        }
    }

    public static void main(String[] args) {
        // 容量3のLRUキャッシュを作成
        LruCache<String, String> cache = new LruCache<>(3);

        System.out.println("キャッシュの初期状態: " + cache);

        cache.put("A", "Apple");
        cache.put("B", "Banana");
        cache.put("C", "Cherry");
        System.out.println("3つの要素を追加: " + cache);

        // "A"にアクセスすると、それが最も新しく使われたことになる
        cache.get("A");
        System.out.println("'A'にアクセス後: " + cache);

        // 新しい要素 "D" を追加すると、最も古く使われていない "B" が削除される
        cache.put("D", "Date");
        System.out.println("新しい要素'D'を追加後: " + cache);

        // SequencedMapの機能ももちろん使える
        System.out.println("現在の最初のキー (最も古く使われた): " + cache.firstEntry().getKey());
        System.out.println("現在の最後のキー (最も新しく使われた): " + cache.lastEntry().getKey());
    }
}

実行結果例:

キャッシュの初期状態: {}
3つの要素を追加: {A=Apple, B=Banana, C=Cherry}
'A'にアクセス後: {B=Banana, C=Cherry, A=Apple}
新しい要素'D'を追加後: {C=Cherry, A=Apple, D=Date}
現在の最初のキー (最も古く使われた): C
現在の最後のキー (最も新しく使われた): D

このキャッシュの put 操作、特に容量を超えた場合に最も古い要素を削除する removeEldestEntry のロジックは、フローチャートで示すと以下のようになります。

容量を超えた場合に最も古い要素を削除する removeEldestEntry のロジック

5. 実践的な注意点:パフォーマンスと不変性

Sequenced Collectionsは非常に便利ですが、効果的に使用するためにはいくつかの注意点を理解しておくことが重要です。


5-1. パフォーマンス特性を意識する

addFirst()removeFirst() のようなメソッドは、すべてのコレクションで同じパフォーマンスを発揮するわけではありません。性能はコレクションの内部的なデータ構造に大きく依存します。以下の比較表は、代表的な実装クラスにおける計算量(オーダー)の違いを示しています。

操作ArrayListLinkedList / ArrayDeque備考
addFirst() / removeFirst()O(n)O(1)先頭操作はLinkedList等が圧倒的に有利
addLast() / removeLast()O(1) (償却)O(1)末尾操作はどちらも高速
get(index)O(1)O(n)特定位置へのアクセスはArrayListが有利

この表からわかるように、ArrayListは内部が配列であるため、先頭に要素を追加・削除(addFirst/removeFirst)するには、後続の全要素を一つずつシフトする必要があります。このため、操作の計算量は O(n) となり、要素数が増えるほどパフォーマンスは顕著に低下します。

一方、LinkedListArrayDequeは、先頭や末尾への要素追加・削除が非常に効率的に行えるように設計されており、計算量は O(1) です。

指針:

  • 先頭/末尾の操作が頻繁に発生する場合(キュー、スタック、デックのような使い方): ArrayDequeLinkedListを選択することを強く推奨します。
  • 特定の位置へのランダムアクセス (get(index)) が主で、リストのサイズがあまり変わらない場合: ArrayListが最適です。

Sequenced Collectionsは便利な統一インターフェースを提供しますが、その裏側にある実装クラスの特性を理解し、ユースケースに応じて最適なクラスを選択することが、パフォーマンスの高いアプリケーションを構築する鍵となります。


5-2. 不変コレクションでは利用できない

Java 9以降、List.of()Set.of() といったファクトリメソッドで、手軽に不変(immutable)なコレクションを作成できるようになりました。これらのコレクションは、作成後に要素を変更することができません。

当然ながら、不変コレクションに対して addFirst()removeLast() のような変更操作を呼び出すと、UnsupportedOperationException がスローされます。

import java.util.List;
import java.util.SequencedCollection;

public class ImmutableExample {
    public static void main(String[] args) {
        // 不変リストを作成
        SequencedCollection<String> immutableList = List.of("A", "B", "C");

        try {
            // 変更操作を試みると例外が発生する
            immutableList.addFirst("Z"); 
        } catch (UnsupportedOperationException e) {
            System.out.println("例外が発生しました: " + e.getClass().getName());
            // -> 例外が発生しました: java.lang.UnsupportedOperationException
        }
    }
}

実行結果例:

例外が発生しました: java.lang.UnsupportedOperationException

この挙動は仕様通りであり、Sequenced Collectionsのインターフェースを実装しているからといって、すべてのコレクションが変更可能になるわけではないことを覚えておく必要があります。


5-3. 注意点:reversed()が返すのは「変更可能なビュー」

reversed()メソッドが返すのは、元のコレクションのデータをコピーした新しいコレクションではなく、あくまで「逆順に見せるためのビュー」です。そのため、ビューや元のコレクションに対する変更は互いに影響し合う点に注意が必要です。この挙動は、特にコレクションが変更可能(mutable)な場合に重要となります。

import java.util.ArrayList;
import java.util.SequencedCollection;
import java.util.List;

public class ReversedViewExample {
    public static void main(String[] args) {
        // 変更可能なリストを作成
        List<String> originalList = new ArrayList<>(List.of("A", "B", "C"));
        System.out.println("元のリスト: " + originalList);

        // 逆順のビューを取得
        SequencedCollection<String> reversedView = originalList.reversed();
        System.out.println("逆順ビュー: " + reversedView);

        System.out.println("\n--- 元のリストを変更 ---");
        originalList.add("D");
        System.out.println("変更後の元のリスト: " + originalList);
        System.out.println("変更後の逆順ビュー: " + reversedView); // ビューにも変更が反映される

        System.out.println("\n--- 逆順ビューを変更 ---");
        reversedView.addFirst("Z"); // ビューの先頭に追加 = 元のリストの末尾に追加
        System.out.println("変更後の逆順ビュー: " + reversedView);
        System.out.println("変更後の元のリスト: " + originalList); // 元のリストにも変更が反映される
    }
}

実行結果例:

元のリスト: [A, B, C]
逆順ビュー: [C, B, A]

--- 元のリストを変更 ---
変更後の元のリスト: [A, B, C, D]
変更後の逆順ビュー: [D, C, B, A]

--- 逆順ビューを変更 ---
変更後の逆順ビュー: [Z, D, C, B, A]
変更後の元のリスト: [A, B, C, D, Z]

このように、reversed()は元のコレクションと連動するビューを返すため、予期せぬ副作用を生まないよう、その特性を理解した上で利用することが重要です。


6. Sequenced Collectionsのメリットとユースケース

Sequenced Collectionsの導入により、開発者は以下のようなメリットを享受できます。

  • 可読性の向上:
    • getFirst(), getLast() などの直感的なメソッド名により、コードが何をしているのかが明確になります。
  • 型安全と操作の一貫性:
    • コレクションの実装に依存せず、統一的なインターフェースを通じて順序付き操作を行えるため、コードの堅牢性と型安全性が向上します。
  • APIの簡素化:
    • 開発者はListDequeのような具体的なクラスを意識することなく、順序付きコレクション全般に対する共通の操作を適用できます。
  • 新しいユースケースのサポート:
    • 履歴管理システムでの最新・最古アイテムへの効率的なアクセス、LRU(Least Recently Used)キャッシュの実装、イベントキューでの処理順序の厳密な制御など、順序が重要な場面での開発が容易になります。

まとめ:LTSの進化を支える普遍的価値

Java 21のSequenced Collectionsは、Java開発者が日常的に行うコレクション操作を、より簡潔、直感的、かつ型安全にするための重要な進化でした。

Java 21で導入されたこの機能は、最新のLTSであるJava 25に至るまで、その価値を失うことのない基本的な改善です。2年ごとのLTSリリースサイクルが定着した今、Java 21のような過去のLTSで導入された堅牢な機能を理解することは、最新のJavaを効果的に使いこなす上で極めて重要と言えるでしょう。ぜひ自身のプロジェクトにSequenced Collectionsを取り入れ、そのメリットを体感してください。


免責事項

  • 本記事に掲載された情報は、執筆時点での正確性を期すよう努めていますが、その内容の完全性や正確性についていかなる保証もするものではありません。
  • 記事内で提供されるコードスニペットは、特定の環境下での動作を想定したものです。ご自身の環境で利用する際は、十分なテストと検証をご自身の責任において行ってください。
  • 本記事の情報を利用したことによって生じたいかなる損害についても、著者は一切の責任を負いかねますので、あらかじめご了承ください。

SNSでもご購読できます。

コメントを残す

*