Null 检查链与捕获 NullPointerException

Web 服务返回一个巨大的 XML,我需要访问它的深度嵌套字段,例如:

return wsObject.getFoo().getBar().getBaz().getInt()

问题是 getFoo()getBar()getBaz()都可能返回 null

但是,如果我在所有情况下都检查 null,代码就会变得非常冗长和难以阅读。此外,我可能会错过一些领域的检查。

if (wsObject.getFoo() == null) return -1;
if (wsObject.getFoo().getBar() == null) return -1;
// maybe also do something with wsObject.getFoo().getBar()
if (wsObject.getFoo().getBar().getBaz() == null) return -1;
return wsObject.getFoo().getBar().getBaz().getInt();

可以写

try {
return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
return -1;
}

还是说这就是反模式?

19083 次浏览

NullPointerException是一个运行时异常,因此一般不建议捕获它,而是要避免它。

您必须在需要调用该方法的任何地方捕获异常(否则它将向上传播堆栈)。尽管如此,如果在您的情况下,您可以继续使用值为 -1的结果,并且您确信它不会传播,因为您没有使用任何可能为 null 的“片段”,那么在我看来捕获它是正确的

编辑:

我同意@xenteros 后面的 回答,最好启动自己的异常,而不是返回 -1,例如,可以称之为 InvalidXMLException

值得考虑创建自己的 Exception。我们称之为 MyOperationFAILedException。您可以抛出它,而不是返回一个值。结果将是相同的-您将退出函数,但不会返回硬编码的值 -1,这是 Java 反模式。在 Java 中我们使用异常。

try {
return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
throw new MyOperationFailedException();
}

编辑:

根据评论中的讨论,让我补充一些我以前的想法。在这段代码中有两种可能性。一个是您接受 null,另一个是,它是一个错误。

如果出现了错误,当断点不足时,可以使用其他结构调试代码,以便进行调试。

如果它是可以接受的,那么您就不必关心这个 null 出现在哪里,如果您关心,那么您就绝对不应该链接这些请求。

如果效率是一个问题,那么应该考虑“捕捉”选项。 如果‘ catch’不能使用,因为它会传播(正如‘ SCouto’所提到的) ,那么使用本地变量来避免对方法 getFoo()getBar()getBaz()的多次调用。

我建议考虑 Objects.requireNonNull(T obj, String message)。您可以为每个异常构建带有详细消息的链,如

requireNonNull(requireNonNull(requireNonNull(
wsObject, "wsObject is null")
.getFoo(), "getFoo() is null")
.getBar(), "getBar() is null");

我建议您不要使用特殊的返回值,如 -1。这不是 Java 风格。Java 设计了异常机制,以避免这种来自 C 语言的老式方法。

抛出 NullPointerException也不是最好的选择。您可以提供自己的异常(将其设置为 检查过了,以保证它将由用户或 不受约束以更简单的方式处理)或使用正在使用的 XML 解析器中的特定异常。

给出与众不同的答案。

我建议你检查 if中的 NULL

理由:

我们不应该给我们的程序留下任何崩溃的机会。 NullPointer 是由 System 生成的。 < strong > System 的行为 生成的异常无法预测 程序在系统手中时,你已经有了一个处理方法 it by your own. And put the Exception handling mechanism for the extra safety.!!

为了使您的代码易于阅读,请尝试以下方法来检查条件:

if (wsObject.getFoo() == null || wsObject.getFoo().getBar() == null || wsObject.getFoo().getBar().getBaz() == null)
return -1;
else
return wsObject.getFoo().getBar().getBaz().getInt();

EDIT :

这里需要存储这些值 wsObject.getFoo(), wsObject.getFoo().getBar(), wsObject.getFoo().getBar().getBaz() in 一些变量,我不这么做是因为我不知道返回值 types of that functions.

Any suggestions will be appreciated..!!

My answer goes almost in the same line as @janki, but I would like to modify the code snippet slightly as below:

if (wsObject.getFoo() != null && wsObject.getFoo().getBar() != null && wsObject.getFoo().getBar().getBaz() != null)
return wsObject.getFoo().getBar().getBaz().getInt();
else
return something or throw exception;

您还可以为 wsObject添加一个 null 检查,如果该对象有任何可能为 null 的话。

不要抓 NullPointerException。你不知道它是从哪里来的(我知道这是不太可能的,在你的情况下,但 也许吧其他东西扔它) ,它是缓慢的。 您希望访问指定的字段,并且为此,每个其他字段必须不为空。这是检查每个字段的完美有效理由。我可能会检查它在一个如果,然后创建一个方法的可读性。正如其他人指出的那样,返回 -1是非常老派的做法,但我不知道您是否有这样做的理由(例如,与另一个系统交谈)。

public int callService() {
...
if(isValid(wsObject)){
return wsObject.getFoo().getBar().getBaz().getInt();
}
return -1;
}




public boolean isValid(WsObject wsObject) {
if(wsObject.getFoo() != null &&
wsObject.getFoo().getBar() != null &&
wsObject.getFoo().getBar().getBaz() != null) {
return true;
}
return false;
}

编辑: 如果它违反了得墨忒耳定律,这是有争议的,因为 WsObject 可能只是一个数据结构(检查 https://stackoverflow.com/a/26021695/1528880)。

捕捉 NullPointerException非常有问题的事情,因为它们几乎可以发生在任何地方。很容易从一个错误中得到一个,意外地抓住它,然后继续假装一切正常,这样就隐藏了一个真正的问题。处理起来很棘手,所以最好还是避免。(例如,考虑自动解除空 Integer的装箱。)

我建议您改用 Optional类。当您希望处理存在或不存在的值时,这通常是最好的方法。

使用它,您可以像这样编写代码:

public Optional<Integer> m(Ws wsObject) {
return Optional.ofNullable(wsObject.getFoo()) // Here you get Optional.empty() if the Foo is null
.map(f -> f.getBar()) // Here you transform the optional or get empty if the Bar is null
.map(b -> b.getBaz())
.map(b -> b.getInt());
// Add this if you want to return null instead of an empty optional if any is null
// .orElse(null);
// Or this if you want to throw an exception instead
// .orElseThrow(SomeApplicationException::new);
}

为什么选择?

对可能不存在的值使用 Optional而不是 null可以使读者非常清楚地看到这个事实,并且类型系统将确保您不会意外地忘记它。

您还可以更方便地访问使用这些值的方法,如 maporElse


Is absence valid or error?

但是还要考虑中间方法返回 null 是否是有效的结果,或者这是否是错误的标志。如果它始终是一个错误,那么抛出异常可能比返回特殊值或中间方法本身抛出异常更好。


还有更多选择吗?

另一方面,如果中间方法中不存在的值是有效的,也许您也可以为它们切换到 Optional

然后你可以像这样使用它们:

public Optional<Integer> mo(Ws wsObject) {
return wsObject.getFoo()
.flatMap(f -> f.getBar())
.flatMap(b -> b.getBaz())
.flatMap(b -> b.getInt());
}

为什么不选择呢?

我能想到的不使用 Optional的唯一原因是,这是否在代码的性能关键部分,以及垃圾收集开销是否是一个问题。这是因为每次执行代码时都会分配一些 Optional对象,而 VM 也许吧无法优化这些对象。在这种情况下,最初的 if-test 可能会更好。

汤姆在评论中已经指出,

Following statement disobeys the 得墨忒耳定律,

wsObject.getFoo().getBar().getBaz().getInt()

你想要的是 int,你可以从 Foo得到它。得墨忒耳定律永远不要和陌生人说话。对于您的情况,您可以在 FooBar的引擎罩下隐藏实际的实现。

Now, you can create method in Foo to fetch int from Baz. Ultimately, Foo will have Bar and in Bar we can access Int without exposing Baz directly to Foo. So, null checks are probably divided to different classes and only required attributes will be shared among the classes.

为了提高可读性,您可能需要使用多个变量,如

Foo theFoo;
Bar theBar;
Baz theBaz;


theFoo = wsObject.getFoo();


if ( theFoo == null ) {
// Exit.
}


theBar = theFoo.getBar();


if ( theBar == null ) {
// Exit.
}


theBaz = theBar.getBaz();


if ( theBaz == null ) {
// Exit.
}


return theBaz.getInt();

If you don't want to refactor the code and you can use Java 8, it is possible to use Method references.

首先是一个简单的演示(请原谅静态内部类)

public class JavaApplication14
{
static class Baz
{
private final int _int;
public Baz(int value){ _int = value; }
public int getInt(){ return _int; }
}
static class Bar
{
private final Baz _baz;
public Bar(Baz baz){ _baz = baz; }
public Baz getBar(){ return _baz; }
}
static class Foo
{
private final Bar _bar;
public Foo(Bar bar){ _bar = bar; }
public Bar getBar(){ return _bar; }
}
static class WSObject
{
private final Foo _foo;
public WSObject(Foo foo){ _foo = foo; }
public Foo getFoo(){ return _foo; }
}
interface Getter<T, R>
{
R get(T value);
}


static class GetterResult<R>
{
public R result;
public int lastIndex;
}


/**
* @param args the command line arguments
*/
public static void main(String[] args)
{
WSObject wsObject = new WSObject(new Foo(new Bar(new Baz(241))));
WSObject wsObjectNull = new WSObject(new Foo(null));


GetterResult<Integer> intResult
= getterChain(wsObject, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt);


GetterResult<Integer> intResult2
= getterChain(wsObjectNull, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt);




System.out.println(intResult.result);
System.out.println(intResult.lastIndex);


System.out.println();
System.out.println(intResult2.result);
System.out.println(intResult2.lastIndex);


// TODO code application logic here
}


public static <R, V1, V2, V3, V4> GetterResult<R>
getterChain(V1 value, Getter<V1, V2> g1, Getter<V2, V3> g2, Getter<V3, V4> g3, Getter<V4, R> g4)
{
GetterResult result = new GetterResult<>();


Object tmp = value;




if (tmp == null)
return result;
tmp = g1.get((V1)tmp);
result.lastIndex++;




if (tmp == null)
return result;
tmp = g2.get((V2)tmp);
result.lastIndex++;


if (tmp == null)
return result;
tmp = g3.get((V3)tmp);
result.lastIndex++;


if (tmp == null)
return result;
tmp = g4.get((V4)tmp);
result.lastIndex++;




result.result = (R)tmp;


return result;
}
}

Output

241
4

无效
2

The interface Getter is just a functional interface, you may use any equivalent.
GetterResult类,为了清晰起见,去掉了访问器,保存 getter 链的结果(如果有的话) ,或者最后调用的 getter 的索引。

方法 getterChain是一段简单的样板代码,可以自动生成(或者在需要时手动生成)。
我设计了代码的结构,这样重复的代码块就不言自明了。


This is not a perfect solution as you still need to define one overload of getterChain per number of getters.

我会转而重构代码,但是如果不能,并且您发现自己经常使用长的 getter 链,那么您可以考虑构建一个包含从2到,比如说,10个 getter 的重载的类。

正如其他人所说,尊重得墨忒尔定律绝对是解决方案的一部分。另一个部分,只要可能,就是更改那些链接方法,使它们不能返回 null。您可以通过返回一个空的 String、一个空的 Collection或其他虚拟对象来避免返回 null,这些虚拟对象意味着或执行调用者将对 null执行的任何操作。

I'd like to add an answer which focus on the 错误的含义. Null exception in itself doesn't provide any meaning full error. So I'd advise to avoid dealing with them directly.

有成千上万的情况下,你的代码可能会出错: 无法连接到数据库,IO 异常,网络错误... 如果你处理他们一个一个(像这里的空检查) ,这将是太多的麻烦。

密码是:

wsObject.getFoo().getBar().getBaz().getInt();

即使知道哪个字段为空,也不知道哪里出错了。也许酒吧是无效的,但它是预期的吗?还是数据错误?想想那些读过你代码的人

Like in xenteros's answer, I'd propose using 自定义未检查异常. For example, in this situation: Foo can be null (valid data), but Bar and Baz should never be null (invalid data)

代码可以重写:

void myFunction()
{
try
{
if (wsObject.getFoo() == null)
{
throw new FooNotExistException();
}


return wsObject.getFoo().getBar().getBaz().getInt();
}
catch (Exception ex)
{
log.error(ex.Message, ex); // Write log to track whatever exception happening
throw new OperationFailedException("The requested operation failed")
}
}




void Main()
{
try
{
myFunction();
}
catch(FooNotExistException)
{
// Show error: "Your foo does not exist, please check"
}
catch(OperationFailedException)
{
// Show error: "Operation failed, please contact our support"
}
}

假设班级结构确实不在我们的控制范围之内(看起来是这样的) ,我认为抓住问题中提到的 NPE 确实是一个合理的解决方案,除非性能是一个主要问题。一个小的改进可能是包装了 throw/catch 逻辑以避免混乱:

static <T> T get(Supplier<T> supplier, T defaultValue) {
try {
return supplier.get();
} catch (NullPointerException e) {
return defaultValue;
}
}

Now you can simply do:

return get(() -> wsObject.getFoo().getBar().getBaz().getInt(), -1);

您说有些方法“可能返回 null”,但是没有说明在什么情况下它们返回 null。你说你赶上了 NullPointerException,但你没有说为什么你赶上它。这种信息的缺乏表明您并不清楚异常的用途以及为什么它们优于替代方案。

考虑一个类方法,这个类方法是用来执行一个操作的,但是这个方法不能 guarantee,它将执行这个操作,因为环境超出了它的控制范围(实际上是 Java 中 < em > all 方法的情况)。我们调用这个方法,它返回。调用该方法的代码需要知道它是否成功。它怎么会知道?如何构建它来应对成功或失败这两种可能性?

使用异常,我们可以编写具有 作为后置条件的成功的方法。如果方法返回,则表示成功。如果它抛出异常,那么它就失败了。这是个重大的胜利。我们可以编写清楚地处理正常情况、成功情况的代码,并将所有错误处理代码移动到 catch子句中。通常情况下,方法如何失败或为什么失败的细节对调用方来说并不重要,因此可以使用相同的 catch子句来处理多种类型的失败。而且经常发生的情况是,一个方法不需要捕获异常 完全没有,而只是允许它们传播到 它的调用者。由于程序错误而导致的异常发生在后一个类中; 当出现错误时,很少有方法能够作出适当的反应。

那些返回 null的方法。

  • null值是否表示代码中存在 bug?如果是这样,那么根本就不应该捕获异常。而且您的代码不应该试图对自己进行二次猜测。只要写清楚和简洁的假设,它将工作。方法调用链是否清晰简洁?那就用吧。
  • null值是否表示程序的输入无效?如果是这样,那么抛出 NullPointerException就不是一个合适的异常,因为按照惯例,它是为指示 bug 而保留的。您可能希望抛出从 IllegalArgumentException(如果您想要 未检查的异常)或 IOException(如果您想要检查异常)派生的自定义异常。当有无效输入时,是否需要程序提供详细的语法错误消息?如果是这样,检查每个方法的 null返回值,然后抛出适当的诊断异常是您唯一可以做的事情。如果您的程序不需要提供详细的诊断,那么将方法调用链接在一起,捕获任何 NullPointerException,然后抛出自定义异常是最清晰和最简洁的。

其中一个答案声称链式方法调用违反了 得墨忒耳定律,因此是不好的。这种说法是错误的。

  • 当谈到程序设计时,并没有什么绝对的规则来决定什么是好的,什么是坏的。只有启发式: 大部分时间(甚至几乎所有时间)是正确的规则。编程技巧的一部分就是知道什么时候可以打破这些规则。因此,“这违反了 X规则”的简短断言根本不是真正的答案。这是违反 应该规则的情况之一吗?
  • 得墨忒耳定律实际上是一个关于 API 或类接口设计的规则。在设计类时,使用 抽象的层次结构是有用的。您有一些低级类,它们使用语言原语直接执行操作,并在比语言原语更高级的抽象中表示对象。您有一些中级类,它们委托给低级类,并在比低级类更高的级别上实现操作和表示。您有委托给中级类的高级类,并实现更高级别的操作和抽象。(我在这里只讨论了三个抽象层次,但是还有更多可能性)。这允许您的代码在每个级别上以适当的抽象来表达自己,从而隐藏了复杂性。得墨忒耳定律的基本原理是,如果你有一连串的方法调用,这意味着你有一个高级类通过中级类直接处理低级细节,因此你的中级类没有提供高级类需要的中级抽象操作。但这里的情况似乎是 没有: 您没有设计方法调用链中的类,它们是一些自动生成的 XML 序列化代码的结果(对吗?),并且调用链不会在抽象层次结构中降级,因为反序列化的 XML 都处于抽象层次结构的同一级别(对吗?)?

从昨天就开始关注这篇文章了。

我一直在评论/投票的评论说,抓住 NPE 是不好的。这就是为什么我一直这样做。

package com.todelete;


public class Test {
public static void main(String[] args) {
Address address = new Address();
address.setSomeCrap(null);
Person person = new Person();
person.setAddress(address);
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
try {
System.out.println(person.getAddress().getSomeCrap().getCrap());
} catch (NullPointerException npe) {


}
}
long endTime = System.currentTimeMillis();
System.out.println((endTime - startTime) / 1000F);
long startTime1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
if (person != null) {
Address address1 = person.getAddress();
if (address1 != null) {
SomeCrap someCrap2 = address1.getSomeCrap();
if (someCrap2 != null) {
System.out.println(someCrap2.getCrap());
}
}
}
}
long endTime1 = System.currentTimeMillis();
System.out.println((endTime1 - startTime1) / 1000F);
}
}

  public class Person {
private Address address;


public Address getAddress() {
return address;
}


public void setAddress(Address address) {
this.address = address;
}
}

package com.todelete;


public class Address {
private SomeCrap someCrap;


public SomeCrap getSomeCrap() {
return someCrap;
}


public void setSomeCrap(SomeCrap someCrap) {
this.someCrap = someCrap;
}
}

package com.todelete;


public class SomeCrap {
private String crap;


public String getCrap() {
return crap;
}


public void setCrap(String crap) {
this.crap = crap;
}
}

输出

3.216

0.002

我看到了一个明显的赢家。使用 if 检查比捕捉异常要便宜得多。我见过 Java-8的做法。考虑到目前70% 的应用程序仍然在 Java-7上运行,我添加了这个答案。

底线 对于任何关键任务应用程序,处理 NPE 都是昂贵的。

I wrote a class called Snag which lets you define a path to navigate through a tree of objects. Here is an example of its use:

Snag<Car, String> ENGINE_NAME = Snag.createForAndReturn(Car.class, String.class).toGet("engine.name").andReturnNullIfMissing();

这意味着实例 ENGINE_NAME将有效地调用传递给它的实例上的 Car?.getEngine()?.getName(),并在任何引用返回 null时返回 null:

final String name =  ENGINE_NAME.get(firstCar);

它没有在 Maven 上发布,但是如果有人发现这个有用的话,它是 给你(当然没有保修!)

这是一个有点基本,但似乎做的工作。显然,随着 Java 和其他支持安全导航或 Optional的 JVM 语言的最新版本的出现,它已经过时了。

您所使用的方法很长,但是非常可读。如果我是一个新的开发人员来到您的代码库,我可以很快地看到您正在做什么。大多数其他的答案(包括捕捉异常)似乎并没有使事情变得更具可读性,在我看来,有些答案使事情变得不那么具有可读性。

Given that you likely don't have control over the generated source and assuming you truly just need to access a few deeply nested fields here and there then I would recommend wrapping each deeply nested access with a method.

private int getFooBarBazInt() {
if (wsObject.getFoo() == null) return -1;
if (wsObject.getFoo().getBar() == null) return -1;
if (wsObject.getFoo().getBar().getBaz() == null) return -1;
return wsObject.getFoo().getBar().getBaz().getInt();
}

如果你发现自己编写了很多这样的方法,或者你发现自己很想创建这些公共静态方法,那么我会创建一个单独的对象模型,按照你的意愿嵌套,只包含你关心的字段,然后从 Web 服务对象模型转换到你的对象模型。

当您与远程 Web 服务通信时,典型的情况是有一个“远程域”和一个“应用程序域”,并在两者之间切换。远程域通常受到 Web 协议的限制(例如,你不能在纯 RESTful 服务中来回发送助手方法,深度嵌套的对象模型通常可以避免多个 API 调用) ,所以不适合直接在客户端使用。

For example:

public static class MyFoo {


private int barBazInt;


public MyFoo(Foo foo) {
this.barBazInt = parseBarBazInt();
}


public int getBarBazInt() {
return barBazInt;
}


private int parseFooBarBazInt(Foo foo) {
if (foo() == null) return -1;
if (foo().getBar() == null) return -1;
if (foo().getBar().getBaz() == null) return -1;
return foo().getBar().getBaz().getInt();
}


}
return wsObject.getFooBarBazInt();

运用得墨忒耳律法,

class WsObject
{
FooObject foo;
..
Integer getFooBarBazInt()
{
if(foo != null) return foo.getBarBazInt();
else return null;
}
}


class FooObject
{
BarObject bar;
..
Integer getBarBazInt()
{
if(bar != null) return bar.getBazInt();
else return null;
}
}


class BarObject
{
BazObject baz;
..
Integer getBazInt()
{
if(baz != null) return baz.getInt();
else return null;
}
}


class BazObject
{
Integer myInt;
..
Integer getInt()
{
return myInt;
}
}