空检查链与捕获NullPointerException

时间:2016-06-22 06:57:03

标签: java exception nullpointerexception null custom-error-handling

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;
}

还是会被视为反模式?

19 个答案:

答案 0 :(得分:131)

抓住NullPointerException真正有问题的事情,因为它们几乎可以在任何地方发生。很容易从一个bug中获取一个,偶然捕获并继续好像一切正​​常,从而隐藏一个真正的问题。 处理起来非常棘手,所以最好完全避免。(例如,考虑自动取消装箱空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 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);
}

为什么选择?

对于可能缺席的值,使用Optional而不是null会使读者非常明显和清楚这一事实,类型系统将确保您不会意外忘记它。< / p>

您还可以更方便地访问使用此类值的方法,例如maporElse

缺席有效还是错误?

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

也许更多的选择?

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

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

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-tests可能会更好。

答案 1 :(得分:11)

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

id2

我建议你不要使用特殊的返回值,比如requireNonNull(requireNonNull(requireNonNull( wsObject, "wsObject is null") .getFoo(), "getFoo() is null") .getBar(), "getBar() is null"); 。这不是Java风格。 Java设计了异常机制,以避免使用来自C语言的这种老式方法。

投掷-1也不是最好的选择。您可以提供自己的例外(使其选中以确保它将由用户处理或未选中以更简单的方式处理它)或使用特定的例外您正在使用的XML解析器。

答案 2 :(得分:6)

Assuming the class structure is indeed out of our control, as seems to be the case, I think catching the NPE as suggested in the question is indeed a reasonable solution, unless performance is a major concern. One small improvement might be to wrap the throw/catch logic to avoid clutter:

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);

答案 3 :(得分:5)

正如Tom在评论中已经指出的那样,

以下声明违反了Law of Demeter

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

你想要的是int,你可以从Foo获得它。 德米特法则说,从不与陌生人交谈。对于您的情况,您可以隐藏FooBar

范围内的实际实施

现在,您可以在Foo中创建方法,以便从int获取Baz。最终,FooBarBar我们可以Int访问Baz,而不会将Foo直接暴露给var myArray = ['360', '330', '300', '270', '240', '210', '180', '150', '120', '90', '60', '30']; var Spinner1 = sym.$('Spinner1'); Spinner1.click(function(){ // randomize the degree of spin. var mySpin = myArray[Math.floor(Math.random() * myArray.length)]; sym.getSymbol('Spinner1').play(); Spinner1.css({ '-webkit-transform': 'rotate(' + mySpin + 'deg)', '-moz-transform': 'rotate(' + mySpin + 'deg)', '-ms-transform': 'rotate(' + mySpin + 'deg)', '-o-transform': 'rotate(' + mySpin + 'deg)', 'transform': 'rotate(' + mySpin + 'deg)', }); Spinner1.css('-webkit-transition','all 500ms cubic-bezier(0.420, 0.000, 1.000, 1.000)'); if (mySpin > 300 && mySpin < 360) { alert("Winner is number 1!"); } }); 。因此,空检查可能分为不同的类,并且只在类之间共享所需的属性。

答案 4 :(得分:4)

我的回答几乎和@janki一样,但我想稍微修改一下代码片段,如下所示:

twilio

如果该对象有可能为空,您也可以为sdk添加空检查。

答案 5 :(得分:3)

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

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();

答案 6 :(得分:3)

你说某些方法“可能会返回null”,但不会说它们返回null的情况。你说你抓住了NullPointerException,但你没有说你抓住它的原因。缺乏信息表明您没有清楚地了解哪些例外情况以及为什么它们优于替代方案。

考虑一个用于执行操作的类方法,但该方法不能保证它将执行操作,因为它无法控制的情况(实际上是the case for all methods in Java )。我们称之为该方法并返回。调用该方法的代码需要知道它是否成功。它怎么知道?它如何构建以应对成功或失败这两种可能性?

使用例外,我们可以编写成功作为后置条件的方法。如果方法返回,则表示成功。如果它抛出异常,它就失败了。这是一个清晰的大胜利。我们可以编写清楚处理正常成功案例的代码,并将所有错误处理代码移到catch子句中。经常发现方法不成功的方式或原因的细节对调用者来说并不重要,因此可以使用相同的catch子句来处理几种类型的失败。并且通常情况下,方法不需要在所有中捕获异常,但可以允许它们传播到调用者。程序错误造成的例外情况属于后一类;当有bug时,很少有方法可以做出适当的反应。

那么,那些返回null

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

其中一个答案声称链式方法调用违反了Law of Demeter,因此很糟糕。这种说法是错误的。

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

答案 7 :(得分:2)

NullPointerException是一个运行时异常,所以一般来说不建议捕获它,但要避免它。

您必须在任何想要调用方法的地方捕获异常(或者它将在堆栈中向上传播)。然而,如果在你的情况下,你可以继续使用值为-1的结果,并且你确信它不会传播,因为你没有使用任何&#34;件&#34;这可能是空的,那么我抓住它似乎是正确的

修改

我同意来自@xenteros的后期answer,最好是启动自己的异常而不是返回-1,例如可以将其称为InvalidXMLException

答案 8 :(得分:2)

自昨天以来一直关注此帖。

我一直评论/投票评论说,捕捉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

我在这里看到一个明显的赢家。如果检查比捕获异常要便宜得多。我已经看到了Java-8的做法。考虑到70%的当前应用程序仍在Java-7上运行,我正在添加这个答案。

底线对于任何关键任务应用程序,处理NPE代价高昂。

答案 9 :(得分:2)

不要抓住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; } 。你不知道它来自哪里(我知道你的情况不太可能,但可能其他东西扔了它)而且它很慢。 您想要访问指定的字段,为此,每隔一个字段必须不为空。这是检查每个领域的完美有效理由。我可能会在一个中检查它,然后创建一个可读性的方法。正如其他人指出的那样已经返回-1是非常古老的学校,但我不知道你是否有理由(例如与另一个系统交谈)。

{{1}}

编辑:如果因为WsObject可能只是一个数据结构(检查https://stackoverflow.com/a/26021695/1528880),它会违反Demeter法则,这是值得商榷的。

答案 10 :(得分:2)

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

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

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

     


  2

界面Getter只是一个功能界面,你可以使用任何等价物 GetterResult类,为清楚起见剥离了访问器,保存了getter链的结果,如果有的话,或者调用了最后一个getter的索引。

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

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

我会重构代码,但是如果你能找到自己使用长getter链的自我,你可能会考虑建立一个带有从2到10,吸气等的重载的类。

答案 11 :(得分:2)

正如其他人所说,尊重得墨忒耳法则绝对是解决方案的一部分。另一部分,尽可能改变这些链式方法,使它们无法返回null。您可以避免返回null,而是返回空String,空Collection或其他虚拟对象,这意味着或执行调用者对null执行的操作。< / p>

答案 12 :(得分:2)

我想添加一个专注于错误含义的答案。空例外本身并不能提供任何意义上的完整错误。所以我建议不要直接与他们打交道。

有成千上万的情况你的代码可能出错:无法连接到数据库,IO异常,网络错误......如果你逐个处理它们(比如这里的空检查),那就太多了麻烦。

在代码中:

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

即使你知道哪个字段为空,你也不知道出了什么问题。也许吧是空的,但它是否有望?或者是数据错误?考虑一下阅读代码的人

与xenteros的答案一样,我建议使用自定义未经检查的例外。例如,在这种情况下:Foo可以为null(有效数据),但Bar和Baz永远不应为null(无效数据)

代码可以重写:

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"
    }
}

答案 13 :(得分:1)

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;
    }
}

答案 14 :(得分:1)

你拥有的方法很冗长,但非常易读。如果我是一个新的开发人员来到你的代码库,我可以很快看到你在做什么。大多数其他答案(包括捕捉异常)似乎并没有让事情变得更具可读性,而且有些人认为它的可读性更低。

鉴于您可能无法控制生成的源,并且假设您真的需要访问一些深层嵌套的字段,那么我建议使用方法包装每个深层嵌套的访问。

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服务进行通信时,通常会有一个&#34;远程域&#34;和#34;应用程序域&#34;并在两者之间切换。远程域通常受Web协议的限制(例如,您无法在纯RESTful服务中来回发送辅助方法,并且深层嵌套的对象模型通常可以避免多个API调用)因此不适合直接使用在你的客户中使用。

例如:

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();
    }

}

答案 15 :(得分:1)

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

答案 16 :(得分:1)

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

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

修改

根据评论中的讨论,让我在之前的想法中加入一些内容。在此代码中有两种可能性。一个是你接受null而另一个是,这是一个错误。

如果出现错误并且发生错误,您可以使用其他结构调试代码,以便在断点不足时进行调试。

如果可以接受,则不关心此null出现的位置。如果你这样做,你肯定不应该链接这些请求。

答案 17 :(得分:0)

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

  

我建议您在NULL s中查看if

原因:

  

我们不应该为我们的计划留下一次机会。   NullPointer由系统生成。 系统的行为   生成的异常无法预测。你不应该离开你的   当你已经有办法处理时,程序掌握在System的手中   它由你自己。并将Exception处理机制用于额外的安全性。!!

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

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

编辑:

  

您需要存储这些值wsObject.getFoo(),   wsObject.getFoo().getBar()wsObject.getFoo().getBar().getBaz() in   一些变数。我不是这样做的,因为我不知道回报   该功能的类型。

任何建议都将受到赞赏.. !!

答案 18 :(得分:0)

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

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上发布,但是如果有人发现它有用here(当然没有保证!)

它有点基本,但似乎可以完成这项工作。显然,对于支持安全导航或Optional的更新版本的Java和其他JVM语言,它已经过时了。