Java 返回空的不可变集合而不是分配新的空集合有什么缺点?

Java 返回空的不可变集合而不是分配新的空集合有什么缺点?,java,collections,immutability,effective-java,Java,Collections,Immutability,Effective Java,我曾经调用Collections.emptyList(),emptySet和emptyMap从我的函数返回对不可变的空集合的引用,而不是null。这是因为我认为没有理由分配新的空集合实例: private static Collection<Cheese> getCheesesOld() { if (cheesesInStock.isEmpty()) { return Collections.emptyList(); } return new

我曾经调用
Collections.emptyList()
emptySet
emptyMap
从我的函数返回对不可变的空集合的引用,而不是
null
。这是因为我认为没有理由分配新的空集合实例:

private static Collection<Cheese> getCheesesOld() {
    if (cheesesInStock.isEmpty()) {
        return Collections.emptyList();
    }
    return new ArrayList<>(cheesesInStock);
}
private静态集合getCheesesOld(){
if(cheesesInStock.isEmpty()){
返回集合。emptyList();
}
返回新的ArrayList(cheesesInStock);
}
今天,我在《有效Java第三版》第54项中阅读了以下内容:

。。。您可以通过重复返回相同的不可变空集合来避免分配,因为不可变对象可以自由共享。。。 但请记住,这是一种优化,很少需要。如果你认为你需要它,在前后衡量一下性能,以确保它确实有帮助

对我来说,在我能够证明它将提高性能之前,我应该避免返回空的不可变集合。问题是:为什么?返回不可变的空集合除了不能对其进行变异和需要输入更多代码之外,还有其他缺点吗


我是否应该更改默认行为并防御性地返回内部列表的副本(项目50),即使它是空的,而不是返回已分配的
集合。emptyList()

集合。emptyList()
不要分配内部数组,立即知道它们的大小,可能会为它们编写更好的迭代器/拆分器(在调用
.stream()
时很重要);另一方面,
if
分支可能比从
Collections.emptyList()
获得的收益更昂贵

这里通常的建议是——使用任何对你来说更有效、更容易阅读的东西。这种优化可能存在,但实际上不存在的一个地方是
Collectors.toList()
:它总是返回一个空的
ArrayList
,而不是
java-8
中可能返回的
Collections.emptyList()
。我假设这样做是因为对这种很少使用的场景的检查会在频繁使用的场景中引入惩罚;因此没有这样做

你还必须对书中的建议持保留态度——那人写了数百万人使用的图书馆(那里有非常好的建议),但你也这么做了吗

但是请记住,这是一个优化


我认为这是指避免重新分配空集合的做法。如果返回的是一个共享的空集合,那么它应该是不可变的。然而,按照我阅读第54项的方式,布洛赫说,除非有证据表明分配一个新的空列表(
new ArrayList()
)会损害性能,否则这样做是可以的。

我们可以反过来看。显然,这个方法的调用者不应该假设一个可变列表,否则返回一个不可变列表将不起作用

然后,在某些情况下,返回一个实际可变的
ArrayList
,这并不强制执行不变性,这是一个优化

想想看,它没那么大

private static Collection<Cheese> getCheesesOld() {
    if(cheesesInStock.isEmpty()) {
        return Collections.emptyList();
    }
    return Collections.unmodifiableList(new ArrayList<>(cheesesInStock));
}
private静态集合getCheesesOld(){
if(cheesesInStock.isEmpty()){
返回集合。emptyList();
}
返回集合。不可修改列表(新ArrayList(CheeseInstock));
}
这只会增加一小部分开销,无论是从性能角度还是从代码开发/维护角度来看。处理空列表的三行代码更为重要

但是,如果在空列表的情况下返回三个新的包装对象,您可能仍然会感到不舒服,如果您删除了特殊处理,那么返回一个共享对象就足够了。这种感觉可能会持续下去,即使您意识到该场景几乎从未发生过,即调用该方法时列表几乎从未为空

谢天谢地,最近的Java版本有一个简单的解决方案。你可以用

private static Collection<Cheese> getCheesesOld() {
    return List.copyOf(cheesesInStock);
}
private静态集合getCheesesOld(){
退货清单。复印件(奶酪库存);
}
使用Java10或更新版本。返回的列表是不可变的,但不是包装在另一个保护对象中的列表,并且它将具有优化版本,不仅适用于空列表,也适用于JRE开发人员认为有益的小规模列表

如果源已经是一个不可变列表,那么不执行实际的复制操作也足够聪明,这意味着使用相同的方法创建传入列表的防御副本将在它们已经是由此类getter方法创建的副本时获得回报


您只需处理新的
null
处理,即彻底拒绝
null
元素。

另一个缺点是额外的复杂性(需要编写、阅读和理解更多的代码),以及所需的额外文档:调用方可能认为他/她可以自由修改返回的列表,直到他/她遇到异常的空箱,他/她不能再这样做了。
Collectors.toList()
和这个问题的场景有一点不同。使用收集器,将分配一个新的
ArrayList
,当没有添加元素时,它本身就没有数组。相反,
ArrayList(Collection)
构造函数将始终调用源集合上的
toArray()
,在空集合的情况下,可能会创建一个非共享的空数组,该空数组随后才被共享的空数组替换。