空检查链与捕获NullPointerException

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了空检查链与捕获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;
}

还是会被视为反模式?

答案

捕捉NullPointerException是一件非常棘手的问题,因为它们几乎可以在任何地方发生。很容易从一个bug中获取一个,偶然捕获并继续好像一切正​​常,从而隐藏一个真正的问题。处理它是如此棘手,所以最好完全避免。 (例如,考虑一下null 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);
}

为何选择?

使用Optionals代替null可能缺少的值使得这一事实对读者来说非常明显和清晰,类型系统将确保您不会意外忘记它。

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


缺勤有效还是错误?

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


也许更多的选择?

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

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

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

另一答案

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

另一答案

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

有成千上万的情况你的代码可能出错:无法连接到数据库,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"
    }
}
另一答案

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

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

编辑:

我同意来自@xenteros的后来的answer,它最好是启动你自己的异常而不是返回-1你可以称之为InvalidXMLException

另一答案

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

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

另一答案

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

另一答案

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

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

编辑:

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

如果它是一个错误而且它发生了,当断点不够时,您可以使用其他结构调试代码以进行调试。

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

另一答案

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

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

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

如果您发现自己编写了很多这些方法,或者如果您发现自己想要制作这些公共静态方法,那么我会创建一个单独的对象模型,嵌套您想要的方式,只有您关心的字段,并从W​​eb转换将对象模型服务到对象模型。

当您与远程Web服务进行通信时,通常会有一个“远程域”和“应用程序域”,并在两者之间切换。远程域通常受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(

以上是关于空检查链与捕获NullPointerException的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin空安全 ⑤ ( 异常处理 | 捕获并处理异常 | 抛出自定义异常 )

onPostExecute 中的空指针异常

颤振错误,对空值使用空检查运算符

如何使用 Flutter 检查和检索空的双精度值

在运行时出现此错误“用于空值的空检查运算符”

Activity 在 onActivityResult 方法上变为空