跳到主要内容

JEP 269:集合的便利工厂方法

概括

定义库API,可以方便地创建包含少量元素的集合和映射的实例,从而减轻Java编程语言中没有集合字面量的痛苦。

目标

在集合接口上提供静态工厂方法,这些方法将创建紧凑的、不可修改的集合实例。 API 被刻意保持在最小限度。

非目标

提供完全通用的“集合生成器”工具并不是我们的目标,例如,让用户控制集合实现或各种特性,例如可变性、预期大小、加载因子、并发级别等。

支持具有任意数量元素的高性能、可扩展集合并不是我们的目标。重点是小型收藏。

提供不可修改的集合类型不是目标。也就是说,该提案没有暴露类型系统中不可修改的特征,即使所提出的实现实际上是不可修改的。

提供“不可变的持久”或“功能”集合不是目标。

动机

Java 经常因其冗长而受到批评。创建一个小的、不可修改的集合(例如,一个集合)涉及到构造它、将其存储在局部变量中、add()多次调用它,然后包装它。例如,

Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
set = Collections.unmodifiableSet(set);

这是相当冗长的,并且因为它不能用单个表达式来表达,所以静态集合必须填充在静态初始化块中,而不是通过更方便的字段初始化器。或者,可以使用另一个集合中的复制构造函数来填充集合:

Set<String> set = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("a", "b", "c")));

这仍然有点冗长而且不太明显,因为必须List在创建Set.另一种选择是使用所谓的“双括号”技术:

Set<String> set = Collections.unmodifiableSet(new HashSet<String>() {{
add("a"); add("b"); add("c");
}});

这在匿名内部类中使用实例初始化器构造,这更漂亮一些。然而,它相当晦涩,并且每次使用都会花费额外的课程。它还保存对封闭实例和任何捕获的对象的隐藏引用。这可能会导致内存泄漏或序列化问题。由于这些原因,最好避免使用这种技术。

Java 8 Stream API 可用于通过组合流工厂方法和收集器来构造小型集合。例如,

Set<String> set = Collections.unmodifiableSet(Stream.of("a", "b", "c").collect(toSet()));

(流收集器不保证它们返回的集合的可变性。在 Java 8 中,返回的集合是普通的可变集合,例如ArrayListHashSetHashMap,但这在未来的 JDK 版本中可能会改变。)

这有点迂回,虽然并不晦涩,但也不是很明显。它还涉及一定量不必要的对象创建和计算。与典型情况一样,Map异常值也是如此。流不能用这种方式构造 a Map,除非可以根据键计算值,或者流元素同时包含键和值。

过去,已经提出了一些建议来更改 Java 编程语言以支持集合文字。然而,正如语言功能的常见情况一样,没有任何功能像人们首先想象的那样简单或干净,因此集合文字将不会出现在 Java 的下一版本中。

集合文字的大部分好处可以通过提供用于创建小型集合实例的库 API 来获得,与更改语言相比,成本和风险显着降低。例如,创建一个小型 Set 实例的代码可能如下所示:

Set<String> set = Set.of("a", "b", "c");

类中已有工厂Collections支持创建空Lists、Sets、Maps。还有一些工厂可以生产只有一个元素或键值对的单例Lists、Sets 和s。包含多个重载方法,这些方法采用固定或可变数量的参数,以便方便地创建具有指定元素的 。然而,没有好的通用方法来创建s、s 和包含任意类型对象的 s。Map``EnumSet``of(...)``EnumSet``List``Set``Map

类中有组合器方法Collections用于创建不可修改的Lists、Sets 和Maps。这些不会创建本质上不可修改的集合。相反,他们采用另一个集合并将其包装在一个拒绝修改请求的类中,从而创建原始集合的不可修改_视图_。拥有对底层集合的引用仍然允许修改。每个包装器都是一个附加对象,需要另一级间接,并且比原始集合消耗更多内存。最后,包装的集合仍然承担支持突变的费用,即使它从未打算被修改。

描述

在 、 和 接口上提供静态工厂方法,List用于Set创建Map这些集合的不可修改实例。 (请注意,与类上的静态方法不同,接口上的静态方法不是继承的,因此无法通过实现类或接口类型的实例来调用它们。)

对于ListSet,这些工厂方法的工作原理如下:

List.of(a, b, c);
Set.of(d, e, f, g);

这些将包括可变参数重载,因此集合大小没有固定限制。然而,如此创建的集合实例可以调整为更小的尺寸。将提供最多十个元素的特殊情况 API(固定参数重载)。虽然这会给 API 带来一些混乱,但它避免了由 varargs 调用引起的数组分配、初始化和垃圾收集开销。值得注意的是,无论调用的是固定参数还是可变参数重载,调用站点的源代码都是相同的。

对于Maps,将提供一组固定参数的方法:

Map.of()
Map.of(k1, v1)
Map.of(k1, v1, k2, v2)
Map.of(k1, v1, k2, v2, k3, v3)
...

我们预计支持最多十个键值对的小地图将足以覆盖大多数用例。对于大量条目,将提供一个 API,Map根据任意数量的键值对创建一个实例:

Map.ofEntries(Map.Entry<K,V>...)

虽然此方法类似于List和的等效可变参数 API Set,但不幸的是,它要求对每个键值对进行装箱。适合静态导入的对键和值进行装箱的方法将使这更加方便:

Map.Entry<K,V> entry(K k, V v)

使用这些方法,可以创建具有任意数量条目的地图:

Map.ofEntries(
entry(k1, v1),
entry(k2, v2),
entry(k3, v3),
// ...
entry(kn, vn));

(在 JDK 的未来版本中使用值类型可能会减少装箱的开销。entry()便利方法实际上会返回一个新引入的实现 的具体类型Map.Entry,以便于将来可能迁移到值类型。)

提供用于创建小型、不可修改的集合的 API 可以满足大量用例,并且有助于保持规范和实现的简单性。不可修改的集合避免了制作防御性副本的需要,并且它们更适合并行处理。

小集合占用的运行时空间也是一个重要的考虑因素。使用包装器 API直接创建HashSet具有两个元素的不可修改对象将由六个对象组成:包装器 ,HashSet其中包含HashMap、其存储桶表(一个数组)以及Node每个元素一个实例。与存储的数据量相比,这带来了巨大的开销,并且对数据的访问不可避免地需要多个方法调用和指针取消引用。为小型、固定大小的集合设计的实现可以使用紧凑的基于字段或基于数组的布局来避免大部分开销。不需要支持突变(并且在创建时知道集合大小)也有助于节省空间。

这些工厂返回的具体类不会作为公共 API 公开。不保证返回集合的运行时类型或标识。这将允许实现随着时间的推移而改变,而不会破坏兼容性。调用者唯一应该依赖的是返回的引用是其接口类型的实现。

生成的对象将是可序列化的。序列化代理对象将用作实现类的通用序列化形式。这将防止有关具体实现的信息泄漏到序列化形式中,从而保留未来维护的灵活性,并允许具体实现在不同版本之间进行更改,而不会影响序列化兼容性。

不允许使用空元素、键和值。 (最近引入的集合都不支持空值。)此外,禁止空值还提供了实现更紧凑的内部表示、更快的访问和更少的特殊情况的机会。

这些List实现预计将通过索引提供快速元素访问,因此它们将实现RandomAccess标记接口。

这些集合中存储的元素必须支持典型的集合契约,包括对hashCode()和 的适当支持equals()。如果 a 的元素Set或 a 的键以影响其或方法Map的方式发生变异,则集合的行为可能会变得不确定。hashCode()``equals()

一旦构建并安全发布,这些集合实例对于多个线程的并发访问将是安全的。

将搜索 JDK 来寻找可以使用这些新 API 的潜在站点。如果时间和时间表允许,这些站点将进行更新以使用新的 API。

备择方案

语言更改已被考虑多次,但被拒绝:

  1. 项目代币提案,2009 年 3 月 29 日
  2. 项目代币提案,2009 年 3 月 30 日
  3. lambda-dev 上的 JEP 186 讨论,2014 年 1 月至 3 月

语言提案优先于本消息中总结的基于图书馆的提案而被搁置。

Google Guava 库拥有一组丰富的实用程序,用于创建不可变集合(包括构建器模式)以及用于创建各种可变集合。 Guava 库非常有用且通用,但对于包含到 Java SE 平台来说可能有点过分了。该提案与 Stephen Colebourne lambda-dev于 2014 年 2 月 19 日提出的提案类似,包含来自 Guava 不可变集合工厂方法的一些想法。

Map.fromEntries()用任意数量的条目初始化 a 的方法并不Map理想,但它似乎是替代方案中最不坏的。它的优点是类型安全,在语法中具有相邻的键和值,条目数在编译时已知,并且适合用作字段初始值设定项。不过涉及到拳击,而且比较啰嗦。考虑了几种替代方案,它们都引入了权衡,这似乎使它们比当前的提案更糟糕。

具体集合类(例如ArrayList、HashSet)上的静态工厂方法已从该提案中删除。它们看起来很有用,但实际上它们往往会分散开发人员对不可变集合使用工厂方法的注意力。有一小部分用例用于使用一组预定义的值初始化可变集合实例。通常最好将这些预定义值放在不可变集合中,然后通过复制构造函数初始化可变集合。

还有另一个问题,那就是类的静态方法由子类继承。假设要添加一个静态工厂方法 HashMap.of()。由于 LinkedHashMap 是 HashMap 的子类,因此应用程序代码可以调用 LinkedHashMap.of()。这最终会调用 HashMap.of(),这根本不是人们所期望的!缓解这种情况的一种方法是确保所有具体集合实现都具有相同的工厂方法集,这样就不会发生继承。对于具体集合的用户定义子类来说,继承仍然是一个问题。

测试

JDK 回归测试套件中将有一组常见的单元测试,以及针对公共 API 的 JCK 测试。 JCK 也可能涵盖序列化表格。

将开发一套尺寸和性能测试。与与基线测量进行比较的典型目标相反,这些测试将新的集合实现与现有的集合实现进行比较。预期新集合将消耗更少的堆空间,无论是在固定开销方面还是在每个元素的基础上。然而,在某些情况下,由于与现有集合相比内部表示不同,新集合可能会更慢。任何此类放缓都应该是合理的。尽管没有具体的性能目标,但慢 10 倍是不可接受的。此外,随着元素数量的增加,新集合应保持一致的性能。最后,测量将建立基线性能数据,与未来的变化进行比较。