【Java 21 新特性】顺序集合(Sequenced Collections)

发布时间 2024-01-02 09:56:40作者: 公众号-JavaEdge

1 摘要

引入新的接口表示具有定义的遇到顺序的集合。每个这样的集合都有一个明确定义的第一个元素、第二个元素,依此类推,直到最后一个元素。提供统一的API来访问它的第一个和最后一个元素,并以相反的顺序处理它的元素。

"生活只能向后理解;但必须向前生活。"—基尔克高德

2 动机

Java集合框架缺乏表示具有定义的遇到顺序的元素序列的集合类型。它还缺乏适用于这些集合的统一操作集。这些差距一直是问题和抱怨的重要来源。

如List和Deque都定义了遇到顺序,但共同父类Collection却没有定义遇到顺序。同样,Set没有定义遇到顺序,而子类型HashSet也没定义,但子类型如SortedSet和LinkedHashSet却定义了。因此,对遇到顺序的支持在类型层次结构中分散,使得在API中表达某些有用概念很困难,即不能在Collection中描述具有遇到顺序的参数或返回值。Collection太一般了,将这些约束规定到散文规范中,可能导致难以调试的错误。List太具体了,排除了SortedSet和LinkedHashSet。

FAQ

视图集合通常被迫降级到较弱语义。用Collections::unmodifiableSet包装LinkedHashSet会产生一个Set,丢弃了顺序信息。

没有定义它们的接口,与遇到顺序相关的操作要么不一致,要么缺失。虽许多实现支持获取第一个或最后一个元素,但每个集合都定义了自己的方式,有些不明显或完全缺失:

First element Last element
List list.get(0) list.get(list.size() - 1)
Deque deque.getFirst() deque.getLast()
SortedSet sortedSet.first() sortedSet.last()
LinkedHashSet linkedHashSet.iterator().next() // missing
  • 一些是不必要的繁琐,如获取List的最后一个元素
  • 有些甚至没有英雄主义是不可能的:获取LinkedHashSet的最后一个元素的唯一方法是迭代整个集合!
  • 同样,从第一个元素到最后一个元素遍历通常需用迭代器或使用普通for循环,使代码冗长不直观

为解决这些问题,引入新接口SequencedCollection表示具有定义的遇到顺序的集合。每个SequencedCollection都有一个明确定义的第一个元素、第二个元素,依此类推,直到最后一个元素。它还提供统一的API访问它的第一个和最后一个元素,并以相反的顺序处理它的元素。

SequencedCollection还提供新reversed()方法,提供一个反向排序的视图。这视图可让集合以相反顺序处理元素,使用所有常见迭代机制,如增强for循环、显式的iterator()循环、forEach()、stream()、parallelStream()和toArray()。

如以前从LinkedHashSet获取反向排序的流困难,现只需linkedHashSet.reversed().stream()。

SequencedCollection还从Deque中提升一些方法,支持在两端添加、获取和删除元素:

  • void addFirst(E)
  • void addLast(E)
  • E getFirst()
  • E getLast()
  • E removeFirst()
  • E removeLast()

add(E)和remove()方法可选,主要是为支持不可修改的集合的情况。如集合为空,get()和remove()方法将抛出NoSuchElementException。

由于SequencedCollection的子接口具有冲突的定义,所以在SequencedCollection中没有定义equals()和hashCode()方法。

这些改动使得具有遇到顺序的集合更加易于使用和操作,并提供了一致的API来处理这些集合的元素。

Sequenced Sets

Set接口的扩展,有序集合,不含重复元素:

// since 21
interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
    SequencedSet<E> reversed();    // 协变重写
}

SequencedSet接口提供reversed()方法,用于返回一个反转顺序的SequencedSet。对于LinkedHashSet等集合,若元素已存在于集合中,则会将其移动到适当位置。这解决LinkedHashSet无法重新定位元素痛点。

SequencedMap

Map接口的扩展,条目具有定义好的遍历顺序。

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);
    // 从NavigableMap中提升的方法
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}
  • reversed()返回一个反转顺序的SequencedMap
  • sequencedKeySet()返回一个有序的键集合
  • sequencedValues()返回一个有序的值集合
  • sequencedEntrySet()返回一个有序的条目集合

SequencedMap还提供了putFirst()和putLast()方法,用于在指定位置插入键值对。对于SortedMap等映射,这些方法会抛出UnsupportedOperationException。

SequencedMap还提升了一些方法从NavigableMap接口,这些方法支持在两端获取和删除条目。

Retrofitting

为了将新的接口与现有的类和接口结构整合起来,调整:

img

  • List接口现在直接继承自SequencedCollection接口
  • Deque接口现在直接继承自SequencedCollection接口
  • LinkedHashSet类实现了SequencedSet接口
  • SortedSet接口现在直接继承自SequencedSet接口
  • LinkedHashMap类实现了SequencedMap接口
  • SortedMap接口现在直接继承自SequencedMap接口。

在适当的位置为reversed()方法提供了协变重写。如List#reversed()重写为返回一个List类型的值,而不是SequencedCollection类型的值。

还在Collections工具类中添加了一些新的方法,用于创建不可修改的包装器:

  • Collections.unmodifiableSequencedCollection(sequencedCollection)
  • Collections.unmodifiableSequencedSet(sequencedSet)
  • Collections.unmodifiableSequencedMap(sequencedMap)

替代方案

类型

将List接口重新定义为通用的有序集合类型。虽然List是有序的,但它也支持通过整数索引访问元素。许多有序的数据结构并不自然地支持索引,因此它们将被要求通过迭代来支持索引访问,这将导致索引访问的性能从O(1)变为O(n),延续了LinkedList的错误。

Deque作为通用的序列类型似乎是一个不错的选择,因为它已经支持了正确的一组操作。然而,它还包含了其他操作,包括一系列返回null的操作(offer、peek和poll),以及从Queue继承的操作。这些操作对于队列来说是合理的,但对于其他集合来说则不太合适。如果将Deque重新定义为通用的序列类型,那么List也将成为一个Queue,并支持栈操作,导致API变得混乱和令人困惑。

命名

术语"sequence"(序列)在这里被选择,它暗示了元素按照一定的顺序排列。它在各个平台上都被广泛使用,表示具有类似语义的集合。

术语"ordered"(有序)并不够具体。我们需要迭代两个方向上的元素,并在两端进行操作。一个有序的集合,如Queue,是一个明显的例外:它是有序的,但它也明显是不对称的。

术语"reversible"(可逆)在之前的版本中使用过,但它并没有立即唤起双端的概念。也许更大的问题是,Map变体将被命名为ReversibleMap,这会误导地暗示它支持通过键和值进行查找(有时称为BiMap或BidiMap)。

Add, put, and UnsupportedOperationException

对于通过相对比较确定顺序的集合,例如SortedSet的addFirst和SortedMap的putLast方法会抛出UnsupportedOperationException。一些集合不实现所有SequencedCollection操作的不对称性可能看起来不太好。然而,这是有价值的,因为它将SortedSet和SortedMap纳入到有序集合的家族中,使它们可以比以前更广泛地使用。这种不对称性也与集合框架中的先前设计决策保持一致。例如,Map的keySet方法返回一个Set,即使实际返回的实现不支持添加操作。

另一种方法是通过重新调整接口来保持添加操作的独立性。这将导致在结构上有很薄的语义的新接口类型(例如AddableCollection),在实践中没有用处,并且会使类型层次结构变得混乱。

测试

我们将在JDK的回归测试套件中添加一套全面的测试。

风险和假设

在继承层次结构中高层次地引入新的方法可能会导致对明显方法名称(如reversed()和getFirst())的冲突。

特别需要关注的是List和Deque上reversed()方法的协变重写。这些重写与已经实现了List和Deque的现有集合在源代码和二进制兼容性上是不兼容的。在JDK中有两个这样的集合的例子:LinkedList和一个内部类sun.awt.util.IdentityLinkedList。LinkedList类通过在LinkedList本身上引入一个新的reversed()协变重写来处理。内部的IdentityLinkedList类被删除,因为它不再需要。

提案的早期版本在SequencedMap接口上引入了keySet()、values()和entrySet()方法的协变重写。经过一些分析,确定这种方法引入了不兼容性的风险太大;实际上,它使任何现有的子类都无效。选择了另一种方法,即在SequencedMap中引入了新的sequencedKeySet()、sequencedValues()和sequencedEntrySet()方法,而不是调整现有方法为协变重写。回顾起来,这可能是因为在Java 6中引入navigableKeySet()方法时采用了类似的方法,而不是修改现有的keySet()方法为协变重写。

有关不兼容性风险的完整分析,请参见附加到CSR(JDK-8266572)的报告。

历史

这个提案是我们2021年ReversibleCollections提案的一个增量演进。与该提案相比,主要的变化是改名、引入SequencedMap接口以及引入不可修改的包装器方法。

ReversibleCollection提案又基于Tagir Valeev的2020年OrderedMap/OrderedSet提案。该提案中的一些基本概念仍然存在,尽管在细节上有很多不同。

多年来,我们收到了许多关于将List与Set或Map结合的请求和提案。这些请求包括4152834、4245809、4264420、4268146、6447049和8037382。

其中一些请求在Java 1.4中引入的LinkedHashSet和LinkedHashMap中部分得到了满足。虽然这些类满足了一些用例,但它们的引入留下了集合框架中抽象和操作的空白,如上所述。

测试

我们将向JDK的回归测试套件中添加一套全面的测试。

风险和假设

在继承层次结构中引入新的方法存在冲突的风险,例如reversed()和getFirst()这样的明显方法名称。
特别关注的是List和Deque上的covariant overrides的reversed()方法。这些方法与已实现List和Deque的现有集合在源代码和二进制上不兼容。在JDK中有两个这样的集合的示例:LinkedList和一个内部类sun.awt.util.IdentityLinkedList。LinkedList类通过在LinkedList本身上引入了一个新的reversed() covariant override来处理。内部的IdentityLinkedList类被删除,因为它不再需要。
提案的早期版本在SequencedMap接口的keySet()、values()和entrySet()方法上引入了covariant overrides。经过一些分析,确定这种方法引入了太大的不兼容风险;实质上,它使任何现有的子类无效。选择了另一种替代方法,即在SequencedMap中引入新的方法sequencedKeySet()、sequencedValues()和sequencedEntrySet(),而不是调整现有方法为covariant overrides。回顾起来,可能出于同样的原因,在Java 6中引入navigableKeySet()方法时采取了类似的方法,而不是修改现有的keySet()方法为covariant override。
有关不兼容风险的完整分析,请参阅附加到CSR(JDK-8266572)的报告。

参考

本文由博客一文多发平台 OpenWrite 发布!