順序付きコレクション、セットおよびマップの作成

JDK 21で導入された3つのインタフェースは、検出順序が定義されたコレクションを表します。各コレクションには、最初の要素、2番目の要素など、最後の要素まで明確に定義されています。これらは、最初と最後の要素にアクセスし、要素を前方および逆順に処理するための統一されたAPIを提供します。

JDK 21より前は、Java Collections Frameworkには、検出順序が定義された要素のシーケンスを表すコレクション型がありませんでした。たとえば、ListおよびDequeは検出順序を定義しましたが、共通のスーパータイプCollectionは定義しませんでした。同様に、SetおよびHashSetなどのサブタイプでは検出順序は定義されませんが、SortedSetLinkedHashSetなどのサブタイプでは定義されます。検出順序が定義されたコレクション型がないため、検出順序を考慮する統一された操作のセットはありません。検出順序を考慮する操作はありますが、これらは統一されていません。

Collections Frameworkで共通の順序が重要な操作が欠落している例は、DequeListの最初の要素を取得する場合です。Dequeの最初の要素を取得するには、getFirst()メソッドを使用します。一方、Listの最初の要素を取得するには、get(0)を使用します。

検出順序のサポートが型階層に分散していたため、APIで特定の有用な概念を表現することは困難でした。CollectionListも、検出順序を持つパラメータまたは戻り値を記述できません。Collectionは一般的すぎたため、このような制約が仕様に格下げされ、デバッグが難しいエラーを引き起こす可能性があります。検出順序が定義されたコレクションを受け取るAPIの場合、ListSortedSetおよびLinkedHashSetを除外したため、限定的すぎました。関連する問題は、ビュー・コレクションが多くの場合により弱いセマンティクスへのダウングレードを強制されることでした。たとえば、LinkedHashSetCollections::unmodifiableSetでラップすると、検出順序に関する情報を破棄するSetが生成されます。

それらを定義するインタフェースがないと、検出順序に関連する操作は一貫性がないか欠落していました。多くの実装では最初または最後の要素の取得がサポートされていますが、各コレクションは独自のアプローチを定義し、その一部は明確でないか、完全に欠落しています。

順序付きインタフェースを持つCollections Frameworkの再調整

JDK 21以降、JEP 431では、順序付きコレクション、順序付きセットおよび順序付きマップを作成するための3つのJava Collections Frameworkインタフェースが導入されています。
これら3つのインタフェースは、Java Collections Frameworkに、検出順序が定義された一連の要素を表すコレクション型と、コレクション全体に適用される均一な操作セットを提供します。次の図に示すように、インタフェースはコレクション型の階層に収まります。

図5-1 順序付きインタフェースを持つCollections Framework

図5-1の説明が続きます
「図5-1 順序付きインタフェースを持つCollections Framework」の説明
この図は、SequencedCollectionSequencedSetおよびSequencedMapインタフェースをクラスおよびインタフェースのJava Collections Framework階層に統合した次の調整を示しています。
  • Listには、直接のスーパーインタフェースとしてSequencedCollectionがあります。
  • Dequeには、直接のスーパーインタフェースとしてSequencedCollectionがあります。
  • LinkedHashSetSequencedSetを実装します。
  • SortedSetには、直接のスーパーインタフェースとしてSequencedSetがあります。
  • LinkedHashMapSequencedMapを実装します。
  • SortedMapには、直接のスーパーインタフェースとしてSequencedMapがあります。
  • reversed()メソッドの共変オーバーライドは、適切な場所で定義されます。たとえば、List::reversedは、SequencedCollection型の値ではなくList型の値を返すようにオーバーライドされます。
  • Collectionsユーティリティ・クラスに追加されたメソッドは、次の3つの新しい型に対して変更不可能なラッパーを作成します。
    • Collections.unmodifiableSequencedCollection(sequencedCollection)
    • Collections.unmodifiableSequencedSet(sequencedSet)
    • Collections.unmodifiableSequencedMap(sequencedMap)

順序付きコレクション、順序付きセットおよび順序付きマップのインタフェースに関する背景情報については、JEP 431を参照してください。

SequencedCollection

SequencedCollectionは、JDK 21で追加されたコレクション型であり、検出順序が定義された要素の順序を表します。

SequencedCollectionには、最初の要素と最後の要素があり、それらの間に後続要素と先行要素があります。SequencedCollectionは、いずれかの端での一般的な操作をサポートし、最初から最後、最後から最初まで(順方向、逆方向など)の要素の処理をサポートします。

interface SequencedCollection<E> extends Collection<E> {
    SequencedCollection<E> reversed();
    // methods promoted from Deque
    void addFirst(E);
    void addLast(E);
    E getFirst();
    E getLast();
    E removeFirst();
    E removeLast();
}

reversed()メソッドは、元のコレクションの逆順ビューを提供します。元のコレクションに対するすべての変更がビューに表示されます。

返されるビュー内の要素の出現順序は、このコレクション内の要素の出現順序の逆です。逆の順序付けは、返されるビューのビュー・コレクションに対するものを含むすべての順序依存操作に影響します。

実装に応じて、基礎となるコレクションに対する変更が逆順ビューに表示される場合と表示されない場合があります。許可されている場合、ビューに対する変更は元のコレクションに「ライトスルー」されます。逆順ビューでは、通常のすべての反復処理メカニズムを使用して、さまざまな順序付けされた型で要素を両方向で処理できます。
  • 拡張されたforループ
  • 明示的なiterator()ループ
  • forEach()
  • stream()
  • parallelStream()
  • toArray()
たとえば、LinkedHashSetから逆順のストリームを取得することは、以前はかなり困難でしたが、現在は単純です。
linkedHashSet.reversed().stream()

ノート:

reversed()メソッドは、基本的には名前が変更されたNavigableSet::descendingSetであり、SequencedCollectionに昇格されます。
SequencedCollectionの次のメソッドは、Dequeから昇格されます。両端の要素の追加、取得および削除をサポートします。
  • void addFirst(E)
  • void addLast(E)
  • E getFirst()
  • E getLast()
  • E removeFirst()
  • E removeLast()

    add*(E)およびremove*()メソッドはオプションであり、主に変更できないコレクションのケースをサポートします。コレクションが空の場合、get*()およびremove*()メソッドはNoSuchElementExceptionをスローします。サブインタフェースに競合する定義があるため、SequencedCollectionにはequals()およびhashCode()の定義がありません。

SequencedSet

SequencedSetは、SequencedCollectionSetの両方です。

SequencedSetは、適切に定義された検出順序を持つSet、または一意の要素を持つSequencedCollectionと考えることができます。

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
    SequencedSet<E> reversed();    // covariant override
}

このインタフェースの要件は、Set.equalsおよびSet.hashCodeで定義されているequalsおよびhashCodeメソッドと同じです。SetおよびSequencedSetは、順序に関係なく、等しい要素がある場合にのみequalsを比較します。

SequencedSetは、このセットの逆順ビューを提供するreversed()メソッドを定義します。SequencedCollection.reversedメソッドとの唯一の違いは、SequencedSet.reversedの戻り型がSequencedSetであることです。

SequencedSetでは、SequencedCollectionadd*(E)メソッドによって次が実行されます。
  • addFirst(E) - 要素をコレクションの最初の要素として追加します。
  • addLast(E) - 要素をコレクションの最後の要素として追加します。

SequencedCollectionadd*(E)メソッドには、LinkedHashSetおよびSortedSetの次の特殊ケースの動作もあります。

LinkedHashSetの特殊ケースの動作:

  • addFirst(E)およびaddLast(E)メソッドには、LinkedHashSetなどのコレクションの特殊ケースのセマンティクスがあります。LinkedHashSetは、セット内にすでに存在する場合にエントリを再配置します。要素がセット内にすでに存在する場合は、適切な位置に移動されます。これにより、LinkedHashSetの長期にわたる不備、つまり要素を再配置できない状況が修正されます。

SortedSetの特殊ケースの動作:

  • 相対比較によって要素を配置するSortedSetなどのコレクションでは、SequencedCollectionスーパーインタフェースで宣言されたaddFirst(E)メソッドやaddLast(E)メソッドなどの明示的な配置操作をサポートできません。これらのメソッドは、UnsupportedOperationExceptionをスローします。

SequencedMap

SequencedMapには、マップの検出順序のいずれかの端でマッピングの追加、マッピングの取得およびマッピングの削除を行うメソッドが用意されています。このインタフェースは、このマップの逆順ビューを提供するreversed()メソッドも定義します。

SequencedMapには、両端での操作をサポートする、可逆的な検出順序が明確に定義されています。マップの逆順ビューは通常、元のマップがシリアライズ可能であってもシリアライズ可能ではありません。SequencedMapの検出順序はSequencedCollectionの要素の検出順序と似ていますが、順序付けは個々の要素ではなくマッピングに適用されます。

interface SequencedMap<K,V> extends Map<K,V> {
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);
    // methods promoted from NavigableMap
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}

sequencedKeySet()sequencedValues()およびsequencedEntrySet()メソッドは、MapインタフェースのkeySet()values()およびentrySet()メソッドとまったく同じです。これらのすべてのメソッドは、基礎となるコレクションのビューを返します。ビューに対する変更は基礎となるコレクションに表示され、その逆も同様です。これらのビューの検出順序は、基礎となるマップの検索順序に正確に対応します。

SequencedMapインタフェース・メソッドとMapのメソッドの違いは、sequenced*()メソッドに順序付き戻り型があることです。
  • SequencedSet<K> sequencedKeySet()では、実装はマップのkeySetSequencedSetビューを返し、次のように動作します。
    • addおよびaddAllメソッドはUnsupportedOperationExceptionをスローします。
    • reversedメソッドは、マップの逆順ビューのsequencedKeySetビューを返します。
    • その他のメソッドは、マップのkeySetビューの対応するメソッドを呼び出します。
  • SequencedCollection<V> sequencedValues()では、実装はマップのvaluesコレクションのSequencedCollectionビューを返し、次のように動作します。
    • addおよびaddAllメソッドはUnsupportedOperationExceptionをスローします。
    • reversedメソッドは、マップの逆順ビューのsequencedValuesビューを返します。
    • equalsおよびhashCodeメソッドは、Objectから継承されます。
    • その他のメソッドは、マップのvaluesビューの対応するメソッドを呼び出します。
  • SequencedSet<Entry<K,V>> sequencedEntrySet()では、実装はマップのentrySetSequencedSet ビューを返し、次のように動作します。
    • addおよびaddAllメソッドはUnsupportedOperationExceptionをスローします。
    • reversedメソッドは、マップの逆順ビューのsequencedEntrySetビューを返します。
    • その他のメソッドは、マップのentrySetビューの対応するメソッドを呼び出します。
put*(K, V)メソッドには、SequencedSetの対応するadd*(E)メソッドと同様に、特殊ケースのセマンティクスがあります。
  • LinkedHashMapなどのマップでは、エントリがマップにすでに存在する場合にエントリを再配置する追加の効果があります。
  • SortedMapなどのマップの場合、これらのメソッドはUnsupportedOperationExceptionをスローします。
SequencedMapの次のメソッドは、NavigableMapから昇格されます。両端のエントリの取得および削除をサポートします。
  • Entry<K, V> firstEntry()
  • Entry<K, V> lastEntry()
  • Entry<K, V> pollFirstEntry()
  • Entry<K, V> pollLastEntry()

メソッドfirstEntry()lastEntry()pollFirstEntry()およびpollLastEntry()は、呼出し時点でのマッピングのスナップショットを表すMap.Entryインスタンスを返します。これらは、オプションのsetValueメソッドを介した基礎となるマップの変更をサポートしません

ArrayListおよびLinkedHashMapの逆順ビューのデモンストレーション

Collections Frameworkで順序付きインタフェースを使用するシナリオがいくつか用意されています。

トピック

コレクションの逆順ビューのデモンストレーション

次の例は、順序付きインタフェースのreversed()メソッドによってコレクションの逆順ビューがどのように生成されるか、逆順ビューに対する変更が元のコレクションにどのように影響するか、および元のコレクションに対する変更が逆順ビューでどのように表示されるかを示しています。

逆順ビューは「ライブ」であり、コレクションのスナップショットではありません。この特性は、ArrayListおよびその逆順ビューを使用した次の例に示されています。

ノート:

次のコード例には、必須でないjshell出力は含まれていません。

jshellセッションを開始し、ArrayListクラスを使用してStringオブジェクトのリストを作成します。

jshell> var list = new ArrayList<>(Arrays.asList("a", "b", "c", "d", "e"))
list ==> [a, b, c, d, e]

次に、reversed()メソッドを使用して、コレクションの逆順ビューを生成します。

jshell> var rev = list.reversed()
rev ==> [e, d, c, b, a]

逆順ビューを変更すると、元のコレクションに影響します。逆順ビューのエントリとしてfを追加し、それが元のコレクションに追加されていることを確認します。

jshell> rev.add(1, "f")
jshell> rev
rev ==> [e, f, d, c, b, a]
jshell> list
list ==> [a, b, c, d, f, e]

元のコレクションを変更すると、変更内容が逆順ビューに表示されます。索引2の要素をXに設定し、それがコレクションに追加されたことを確認してから、変更したコレクションの逆順ビューを生成します。

jshell> list.set(2, "X")
jshell> list
list ==> [a, b, X, d, f, e]
jshell> rev
rev ==> [e, f, d, X, b, a]

LinkedHashMapビューの構成のデモンストレーション

ArrayListを使用する以外に、reversed()ビューは、List.subList().reversed()SequencedMap.sequencedKeySet().reversed()SequencedMap.reversed().sequencedKeySet()などの他のビューで構成することもできます。

SequencedMap.sequencedKeySet().reversed()ビューとSequencedMap.reversed().sequencedKeySet()ビューは機能的に同等で、次のコード例のLinkedHashMapクラスを使用して説明されています。

jshellセッションを開始し、LinkedHashMapクラスを使用してStringオブジェクトのmapを作成します。

jshell> var map = new LinkedHashMap<String, Integer>()
jshell> map.put("a", 1)
jshell> map.put("b", 2)
jshell> map.put("c", 3)
jshell> map.put("d", 4)
jshell> map.put("e", 5)
map ==> {a=1, b=2, c=3, d=4, e=5}

次に、reversed()メソッドを使用して、元のコレクションのkeySetビューの逆順ビューを生成します。

jshell> map.sequencedKeySet().reversed()
$17 ==> [e, d, c, b, a]

SequencedMapでは基礎となるマップの変更がサポートされないことのデモンストレーション

このデモンストレーションでは、SequencedMapセクションの最後の文を示します。firstEntry()lastEntry()pollFirstEntry()およびpollLastEntry()メソッドは、オプションのsetValueメソッドを使用した基礎となるマップの変更をサポートしていません。

これらのメソッドでsetValue()を使用して基礎となるマップのエントリを変更しようとすると、UnsupportedOperationExceptionがスローされます。これは、entrySet反復によって取得されるマップ・エントリの変更とは対照的です。seqmap.entrySet().iterator().next()を呼び出してマップ・エントリを返し、エントリでsetValue()を呼び出すと、元のマップが変更されます。

jshellセッションを開き、「LinkedHashMapビューの構成のデモンストレーション」で生成されたマップを使用します。

map.entrySet().iterator().next()を呼び出して、最初のマップ・エントリを返します。

jshell> var entry = map.entrySet().iterator().next()
entry ==> a=1

setValue()を使用して、マップ・エントリの値を77に変更します。エントリは、entrySet反復することによって取得されたため、元のmapで変更できます。mapの値が77に変更されたことを確認します。

jshell> entry.setValue(77)
$19 ==> 1
jshell> map
map ==> {a=77, b=2, c=3, d=4, e=5}

ノート:

イテレータによって返されるエントリでsetValue()を呼び出す機能は、JDK 21で新たに導入された動作ではありません。

setValue()を使用して、マップ・エントリを999に変更してみます。マップ・エントリはentrySetで反復して取得されなかったため、UnsupportedOperationExceptionがスローされます。

jshell> entry = map.firstEntry()
entry ==> a=77
jshell> entry.setValue(999)
 | Exception java.lang.UnsupportedOperationException: not supported
 | at NullableKeyValueHolder.setValue (NullableKeyValueHolder.java:126)
 | at (#22:1)