空检查链与捕获NullPointerException

问题描述 投票:105回答:19

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

还是会被视为反模式?

java exception nullpointerexception null custom-error-handling
19个回答
131
投票

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


2
投票

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


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

2
投票

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

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

编辑:

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


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成本很高。


1
投票

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


1
投票

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

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

编辑:

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

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

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


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

如果您发现自己编写了很多这些方法,或者如果您发现自己想要制作这些公共静态方法,那么我会创建一个单独的对象模型,嵌套您想要的方式,只有您关心的字段,并从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() == null) return -1;
        return foo().getBar().getBaz().getInt();
    }

}

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

0
投票

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

我建议你检查NULLs中的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()。我不是这样做的,因为我不知道该函数的返回类型。

任何建议将不胜感激.. !!


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语言来说,它已经过时了。


11
投票

我建议考虑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解析器的特定异常。


6
投票

假设类结构确实不受我们的控制,就像在这种情况下一样,我认为按照问题中的建议捕捉NPE确实是一个合理的解决方案,除非性能是一个主要问题。一个小的改进可能是包装throw / catch逻辑以避免混乱:

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

现在你可以简单地做:

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

5
投票

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

以下声明违反了Law of Demeter

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

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

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


4
投票

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

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;

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


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

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都处于抽象层次结构的同一级别(对吗?)?

2
投票

不要抓住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),因此它不符合Demeter法则是值得商榷的。


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是一个简单的样板代码片段,可以自动生成(或在需要时手动生成)。 我构造了代码,以便重复块是不言而喻的。


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

我会重构代码,但是如果不能并且你发现自己经常使用长getter链,那么你可以考虑建立一个带有从2到10个getter的重载的类。

© www.soinside.com 2019 - 2024. All rights reserved.