是否遵循java约定仅为集合提供getter?

use*_*738 5 java

我最近遇到了如下代码:

public List<Item> getItems() {
    if (items == null) {
        items = new ArrayList<Item>();
    }
    return this.items;
}
Run Code Online (Sandbox Code Playgroud)

而且至关重要的是,没有特定的方法.

因此,如果您想添加到arrayList,则必须这样做

foo.getItems().add(...)  
Run Code Online (Sandbox Code Playgroud)

而不是

foo.setItems(myArrayList)
Run Code Online (Sandbox Code Playgroud)

我之前没有见过这个成语,我不能说我喜欢它,但是当我使用mapstruct.org(顺便说一句很棒的工具)生成一些映射代码时,mapstruct处理得很好并正确生成使用getter的代码作为一个制定者.

我只是想知道 - 这是一个我不知何故错过的常见习语吗?这对我来说似乎毫无意义,但也许背后有一些智慧,我没有看到?

biz*_*lop 7

这很常见,但不建议这样做.

问题不在于懒惰的初始化(那部分是好的),而是暴露应该是一个实现细节.一旦暴露了存储在字段中的实际可变(!)列表对象,代码的调用者就可以对列表执行任何操作,甚至是您不期望的事情.

例如,他们可以在你想要的时候删除对象add().他们可以从不同的线程修改它,使您的代码以有趣和令人沮丧的方式中断.他们甚至可以将它转换为raw List,并用完全不同类型的对象填充它,使你的代码抛出ClassCastException.

换句话说,它使得无法强制执行类不变量.

请注意,有两件事情共同导致此问题:

  1. 存储在字段中的对象的曝光.
  2. 事实上这个对象是可变的.

如果这两个中的任何一个都不正确,那就没问题了.所以这很好:

public String getFoo() {
   return this.foo;
}
Run Code Online (Sandbox Code Playgroud)

因为String是不可改变的.这也很好:

public List<String> getFooList() {
   return new ArrayList<>( this.fooList );
}
Run Code Online (Sandbox Code Playgroud)

因为现在你正在返回防御性副本而不是实际对象.(但是,如果列表中的元素是可变的,那么你会再遇到麻烦.)


这个问题有一个更微妙的变化......

想象一下这种情况:

public class Foo { 
   private List<String> list;
   public Foo( List<String> list ) {
     this.list = list; // Don't do this
   }
   ...
}
Run Code Online (Sandbox Code Playgroud)

这看起来完全无害,你可以在很多地方看到它.然而,这里也有一个隐藏的问题:通过在存储列表之前不复制,你处于完全相同的情况.你不能阻止别人这样做:

List<String> list = new ArrayList<>();
list.add( "nice item" );
Foo foo = new Foo( list );
list.add( "hahahaha" );
list.add( "i've just added more items to your list and you don't know about it." );
list.add( "i'm an evil genius" );
Run Code Online (Sandbox Code Playgroud)

因此,在将可变对象分配给字段和返回字段之前,您应该制作防御性副本.


如果暴露一个类的可变字段是如此危险,那么为什么人们不会一直制作防御性副本呢?

除了根本不知道为什么这不是一个好主意之外,还有两大类的借口.

  1. 性能.每次调用getter时复制一个对象都很昂贵.这当然是正确的,但并不总是像你想象的那么昂贵.如果你发现自己为防御性拷贝付出了太高的代价,通常会有出路:例如通过设计你的类是不可变的.如果你的所有课程都是不可改变的,那么你永远不需要防御性的副本.
  2. 只有我会调用此代码,我不会滥用它.诺言.根据具体情况,这也是有效的.如果您自己编写一个小型独立应用程序,那么您可能不会滥用它.或者,如果您正在编写一个小型库,但是您只公开不属于公共API的类的详细信息.在大多数其他情况下,虽然你根本无法确定某个地方的人不会滥用它.当这种情况发生时,你的代码可能不会爆炸.它通常只是稍微开始......偶尔会出现奇怪的事情.这是尝试找到的最糟糕的错误.


Ada*_*zyk 3

您的代码中有两件事:

  1. 延迟初始化,这是一种常见的方法。
  2. 返回对可变内部的引用ArrayList,这不好,因为它破坏了封装。如果您想向此列表添加项目,您应该addItem()仅公开方法。

  • @user4782738 返回 null 将是糟糕的设计。这显然会破坏良好的编码实践。调用者不必在每次使用返回的列表时都测试 null。空列表是返回信号“我有 0 项”的正确选择。要“设置”列表,您需要的只是 `getItems().clear(); getItems.addAll(myList);`。请注意,恕我直言,ArrayList 的延迟实例化是一种无用的优化。列表本身已经延迟实例化其支持数组。它使代码变得更加复杂,但性能提升却很小,可能微不足道。 (2认同)