空检查链与捕获 NullPointerException

Null check chain vs catching NullPointerException

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

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

问题是getFoo()getBar()getBaz()可能都是returnnull.

但是,如果我在所有情况下都检查 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是运行时的异常,所以一般来说不建议catch,尽量避免。

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

编辑:

我同意@xenteros 后面的 ,最好启动你自己的异常而不是返回 -1 你可以调用它 InvalidXMLException 例如。

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

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

编辑:

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

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

如果可以接受,你就不用关心这个null出现在什么地方了。如果你这样做,你绝对不应该链接这些请求。

如果效率是个问题,那么应该考虑 'catch' 选项。 如果 '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");

我建议您不要使用特殊的 return 值,例如 -1。这不是 Java 风格。 Java设计了异常机制,避免了这种从C语言传来的老套路

NullPointerException 也不是最好的选择。您可以提供自己的异常(使其 checked 以保证它将由用户处理或 unchecked 以更简单的方式处理它)或者使用您正在使用的 XML 解析器的特定异常。

给出的答案似乎与其他答案不同。

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.!!

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

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.

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

我的回答与@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;

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

不抓NullPointerException。你不知道它是从哪里来的(我知道你的情况不太可能,但 可能 其他东西扔了它)而且它很慢。 您想要访问指定的字段,为此每个其他字段都必须不为空。这是检查每个字段的完美正当理由。我可能会在一个 if 中检查它,然后创建一种可读性方法。正如其他人指出的那样,已经返回 -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;
}

编辑:它是否违反 Demeter 法则值得商榷,因为 WsObject 可能只是一个数据结构(检查 )。

捕捉 NullPointerException 是一件真正有问题的事情,因为它们几乎可以在任何地方发生。很容易从 bug 中得到一个,偶然捕获它并继续,就好像一切正​​常,从而隐藏了一个真正的问题。 处理起来非常棘手,因此最好完全避免。(例如,考虑自动拆箱 null Integer。)

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

使用它你可以像这样编写你的代码:

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

为什么可选?

对于可能不存在的值,使用 Optionals 而不是 null 会使这一事实对读者来说非常明显和清楚,并且类型系统将确保您不会不小心忘记它.

您还可以访问更方便地处理这些值的方法,例如 map and orElse


缺席有效还是错误?

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


也许有更多选择?

如果另一方面,中间方法中缺少的值是有效的,也许您也可以为它们切换到 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 测试可能会更好。

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

以下声明违反了 Law of Demeter

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

你想要的是int,你可以从Foo得到。 Demeter 法则 说,永远不要和陌生人说话。对于您的情况,您可以隐藏 FooBar.

的实际实现

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

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

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

如果不想重构代码,可以使用Java8,可以使用Method references。

先做一个简单的演示(原谅静态内部classes)

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 class,为清楚起见,访问器被剥离,保留 getter 链的结果(如果有的话),或最后调用的 getter 的索引。

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


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

我会重构代码,但如果不能,并且您发现自己经常使用长 getter 链,您可能会考虑构建一个 class,其重载从 2 到,比如说,10,getters.

正如其他人所说,尊重 Demeter 法则绝对是解决方案的一部分。另一部分,只要有可能,就是改变那些链接的方法,使它们不能 return null。您可以避免 returning null,方法是 returning 一个空的 String、一个空的 Collection,或者其他一些表示或执行任何调用者的虚拟对象null.

我想添加一个着重于错误含义的答案。空异常本身不提供任何意义的完整错误。所以我建议避免直接与他们打交道。

你的代码出错的情况有几千种:无法连接数据库,IO异常,网络错误...如果你一个一个处理它们(比如这里的null检查),那就太好麻烦。

代码中:

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

即使知道哪个字段为空,也不知道哪里出了问题。也许 Bar 是空的,但这是预期的吗?还是数据错误?想想阅读你代码的人

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

假设 class 结构确实超出了我们的控制范围,似乎是这种情况,我认为按照问题中的建议捕获 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);

你说有些方法"may return null"但是没有说在什么情况下returnnull。你说你抓住了 NullPointerException 但你没有说你为什么抓住它。这种信息的缺乏表明您不清楚异常的用途以及它们优于替代方案的原因。

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

使用异常,我们可以编写将 成功作为 post 条件 的方法。如果方法returns,则成功。如果它抛出一个异常,它就失败了。对于清晰度而言,这是一个巨大的胜利。我们可以编写清楚地处理正常、成功案例的代码,并将所有错误处理代码移到 catch 子句中。通常情况下,一个方法如何或为什么不成功的细节对调用者来说并不重要,因此相同的 catch 子句可用于处理多种类型的失败。并且经常发生这样的情况,一个方法根本不需要捕获异常,但可以让它们传播给它的调用者。由于程序错误导致的异常在后者class;很少有方法可以在出现错误时做出适当的反应。

所以,那些方法 return null.

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

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

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

从昨天开始就一直在关注这个post。

我被commenting/voting评论说抓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 的成本都很高。

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

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

意味着实例 ENGINE_NAME 将在传递给它的实例上有效地调用 Car?.getEngine()?.getName(),并且 return null 如果有任何引用 returned null:

final String name =  ENGINE_NAME.get(firstCar);

它没有在 Maven 上发布,但如果有人觉得它有用,它就是 here(当然没有保证!)

它有点基础,但似乎可以胜任。显然,对于更新版本的 Java 和其他支持安全导航或 Optional.

的 JVM 语言,它显然已经过时了

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

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

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

如果您发现自己写了很多这样的方法,或者如果您发现自己很想制作这些 public 静态方法,那么我会创建一个单独的对象模型,按照您的喜好嵌套,只包含字段您关心并从 Web 服务对象模型转换为您的对象模型。

当您与远程 Web 服务通信时,通常会有 "remote domain" 和 "application domain" 并在两者之间切换。远程域通常受到 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();
    }

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