为什么从创建列表的方法中返回unmodifiableList更好呢?

时间:2018-08-16 16:42:31

标签: java collections java-7

 public List<Integer> getInts()
{
    List<Integer> xs = new ArrayList<Integer>();
    xs.add(1);
    // return Collections.unmodifiableList(xs);
    return xs;
}   

我了解到,返回不可修改的列表将阻止使用者将其他元素添加到列表引用中,但除此之外,我通过将其包装在不可修改的列表中还能获得什么呢?每次调用该方法时,我都会创建一个新列表。

2 个答案:

答案 0 :(得分:7)

波希米亚人的答案陈述了一些很好的一般性原则,即关于返回不可修改的,不变的或防御性复制的数据,以保留封装。如果数据是对象内部的数据,例如存储在字段中,则肯定是正确的。

但是,OP声明每次调用该方法时都会重新创建返回的列表。在这种情况下,为什么返回不可修改的列表而不是常规的ArrayList?有原因,但它们有些微妙,与封装无关,而与保留实现的灵活性无关。

作为背景主题,您需要确定此API是否具有任何长期兼容性约束或策略。如果返回可变列表,则呼叫者有可能(根据Hyrum's Law)取决于其可变性。 (Hyrum定律从本质上说,系统的任何可观察属性最终都将取决于用户。)我个人认为,对返回给您的集合进行变异是草率的编程,但事实是,人们确实这样做。如果是这样,并且将来您建议将返回的列表更改为不可修改,是否会因为它不兼容而导致某些呼叫者中断而被禁止?如果您不关心兼容性(某些项目也不关心),那么也许没关系。但是,如果这样做,那么您应该考虑现在返回一个不可修改的列表。

一个原因(在注释中被其他人提到)是您可能决定不每次都创建一个新列表,但可以将其缓存并返回给多个调用者。如果这样做,则绝对应将其设置为不可修改,以防止一个调用者修改列表并影响所有列表。

另一个原因是不同的列表实现具有不同的性能和空间特征。 Collections.singletonListList.of(x)实现将其单个元素存储在List对象本身的字段中,而ArrayList则将其元素(即使只有一个)存储在单独的数组对象中。小清单实现可以节省大量空间 与ArrayList相比,如果要创建很多的话。如果将来想切换到单例列表或Java 9不可修改列表实现,则在ArrayList周围返回不可修改的包装将减轻兼容性问题。

您可能还想向该方法添加一些自适应行为,例如,取决于返回列表中元素的数量。例如,

if (count == 0) {
    return Collections.emptyList(); // eventually, List.of()
} else if (count == 1) {
    return Collections.singletonList(i); // eventually, List.of(i)
} else {
    List<Integer> list = new ArrayList<>();
    // populate list
    return Collections.unmodifiableList(list);
}

如果不将ArrayList包装在不可修改的包装器中,则调用者将面临行为上的奇怪差异,例如列表有时是可修改的,而有时则是不可修改的。如果可能的话,最好在所有情况下都提供统一的行为,从而为将来的更改保留实现的灵活性。

答案 1 :(得分:6)

当对象返回可变的私有字段时,它会受到外部代理的内部状态的有害/未知更改,这违反了封装。

您所引用的模式称为安全发布,可以防止出现此问题。可以通过返回深拷贝或不可变包装中的字段(以及任何可变子字段)来实现。

对于您注释掉的代码:

return Collections.unmodifiableList(xs);

它正在创建一个新对象,但它只是列表顶部的一个很薄的层,您几乎无法衡量执行此操作的cpu或内存性能成本。

您还可以:

return new ArrayList<>(xs);

进行深拷贝(在这种情况下,因为Integer是不可变的)。