在从getter方法中返回之前检查null是一种不好的做法吗?

6
我希望防止getter方法返回null值,就像下面的例子一样。这是一个不好的做法吗?
例1:
public Integer getMinutes() {
   if (minutes == null)
       minutes = 0;
   return minutes;
}

例子 2
public List getTasks() {
   if (tasks == null)
       tasks = new ArrayList();
   return tasks;
}

这似乎不像是一个getter,对吧?此外,《Effective Java》第15条:最小化可变性告诉我们对象应该是不可变的。 - Matt Ball
2
getMinutes 返回一个 int(问题解决了,null 本来就是一个错误的值),并且总是在构造函数中或直接声明时初始化列表。 - Tunaki
@MattBall,你为什么认为那似乎不再是一个getter了呢? - Luis Celestino
1
因为它操纵了所请求字段的值,从而假装了某些不真实的东西。 - Tom
3个回答

4
简短的回答是:这取决于具体情况
更具体地说,这个问题无法一般性地回答,需要看整个类并了解其设计。空检查是实现细节,在某些设计中是合适的,但在其他设计中可能是有问题的。
总的来说,方法必须遵守其契约,不能超出范围。因此,从 API 设计的角度来看,这个问题是不相关的。你应该根据一长串现有的最佳实践来设计你的 API,然后再设计你的实现以适应它。其中一个最佳实践是,在返回 Collection 时不要将“零元素”特殊处理为 null。也就是说,如果你的方法可以合理地返回一个空集合,它应该返回一个空集合,而不是一个特殊值,比如 null。这几乎总是简化客户端代码,并消除了一整类潜在的 NPEs。这并不意味着你不能在内部使用 null 来标记“零元素”,但这意味着如果你这样做了,你应该在返回时执行转换(如你的示例中所示)。
因此,如果您的getTasks()方法指定它始终返回一个(可能为空的)列表,则像您的示例中使用null检查是合理的实现选择。在您具体的示例中,这可能不是最佳选择,因为JDK提供了像Collections.emptyList<T>()这样的方法,允许您廉价地重用单例空列表实例,因此您在内存方面没有任何节省,并且CPU节省是可疑的1
此外,通过使用“null表示空列表”的约定,您要求所有内部方法都必须检查tasks == null,否则使用getTasks()方法。在集合对象的特定情况中,我通常更喜欢使用Collections.empty*方法而不是null来表示空列表2。类似的推理适用于Integer情况,因为Integer.valueOf(0)所有兼容的JVM上返回一个单例对象。

除了那些特殊情况,当没有好的“空”元素存在或性能很重要时,确实有理由使用空检查来表示缺失或默认元素。你会发现这种模式在设计良好的库中被频繁使用,例如Java运行时和Guava。因此,不能说这是一种不好的做法。它是一种需要仔细评估的做法。


1这些Collection.empty*方法的使用也适用于您的示例。在您的空值情况下,您可以考虑直接返回Collections.emptyList(),而不是按需创建新的ArrayList。是否适当取决于整个类设计,例如该列表是否可变。

2Collections.emptyList()是否比显式的空检查更快实际上是一个复杂的问题。如果空列表很少在给定进程中出现,那么emptyList()方法可能更快,因为其成本(相对于getter)随着空列表频率趋近于零而变为零。另一方面,当空列表确实出现时,它可能会为使用List的所有方法创建多态调用站点:每个这样的站点都可能看到特殊的“空列表”实现和ArrayList。这可能会减慢代码,尽管很多取决于内联决策。因此,像通常的性能考虑一样,确切的答案是复杂的,需要进行分析,如果细节很重要。


谢谢解释。但是如果直接返回Collections.emptyList(),然后立即调用add()方法会怎么样呢? - Luis Celestino
这取决于你的类的设计。精心设计的类很少返回其内部状态的可变视图,而“tasks”是内部可变状态。我假设你的类不仅仅是简单地包装任务列表(否则,为什么不直接使用ArrayList<Tasks>?) - 在这种情况下,它通常需要维护一致的内部状态(例如,任务可能具有特定的顺序,最高优先级在列表的前面)。将列表暴露给任意修改会使这个过程变得困难或不可能。这就是为什么大多数精心设计的类返回不可变列表的原因。 - BeeOnRope
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - BeeOnRope

3
一般来说,是的,这样做是不好的实践。
例如1,如果对象的值(在这种情况下是一个整数)确实为null,则API用户期望返回null。而不是0。例如,也许您的API的用户将0视为需要0分钟的操作,而null可能被解释为某些事情从未发生过。
例如2,API用户不希望getter具有构造新列表等副作用。
除非您在方法的契约(javadoc)中包含这些行为,否则不能指望用户意识到实际正在发生什么,这可能导致他们使用不正确的数据。相反,他们应该预期这些方法可能会产生null,并自行检查它。
但是,您可以通过使用注释(例如javax.annotation.Nullable和javax.annotation.Nonnull)来帮助他们,向用户(及其IDE)暗示返回值可以或不可以为空。这样,他们可以智能地预测并对您的方法结果的可空性做出反应。
注意:正如Robert在他的答案中所说,有些情况下懒加载是有意义的。但是必须小心进行,仅在有意义的上下文中进行。Joshua Bloch在他的书“Effective Java,Second Edition”中写了一篇关于Lazy Initialization的子章节,在其中解释了Lazy Initialization应该被“谨慎地”执行,并有效地称之为预优化。

2
我认为返回 null 总是不好的选择,你应该返回一个 Optional(或抛出一些异常):这是让用户意识到“注意!这可能为空”的最佳方式。很遗憾,没有人会读文档。 - Tunaki
@Tunaki 我不同意返回 null 总是一个坏主意。Optional 是 Java 中相当新的构造,它会很有用,但不能完全替代 null。确实,很多程序员忽略文档,这是不幸的。但一个人的无知不应该成为另一个人脚上的铁链。有效地使用 null 是一种技能,但如果做得好,它是非常 强大 的,不应该被视为代码异味或类似的东西。 - nasukkin
@nasukkin 第一个例子实际上是基于我正在使用JAXB时遇到的问题。它似乎忽略了null值,这反过来又不会进入xml。问题是消费者API需要minutes - Luis Celestino
1
@LuisCelestino 感谢澄清。但是JAXB有点不同。如果外部API严格要求一个值,那么该API的所有者就会强制你采取行动,在这种情况下,最好编写一个包装器,围绕原始类编写一个可以生成默认值的包装器,其中null不允许。或者,您可能希望查看JAXB XmlElement注释的nillable选项:https://jaxb.java.net/nonav/2.1.3/docs/api/javax/xml/bind/annotation/XmlElement.html#nillable() - nasukkin
1
@nasukkin非常感谢您提供的JAXB链接。在我的情况下,似乎@XmlElement(defaultValue = "0")是正确的选择。 - Luis Celestino
1
@LuisCelestino 如果那个可行的话,另一个不错的选择! - nasukkin

2
您所描述的是一种设计模式,称为“懒加载”(https://en.wikipedia.org/wiki/Lazy_loading)。如果在特定情况下使用,它是一种很好的实践,但并非万能解决方案或仅仅出于兴趣而使用的东西。

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