为什么返回一个空集合被认为是良好的实践?

10

我读过几本书和看了几篇博客,讨论了返回空集合比返回空值更好的问题。我完全理解尝试避免检查,但我不明白为什么返回空集合比返回null更好。例如:

public class Dog{

   private List<String> bone;

   public List<String> get(){
       return bone;
   }

}

对决

 public class Dog{

   private List<String> bone;

   public List<String> get(){
       if(bone == null){
        return Collections.emptyList();
       }
       return bone;
   }

}

例子一会抛出NullPointerException,而例子二会抛出UnsupportedOperation异常,但它们都是非常通用的异常。什么因素使一个比另一个更好或更糟糕呢?

另外,第三个选项是这样做:

 public class Dog{

   private List<String> bone;

   public List<String> get(){
       if(bone == null){
        return new ArrayList<String>();
       }
       return bone;
   }

}
但问题在于这样做会给您的代码添加意外的行为,可能需要其他人来维护。我真的正在寻找解决这个困境的方法。许多博客上的人倾向于只是说没有详细的解释为什么最好。如果返回一个不可变列表是最佳实践,那么我可以这样做,但我想了解为什么它更好。

1
如果有一个空数组,你总是可以访问它并迭代它。无需检查 null 值。这样更容易。但是否应该使用它是基于个人意见的问题。 - Sami Kuhmonen
这个问题在于你正在给你的代码添加意外的行为。你能详细说明一下吗? - markspace
为什么你在最后一个选项中说“但是这样做的问题是,你会给你的代码添加意外的行为,而其他人可能需要维护它。”?个人而言,我通常选择最后一个选项,因为你不需要检查 null,接收方可以直接遍历返回的集合。在 javadoc 中,你可以注明如果没有找到任何内容,则返回一个空列表等。 - uniknow
我认为这取决于用户对集合要执行的操作。如果您返回一个“只读”集合,那么我想不出您如何期望获得UnsupportedOperationException异常?(您在调用方的代码中假定了什么?) - JVMATL
返回一个空集合是Null Object Pattern的一个例子。 - Andy Turner
1
请注意,类频繁地向其内部公开可变引用通常是不可取的,因为它们失去了对这些值发生的情况的控制。如果您想能够“添加骨头”,最好提供一个addBone(String)方法; get()方法最好返回不可变值或可变副本。 - Andy Turner
5个回答

11

如果你返回一个空的集合(但并不一定是 Collections.emptyList()),你就可以避免让这个方法的下游消费者产生意外的NullPointerException。

这比返回null更可取,因为:

  • 消费者不必对其进行保护
  • 无论集合中有多少元素,消费者都可以对其进行操作

我说不一定要使用Collections.emptyList(),因为正如你所指出的,你会将一个运行时异常替换为另一个运行时异常,从而使添加到此列表变得不受支持,并再次让消费者感到惊讶。

最理想的解决方案是: 立即初始化该字段。

private List<String> bone = new ArrayList<>();
下一个解决方案是:使其返回一个Optional,并在不存在的情况下进行一些操作。您也可以在此处提供空集合,而不是抛出异常。
Dog dog = new Dog();
dog.get().orElseThrow(new IllegalStateException("Dog has no bones??"));

1
我同意急切初始化可能更好,但有些人可能会争论在使用列表之前初始化列表是没有充分理由的浪费内存(尽管是非常小的量)。 - user1870035
2
@Adam:我宁愿在这里牺牲一点内存,也不想在其他地方遇到愚蠢和完全不负责任的NPE。 - Makoto
谢谢你,很有道理 =) - user1870035
但是如果一点点变成了很多呢?例如,您正在序列化/反序列化数十万个对象(甚至可能是数百万个),其中列表已经被急切地初始化。策略会如何改变?这更多是假设性的问题,但我只是好奇。 - user1870035
@Adam:在那种情况下,你应该考虑正式地对你的应用程序进行分析。很可能,如果有成千上万的对象同时存在,就不止一个瓶颈(而且,修复起来比简单的急切初始化更有成效)。 - Makoto

4

因为返回空集合的替代方案通常是返回null; 而调用者必须添加防止NullPointerException的保护。如果您返回一个空集合,那么这种错误就得到了缓解。在Java 8+中还有一个Optional类型,可以在没有Collection的情况下实现相同的目的。


没错,但我的观点是,如果您尝试向不可变列表添加内容,仍然会收到UnsupportedOperationException的异常,因此您必须防范这种情况,所以我想知道其中的好处在哪里。 - user1870035
2
谁说一个空集合必须是不可变的列表?如果你计划往里面添加元素,就不要返回一个不可变的列表。 - Elliott Frisch

2
以下答案可能与您的问题相关:should-functions-return-null-or-an-empty-object
总结:
返回null通常是最好的选择,如果您打算表示没有可用数据。
空对象意味着已返回数据,而返回null清楚地表明未返回任何内容。
此外,返回null将导致null异常,如果尝试访问对象中的成员,则可以用于突出显示有错误的代码 - 尝试访问不存在的成员没有意义。访问空对象的成员不会失败,这意味着错误可能无法被发现。
此外,从clean code中:
使用null的问题在于使用接口的人不知道null是否是可能的结果,以及他们是否必须检查它,因为没有非null引用类型。
从Martin Fowler的Special Case模式中
null在面向对象程序中是棘手的东西,因为它们破坏了多态性。通常,您可以在给定类型的变量引用上自由调用foo,而不必担心该项是确切类型还是子类。使用强类型语言,甚至可以让编译器检查调用是否正确。但是,由于变量可以包含null,因此如果在null上调用消息,可能会遇到运行时错误,这将为您提供一个友好的堆栈跟踪。
如果变量可能为空,则必须记住用null测试代码将其包围,以便在存在null时执行正确的操作。通常,在许多情况下,正确的事情是相同的,因此您最终会在许多地方编写类似的代码 - 犯有代码重复的罪名。
null是此类问题的常见示例,其他问题经常出现。在数字系统中,您必须处理无限大,它具有打破实数通常不变量的特殊规则,例如加法。我最早在业务软件中的一个公用事业客户身上遇到了这个问题,他没有完全知道,被称为“占用者”。所有这些都意味着更改类型的通常行为。
而不是返回null或某些奇怪的值,请返回与调用者期望的相同接口的Special Case。
最后从Billion Dollar Mistake!:
我称之为我的十亿美元的错误。这是在1965年发明空引用时。当时,我正在为对象导向语言(ALGOL W)设计第一个全面的引用类型系统。
我的目标是确保所有引用的使用都应绝对安全,并由编译器自动执行检查。但是,我无法抵制放置null引用的诱惑,只是因为它非常容易实现。
这导致了无数错误,漏洞和系统崩溃,这可能在过去四十年中造成了数十亿美元的痛苦和损失。
近年来,微软的PREfix和PREfast等多个程序分析工具被用于检查引用,并且如果存在非空的风险时则提供警告。较新的编程语言如Spec#引入了非空引用的声明。这是解决方案,但我在1965年拒绝了这种方法。
Tony Hoare
希望这能提供足够的理由,说明为什么返回一个空的集合或特殊的返回值而不是null更加优秀。

这并不是你的答案。你直接从至少两个其他地方抄袭了它。 - Makoto

1
我不认为我理解你反对空集合的原因,但与此同时,我想指出你的代码需要改进。也许这就是问题所在?
避免在自己的代码中进行不必要的null检查:
public class Dog{

   private List<String> bone = new ArrayList<>();

   public List<String> get(){
       return bone;
   }
}

或者考虑不每次创建一个新列表:
 public class Dog{

   private List<String> bone;

   public List<String> get(){
       if(bone == null){
        return Collections.EMPTY_LIST;
       }
       return bone;
   }
}

你的第一种方法更好。你的第二种方法让人惊讶的是,一个类的get方法突然实例化了一个变量。 - Makoto

0

你对此感到困惑的原因是因为你没有在第一次初始化bone。如果你在构造函数中创建一个新的List<string>,你会发现get()中的检查是多余的。

不过,如果你从数学的角度来看这个问题,你会希望保留返回null值的情况是指那些不存在的集合,而不是只是空集合。

虽然在编程中不存在的集合没有太多实际应用,但这仍然是一种思考集合的好方法。


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