关于java:空检查链与捕获NullPointerException

Null check chain vs catching NullPointerException

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

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

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

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

1
2
3
4
5
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();

写可以接受吗

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

或者这会被视为反模式?


抓到EDOCX1[0]是一件很麻烦的事情,因为它们几乎可以发生在任何地方。从一个错误中得到一个错误是非常容易的,偶然地抓住它,然后像一切正常一样继续下去,从而隐藏了一个真正的问题。这是很难处理的,所以最好完全避免。(例如,考虑自动拆箱一个空的Integer。)

我建议你用Optional类代替。当您想要处理存在或不存在的值时,这通常是最好的方法。

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

1
2
3
4
5
6
7
8
9
10
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 an -1 int instead of an empty optional if any is null
        // .orElse(-1);
        // Or this if you want to throw an exception instead
        // .orElseThrow(SomeApplicationException::new);
}

为什么选择?

使用Optionals而不是null来表示可能不存在的值,这使得读者能够非常清楚地看到这一事实,并且类型系统将确保您不会意外地忘记它。

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

缺勤有效还是错误?

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

也许更多的选择?

另一方面,如果中间方法中缺少的值是有效的,也许您也可以为它们切换到Optionals?

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

1
2
3
4
5
6
public Optional<Integer> mo(Ws wsObject) {
    return wsObject.getFoo()
        .flatMap(f -> f.getBar())
        .flatMap(b -> b.getBaz())
        .flatMap(b -> b.getInt());        
}

为什么不选?

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


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

1
2
3
4
requireNonNull(requireNonNull(requireNonNull(
    wsObject,"wsObject is null")
        .getFoo(),"getFoo() is null")
            .getBar(),"getBar() is null");

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

抛出NullPointerException也不是最好的选择。您可以提供自己的异常(使其处于选中状态以确保用户能够处理它,或者不选中它以更简单的方式处理它),或者使用您正在使用的XML解析器中的特定异常。


假设类结构确实不在我们的控制范围内,正如看起来的那样,我认为按照问题中的建议捕获NPE确实是一个合理的解决方案,除非性能是一个主要问题。一个小的改进可能是包装抛出/捕获逻辑以避免混乱:

1
2
3
4
5
6
7
static <T> T get(Supplier<T> supplier, T defaultValue) {
    try {
        return supplier.get();
    } catch (NullPointerException e) {
        return defaultValue;
    }
}

现在您只需执行以下操作:

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

正如汤姆在评论中所指出的,

以下声明违反了德米特的法律,

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

你想要的是int,你可以从Foo那里得到。德米特定律说,不要和陌生人说话。对于您的案例,您可以将实际的实现隐藏在FooBar的框架下。

现在,您可以在Foo中创建方法,从Baz中获取int。最终,Foo将有Bar,在Bar中,我们可以访问int,而不直接向Foo暴露Baz。因此,空检查可能被划分为不同的类,并且类之间只共享必需的属性。


我的答案几乎与@janki在同一行,但我想稍微修改一下代码片段,如下所示:

1
2
3
4
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添加一个空检查。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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();


你说有些方法"可能返回null",但没有说在什么情况下返回null。你说你抓住了埃多克斯,但你没有说你为什么抓住它。这种缺乏信息的情况表明,您不清楚什么是例外,以及为什么它们优于其他选择。好的。

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

使用异常,我们可以编写成功的方法作为后置条件。如果方法返回,则成功。如果它抛出一个异常,它就失败了。为了清晰起见,这是一个巨大的胜利。我们可以编写清晰处理正常、成功案例的代码,并将所有错误处理代码移入catch子句。通常会发现,方法失败的方式或原因的细节对调用方并不重要,因此同一个catch子句可以用于处理几种类型的失败。而且,经常会发生这样的情况:一个方法根本不需要捕获异常,而只需要允许它们传播到调用方。由于程序错误而产生的异常在后一类中;当存在错误时,很少有方法能够做出适当的反应。好的。

所以,那些返回null的方法。好的。

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

其中一个答案声称链式方法调用违反了德米特定律,因此是不好的。那个说法是错误的。好的。

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

好啊。


我想添加一个重点关注错误含义的答案。空异常本身不提供任何意义上的完全错误。所以我建议不要直接和他们打交道。

有上千种情况下您的代码可能出错:无法连接到数据库、IO异常、网络错误…如果你一个接一个地处理它们(比如这里的空支票),那将是一件麻烦事。

在代码中:

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

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

和Xenteros的答案一样,我建议使用自定义未检查异常。例如,在这种情况下:foo可以为空(有效数据),但是bar和baz不应该为空(无效数据)。

代码可以重新编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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"
    }
}


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


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

无论您想在哪里调用该方法,都必须捕获异常(或者它将向上传播到堆栈)。然而,如果在您的案例中,您可以继续使用值为-1的结果,并且您确信它不会传播,因为您没有使用任何可能为空的"片段",那么我似乎可以捕捉到它。

编辑:

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


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
  public class Person {
    private Address address;

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.todelete;

public class Address {
    private SomeCrap someCrap;

    public SomeCrap getSomeCrap() {
        return someCrap;
    }

    public void setSomeCrap(SomeCrap someCrap) {
        this.someCrap = someCrap;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.todelete;

public class SomeCrap {
    private String crap;

    public String getCrap() {
        return crap;
    }

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

产量

三点二一六

零点零零二

我在这里看到了一个明显的赢家。拥有if检查比捕获异常要便宜得多。我已经看到了Java-8的工作方式。考虑到目前70%的应用程序仍然运行在Java-7上,我添加了这个答案。

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


不要抓NullPointerException。你不知道它是从哪里来的(我知道这在你的情况下是不可能的,但可能是其他东西扔了它),而且它很慢。您要访问指定的字段,因此每个其他字段都不能为空。这是检查每个字段的一个完全有效的理由。我可能会在一个if中检查它,然后创建一个可读性方法。正如其他人所指出的,已经返回-1是非常古老的学校,但我不知道你是否有理由这样做(例如,与另一个系统交谈)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)。


如果不想重构代码,而可以使用Java 8,则可以使用方法引用。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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;
            }
}

产量

241
4

null
2

接口Getter只是一个功能接口,您可以使用任何等效的接口。GetterResult类,为清晰起见,去掉了访问器,保留getter链(如果有)的结果或最后一个getter调用的索引。

方法getterChain是一个简单的样板代码,可以自动(或在需要时手动)生成。我对代码进行了结构化,以便重复块是不言而喻的。

这不是一个完美的解决方案,因为您仍然需要为每个getter数定义一个getterChain重载。

我改为重构代码,但是如果不能,并且您发现自己经常使用长getter链,那么您可以考虑使用2到10个getter的重载来构建类。


1
return wsObject.getFooBarBazInt();

通过运用德米特定律,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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;
    }
}

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

考虑到您可能无法控制生成的源代码,并且假设您确实只需要在这里或那里访问一些深度嵌套的字段,那么我建议用一个方法包装每个深度嵌套的访问。

1
2
3
4
5
6
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服务中来回发送helper方法,而深度嵌套的对象模型是常见的,以避免多次API调用),因此不适合直接在客户机中使用。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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();
    }

}

如果效率是一个问题,那么应该考虑"捕获"选项。如果不能使用"catch",因为它会传播(如"scouto"所述),则使用局部变量来避免对方法getFoo()getBar()getBaz()的多次调用。


值得考虑创建自己的异常。我们称之为肌动手术失败感觉。您可以抛出它,而不是返回一个值。结果将是相同的——您将退出该函数,但不会返回硬编码值1,它是Java反模式。在Java中,我们使用异常。

1
2
3
4
5
try {
    return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
    throw new MyOperationFailedException();
}

编辑:

根据评论中的讨论,让我在以前的想法中添加一些东西。在这个代码中有两种可能性。一个是接受空值,另一个是错误。

如果这是一个错误并且发生了,那么当断点不够时,可以使用其他结构来调试代码。

如果可以接受的话,你不在乎这个空值出现在哪里。如果你这样做了,你绝对不应该链接那些请求。


我编写了一个名为Snag的类,它允许您定义一个路径来浏览对象树。以下是其使用示例:

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

即实例ENGINE_NAME对传递给它的实例有效地调用Car?.getEngine()?.getName(),如果引用返回null则返回null

1
final String name =  ENGINE_NAME.get(firstCar);

它没有在Maven上发布,但是如果有人发现它有用,它就在这里(当然没有担保!)

这有点基础,但似乎能完成任务。显然,Java和其他JVM语言支持安全导航或EDCOX1〔5〕的最新版本已经过时了。


给出与其他人不同的答案。

I recommend you to check for NULL in ifs.

原因:

We should not leave a single chance for our program to be crashed.
NullPointer is generated by system. The behaviour of System
generated exceptions can not be predicted. You should not leave your
program in the hands of System when you already have a way of handling
it by your own. And put the Exception handling mechanism for the extra safety.!!

为了使代码易于阅读,请尝试使用此方法检查条件:

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

编辑:

Here you need to store these values wsObject.getFoo(),
wsObject.getFoo().getBar(), wsObject.getFoo().getBar().getBaz() in
some variables. I am not doing it because i don't know the return
types of that functions.

如有任何建议,我们将不胜感激。!