为什么 return 一个空集合被认为是好的做法?

Why is it considered good practice to return an empty collection?

我读了几本书,也看过几篇博客讨论返回空集合比返回 null 更好。我完全理解试图避免检查,但我不明白为什么返回一个空集合比返回 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;
   }

}

但这样做的问题是您在代码中添加了其他人可能必须维护的意外行为。

我真的在寻找解决这个困境的方法。博客上的许多人倾向于只说它更好,而没有详细解释原因。如果返回不可变列表是最佳实践,我可以这样做,但我想了解为什么它更好。

因为 return 空集合的替代方法通常是 returning null;然后调用者必须添加防范 NullPointerException。如果你 return 一个空集合,那么 class 的错误就会减少。在 Java 8+ 中还有一个 Optional 类型,它可以在没有 Collection.

的情况下达到同样的目的

您对此感到困惑的原因是您没有首先初始化 bone。如果您在构造函数中创建一个新的 List<string>,您会发现 get() 中的检查是没有根据的。

虽然如果您从数学角度处理这个问题,您可能希望为 不存在 的集合保留返回 null 值,而不是为空的集合.

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

我想我不明白您为什么反对空集合,但同时我会指出我认为您的代码需要改进。也许这就是问题所在?

避免在您自己的代码中进行不必要的 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;
   }
}

您可能会对以下答案感兴趣:should-functions-return-null-or-an-empty-object

总结:


如果您打算指示没有数据可用,则返回 null 通常是最好的主意。

一个空对象意味着数据已经被 returned,而 returning null 清楚地表明没有任何东西被 returned。

此外,return如果您尝试访问对象中的成员,return空值将导致空值异常,这对于突出显示错误代码很有用 - 尝试访问什么都没有的成员是没有意义的.访问空对象的成员不会失败,这意味着错误不会被发现。


同样来自干净的代码:


使用 null 的问题是使用接口的人不知道 null 是否是可能的结果,也不知道他们是否必须检查它,因为没有非空引用类型。


来自 Martin Fowler 的特例模式


空值在面向对象程序中很尴尬,因为它们会破坏多态性。通常,您可以在给定类型的变量引用上自由调用 foo,而不必担心该项目是确切类型还是子 class。使用强类型语言,您甚至可以让编译器检查调用是否正确。但是,由于变量可以包含 null,您可能会 运行 通过在 null 上调用消息而陷入 运行 时间错误,这将为您提供一个友好的堆栈跟踪。

如果一个变量有可能为空,您必须记住用空测试代码将其包围起来,这样当出现空时您就会做正确的事情。通常正确的事情在很多情况下都是相同的,所以你最终会在很多地方编写类似的代码——犯下代码重复的罪过。

空值是此类问题的常见示例,其他问题经常出现。在数字系统中,您必须处理无穷大,无穷大对于加法等打破实数通常不变量的事物有特殊规则。我在商业软件方面最早的经历之一是与一个不为人知的公用事业客户打交道,称为 "occupant." 所有这些都意味着改变该类型的通常行为。

不是 return空值或一些奇数,return 一个特例,它具有与调用者期望的相同的接口。


最后是十亿美元的错误!


我称之为我的十亿美元错误。是1965年空引用的发明,当时我正在用面向对象语言(ALGOL W)设计第一个引用综合类型系统。

我的目标是确保所有引用的使用都绝对安全,并由编译器自动执行检查。但我无法抗拒放入空引用的诱惑,只是因为它很容易实现。

这导致了无数的错误、漏洞和系统崩溃,在过去四十年中可能造成了十亿美元的痛苦和损失。

近年来,微软的PREfix、PREfast等程序分析器被大量用于检查引用,并在存在可能为非空的风险时发出警告。最近的编程语言(如 Spec#)引入了非空引用的声明。这是我在 1965 年拒绝的解决方案。

托尼·霍尔


希望这能提供足够的理由,说明为什么 return 空集合或特殊 return 值比 null 更好。

如果您 return 一个空集合(但不一定 Collections.emptyList()),您可以避免使用无意的 NPE 让下游消费者感到惊讶。

这优于 returning null 因为:

  • 消费者无需防备
  • 无论其中有多少元素,消费者都可以对集合进行操作

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

最理想的解决方案:字段的eager初始化。

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

下一个解决方案:将其 return 设为 Optional 并采取措施以防它不存在。如果需要,您也可以在此处提供空集合,而不是抛出。

Dog dog = new Dog();
dog.get().orElseThrow(new IllegalStateException("Dog has no bones??"));