Java 9中重载方便的集合工厂方法有什么意义?

44

Java 9带来了用于创建不可变列表的便利工厂方法。现在,创建列表就像这样简单:

List<String> list = List.of("foo", "bar");

但是这个方法有12个重载版本,11个版本接受0到10个元素参数,还有一个版本接受可变参数。

static <E> List<E>  of(E... elements)

SetMap也是这种情况。

由于存在变量参数方法,那么再添加11个方法的意义何在呢?

我认为变量参数方法会创建一个数组,所以其他11个方法可以跳过创建多余的对象,并且在大多数情况下只需要0-10个元素。还有其他原因吗?


11
你已经回答了自己的问题 - 使用0-10个参数使其不需要创建不必要的数组。 - luk2302
3
@Marco13,我投票支持重新开放此问题,因为有很多问题超越了技术层面。同时,考虑到这些特性现在已经在java中可用,人们更可能搜索“java 9 collection.of”而不是“Guava colleciton.of”,因此我也投票支持重新开放。 - Chetan Kinger
3
我认为创建一个规范问题,并将这两个问题指向它作为重复问题,这样做是有意义的。在我们这样做之前,我觉得这个问题应该保持开放状态。我不想因为这个讨论而占用这个问题的评论空间,所以让我们看看其他人怎么说。 - Chetan Kinger
10
我投票支持重新开放。 Guava、EnumSet等其他API中提供类似过载的 justification 不同 - 它们提供了这些过载是因为此时 @SafeVarargs 不存在,而 JEP 269 中过载的驱动程序是性能 - Stefan Zobel
3
@StefanZobel 同意;特别是因为一般讨论的重点是这是一种微小的优化,通过排除数组创建同时所有方法(除了 2 个参数)都委托给此方法:@SafeVarargs @SuppressWarnings("unchecked") SetN(E... input) { - Eugene
显示剩余11条评论
6个回答

35

JEP文档本身 -

描述 -

这将包括可变参数重载,因此在集合大小上没有固定限制。但是,所创建的集合实例可能会针对较小的大小进行调整。为最多十个元素提供特殊情况的API(固定参数重载)。虽然这在API中引入了一些混乱,但它避免了由可变参数调用产生的数组分配、初始化和垃圾收集开销。值得注意的是,无论是调用固定参数还是可变参数重载,调用站点的源代码都是相同的。


编辑 - 添加动机,并如@CKing在评论中已经提到的:

非目标 -

不旨在支持具有任意数量元素的高性能、可扩展集合。重点是小集合

动机 -

创建一个小的、不可修改的集合(例如,一个集合)涉及构造它,在本地变量中存储它,并对其多次调用add(),然后包装它。

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

Java 8流API可以通过结合流工厂方法和收集器来构建小型集合。

// Java 8
Set<String> set1 = Collections.unmodifiableSet(Stream.of("a", "b", "c").collect(Collectors.toSet()));

提供用于创建小型集合实例的库API可以在不改变语言的情况下,大大降低成本和风险的前提下获取集合字面量的许多好处。例如,创建一个小型Set实例的代码可能如下所示:

// Java 9 
Set set2 = Set.of("a", "b", "c");

3
在如Java这样的高级语言和充足硬件资源的时代,这不是微观优化吗?我很希望能有其他原因,但我想不到。 - Chetan Kinger
26
@CKing 是的,这是微小优化。JDK库的性能标准比大多数应用程序要严格得多。这些API在JDK本身中被使用,并且它们会影响启动时间,因此即使是看似微小的优化也是值得的。 - Stuart Marks
3
@StuartMarks 但是它们(除了两个参数的方法)都委托给这个方法:@SafeVarargs @SuppressWarnings("unchecked") SetN(E... input) {。请注意,我已经尽力使翻译保持原意和易读性。 - Eugene
6
是的,那个在实现中被埋藏了,而且可以进行兼容性更改。只是它还没有完全优化。 - Stuart Marks
2
@CKing 这些API旨在供一般使用。但我们希望能够在不牺牲性能的情况下在JDK内部使用它们。关于新API的通用用法,通常情况下,源代码无需知道或关心它是调用定长参数还是可变参数方法。如果你有Set.of(a1, a2, ... a10)并且添加了另一个参数,它将切换到可变参数调用,但其余行为完全相同。因此,可以直接使用新API。 - Stuart Marks
显示剩余6条评论

11

正如你所怀疑的那样,这是一种性能增强。Vararg方法在幕后创建一个数组,而直接使用1-10个参数的方法避免了这种冗余的数组创建。


3
在像Java这样的高级语言中,硬件资源丰富的时代,这不是微观优化吗?我真希望有其他原因,但想不出来了。 - Chetan Kinger
3
语言/库设计者需要考虑他们的代码在不仅仅是在偶尔运行的代码中的执行性能,还要考虑当有人在计算密集型应用程序的热循环中使用它时会发生什么。在这些情况下,那些本来可以被视为“无意义的微小优化”的东西确实具有重要的性能影响。 - Dan Is Fiddling By Firelight
3
这个论点在这种情况下是否有效?创建一个包含10个元素的数组需要多少费用? - Chetan Kinger
@DanNeely:我同意“热循环”场景的观点。编程语言需要尽可能地进行优化。虽然影响微小,但如果在长时间运行的循环中实现优化,仍会产生巨大的差异。 - ares
6
具有讽刺意味的是,“热循环”场景在这种优化中显然被认为是不相关的。 - Stefan Zobel
显示剩余2条评论

11
您可能会发现Josh Bloch的《Effective Java》(第2版)条款42的以下内容具有启示作用:
每次调用可变参数方法都会导致数组分配和初始化。如果您已经通过实验确定无法承受此成本,但是需要可变参数的灵活性,则有一种模式可以让您两全其美。假设您已确定对于某个方法,95%的调用只有三个或更少的参数。然后声明五个重载方法,一个没有普通参数,另外四个分别带有0到3个普通参数,并为当参数个数超过三个时使用单个可变参数方法[...]

好的,所以这个实现是直接从Josh Bolch的文本中复制过来的。 - ares
2
需要注意的最重要的两个词是:“经验确定”。微观优化绝对需要经验证明。 - Chetan Kinger

4
你也可以从另一个角度来看待它。由于可变参数方法可以接受数组,这样的方法将作为将数组转换为List的另一种选择。
String []strArr = new String[]{"1","2"};
List<String> list = List.of(strArr);

另一种方法是使用Arrays.asList,但在这种情况下对List所做的任何更改都会反映在数组中,而这在使用List.of时并不是这样。因此,当您不希望List和数组保持同步时,可以使用List.of

注意:规范中给出的理由似乎对我来说只是微观优化。(API的所有者在另一个答案的评论中已经确认了这一点)


1
这就像是编译器不再为我创建数组,而是由我自己创建。 - ares
@ares 这样想。有时您需要与返回数组的代码交互。当您想将此数组转换为列表时,可以直接传递给List.of方法。此外,与Arrays.asList方法相比,List和数组不会保持同步,这是使用此方法的另一个用例。 - Chetan Kinger

3

这种模式用于优化接受可变参数的方法。

如果您能确定您大部分时间只使用其中几个参数,您可能会希望定义一个重载方法,并使用最常用参数的数量:

public void foo(int num1);
public void foo(int num1, int num2);
public void foo(int num1, int num2, int num3);
public void foo(int... nums);

这将帮助您在调用可变参数方法时避免创建数组。为了进行性能优化,使用的模式是:
List<String> list = List.of("foo", "bar");
// Delegates call here
static <E> List<E> of(E e1, E e2) { 
    return new ImmutableCollections.List2<>(e1, e2); // Constructor with 2 parameters, varargs avoided!
}

更有趣的是,从3个参数开始,我们再次委托给可变参数构造函数:

static <E> List<E> of(E e1, E e2, E e3) { 
    return new ImmutableCollections.ListN<>(e1, e2, e3); // varargs constructor
}

目前看起来很奇怪,但我猜测这是为了未来的改进保留的选项,并且作为一个选择,可以重载所有构造方法List3(3个参数),List7(7个参数)...等等。


-2
根据Java doc:由便捷工厂方法返回的集合比它们的可变等效物更节省空间。
在Java 9之前:
Set<String> set = new HashSet<>(3);   // 3 buckets

set.add("Hello");
set.add("World");
set = Collections.unmodifiableSet(set);

在上面的Set实现中,创建了6个对象:不可修改的包装器;包含HashMapHashSet;桶表(一个数组);和两个节点实例(每个元素一个)。如果虚拟机每个对象占用12字节,则有72字节用于开销,再加上28*2 = 56字节,用于2个元素。在这里,与集合中存储的数据相比,大量空间被开销消耗了。但是在Java 9中,这种开销非常少。

在Java 9之后:

Set<String> set = Set.of("Hello", "World");

在上述的Set实现中,只创建了一个对象,由于最小化开销,这将占用非常少的空间来存储数据。

5
虽然这个回答可能是正确的,但它与所提出的问题无关。 - ares

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接