java 空检查链 vs 捕获 NullPointerException

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/37960674/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 02:58:43  来源:igfitidea点击:

Null check chain vs catching NullPointerException

javaexceptionnullpointerexceptionnullcustom-error-handling

提问by David Frank

A web service returns a huge XML and I need to access deeply nested fields of it. For example:

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

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

The problem is that getFoo(), getBar(), getBaz()may all return null.

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

However, if I check for nullin all cases, the code becomes very verbose and hard to read. Moreover, I may miss the checks for some of the fields.

但是,如果我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();

Is it acceptable to write

可以写吗

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

or would that be considered an antipattern?

或者这会被视为反模式吗?

回答by Lii

Catching NullPointerExceptionis a really problematic thing to dosince they can happen almost anywhere. It's very easy to get one from a bug, catch it by accident and continue as if everything is normal, thus hiding a real problem. It's so tricky to deal with so it's best to avoid altogether.(For example, think about auto-unboxing of a null Integer.)

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

I suggest that you use the Optionalclass instead. This is often the best approach when you want to work with values that are either present or absent.

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

Using that you could write your code like this:

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

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


Why optional?

为什么可选?

Using Optionals instead of nullfor values that might be absent makes that fact very visible and clear to readers, and the type system will make sure you don't accidentally forget about it.

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

You also get access to methods for working with such values more conveniently, like mapand orElse.

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



Is absence valid or error?

缺席是有效的还是错误的?

But also think about if it is a valid result for the intermediate methods to return null or if that is a sign of an error. If it is always an error then it's probably better throw an exception than to return a special value, or for the intermediate methods themselves to throw an exception.

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



Maybe more optionals?

也许更多的选择?

If on the other hand absent values from the intermediate methods are valid, maybe you can switch to Optionals for them also?

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

Then you could use them like this:

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

public Optional<Integer> mo(Ws wsObject) {
    return wsObject.getFoo()
        .flatMap(f -> f.getBar())
        .flatMap(b -> b.getBaz())
        .flatMap(b -> b.getInt());        
}


Why not optional?

为什么不是可选的?

The only reason I can think of for not using Optionalis if this is in a really performance critical part of the code, and if garbage collection overhead turns out to be a problem. This is because a few Optionalobjects are allocated each time the code is executed, and the VM mightnot be able to optimize those away. In that case your original if-tests might be better.

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

回答by Andrew Tobilko

I suggest considering Objects.requireNonNull(T obj, String message). You might build chains with a detailed message for each exception, like

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

requireNonNull(requireNonNull(requireNonNull(
    wsObject, "wsObject is null")
        .getFoo(), "getFoo() is null")
            .getBar(), "getBar() is null");

I would suggest you not to use special return-values, like -1. That's not a Java style. Java has designed the mechanism of exceptions to avoid this old-fashioned way which came from the C language.

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

Throwing NullPointerExceptionis not the best option too. You could provide your own exception (making it checkedto guarantee that it will be handled by a user or uncheckedto process it in an easier way) or use a specific exception from XML parser you are using.

投掷NullPointerException也不是最好的选择。您可以提供自己的异常(进行检查以保证它将由用户处理或取消检查以更简单的方式处理它)或使用来自您正在使用的 XML 解析器的特定异常。

回答by shmosel

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:

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

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

回答by CoderCroc

As already pointed out by Tomin the comment,

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

Following statement disobeys the Law of Demeter,

以下陈述违反了得墨忒耳定律

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

What you want is intand you can get it from Foo. Law of Demetersays, never talk to the strangers. For your case you can hide the actual implementation under the hood of Fooand Bar.

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

Now, you can create method in Footo fetch intfrom Baz. Ultimately, Foowill have Barand in Barwe can access Intwithout exposing Bazdirectly to Foo. So, null checks are probably divided to different classes and only required attributes will be shared among the classes.

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

回答by Raedwald

You say that some methods "may return null" but do not say in what circumstances they return null. You say you catch the NullPointerExceptionbut you do not say why you catch it. This lack of information suggests you do not have a clear understanding of what exceptions are for and why they are superior to the alternative.

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

Consider a class method that is meant to perform an action, but the method can not guaranteeit will perform the action, because of circumstances beyond its control (which is in fact the case for allmethods in Java). We call that method and it returns. The code that calls that method needs to know whether it was successful. How can it know? How can it be structured to cope with the two possibilities, of success or failure?

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

Using exceptions, we can write methods that have success as a post condition. If the method returns, it was successful. If it throws an exception, it had failed. This is a big win for clarity. We can write code that clearly processes the normal, success case, and move all the error handling code into catchclauses. It often transpires that the details of how or why a method was unsuccessful are not important to the caller, so the same catchclause can be used for handling several types of failure. And it often happens that a method does not need to catch exceptions at all, but can just allow them to propagate to itscaller. Exceptions due to program bugs are in that latter class; few methods can react appropriately when there is a bug.

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

So, those methods that return null.

所以,那些返回null.

  • Does a nullvalue indicate a bug in your code? If it does, you should not be catching the exception at all. And your code should not be trying to second guess itself. Just write what is clear and concise on the assumption that it will work. Is a chain of method calls clear and concise? Then just use them.
  • Does a nullvalue indicate invalid input to your program? If it does, a NullPointerExceptionis not an appropriate exception to throw, because conventionally it is reserved for indicating bugs. You probably want to throw a custom exception derived from IllegalArgumentException(if you want an unchecked exception) or IOException(if you want a checked exception). Is your program required to provide detailed syntax error messages when there is invalid input? If so, checking each method for a nullreturn value then throwing an appropriate diagnostic exception is the only thing you can do. If your program need not provide detailed diagnostics, chaining the method calls together, catching any NullPointerExceptionand then throwing your custom exception is clearest and most concise.
  • null值是否表示您的代码中存在错误?如果是这样,您根本不应该捕获异常。并且您的代码不应该试图自行猜测。只要假设它会起作用,就写出清晰简洁的内容。方法调用链是否清晰简洁?然后只需使用它们。
  • null值是否表示对程序的无效输入?如果是,aNullPointerException不是一个合适的异常抛出,因为通常它被保留用于指示错误。您可能想要抛出源自IllegalArgumentException(如果您想要未经检查的异常)或IOException(如果您想要已检查的异常)的自定义异常。当输入无效时,您的程序是否需要提供详细的语法错误消息?如果是这样,检查每个方法的null返回值然后抛出适当的诊断异常是您唯一可以做的事情。如果您的程序不需要提供详细的诊断信息,将方法调用链接在一起,捕获任何一个NullPointerException然后抛出您的自定义异常是最清晰和最简洁的。


One of the answers claims that the chained method calls violate the Law of Demeterand thus are bad. That claim is mistaken.

其中一个答案声称链式方法调用违反了迪米特法则,因此是不好的。这种说法是错误的。

  • When it comes to program design, there are not really any absolute rules about what is good and what is bad. There are only heuristics: rules that are right much (even almost all) of the time. Part of the skill of programming is knowing when it is OK to break those kinds of rules. So a terse assertion that "this is against rule X" is not really an answer at all. Is this one of the situations where the rule shouldbe broken?
  • The Law of Demeteris really a rule about API or class interface design. When designing classes, it is useful to have a hierarchy of abstractions. You have low level classes that uses the language primitives to directly perform operations and represent objects in an abstraction that is higher level than the language primitives. You have medium level classes that delegate to the low level classes, and implement operations and representations at a higher level than the low level classes. You have high level classes that delegate to the medium level classes, and implement still higher level operations and abstractions. (I've talked about just three levels of abstraction here, but more are possible). This allows your code to express itself in terms of appropriate abstractions at each level, thereby hiding complexity. The rationale for the Law of Demeteris that if you have a chain of method calls, that suggests you have a high level class reaching in through a medium level class to deal directly with low level details, and therefore that your medium level class has not provided a medium-level abstract operation that the high level class needs. But it seems that is notthe situation you have here: you did not design the classes in the chain of method calls, they are the result of some auto-generated XML serialization code (right?), and the chain of calls is not descending through an abstraction hierarchy because the des-serialized XML is all at the same level of the abstraction hierarchy (right?)?
  • 在程序设计方面,对于什么是好什么是坏并没有任何绝对的规则。只有启发式规则:大部分(甚至几乎所有)时间都正确的规则。编程技能的一部分是知道什么时候可以打破这些规则。因此,“这违反规则X”的简洁断言根本不是真正的答案。这是应该打破规则的情况之一吗?
  • 得墨忒耳定律确实是一个关于API或类接口的设计规则。在设计类时,有一个抽象层次结构很有用. 您有使用语言原语直接执行操作并在比语言原语更高级别的抽象中表示对象的低级类。您拥有委托给低级类的中级类,并在比低级类更高的级别上实现操作和表示。您拥有委托给中级类的高级类,并实现更高级别的操作和抽象。(我在这里只讨论了三个抽象级别,但更多是可能的)。这允许您的代码根据每个级别的适当抽象来表达自己,从而隐藏复杂性。得墨忒耳定律的基本原理就是如果你有一个方法调用链,那表明你有一个高级类通过一个中级类直接处理低级细节,因此你的中级类没有提供中级抽象操作高级班需要的。但似乎不是你这里的情况:你没有设计方法调用链中的类,它们是一些自动生成的 XML 序列化代码的结果(对吧?),调用链不是降序的通过抽象层次结构,因为反序列化的 XML 都在抽象层次结构的同一级别(对吗?)?

回答by Arka Ghosh

My answer goes almost in the same line as @janki, but I would like to modify the code snippet slightly as below:

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

You can add a null check for wsObjectas well, if there's any chance of that object being null.

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

回答by JimmyB

To improve readability, you may want to use multiple variables, like

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

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

回答by Margaret Bloom

If you don't want to refactor the code and you can use Java 8, it is possible to use Method references.

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

A simple demo first (excuse the static inner 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;
            }
}

Output

输出

241
4

null
2

241
4


2

The interface Getteris just a functional interface, you may use any equivalent.
GetterResultclass, accessors stripped out for clarity, hold the result of the getter chain, if any, or the index of the last getter called.

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

The method getterChainis a simple, boilerplate piece of code, that can be generated automatically (or manually when needed).
I structured the code so that the repeating block is self evident.

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



This is not a perfect solution as you still need to define one overload of getterChainper number of getters.

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

I would refactor the code instead, but if can't and you find your self using long getter chains often you may consider building a class with the overloads that take from 2 to, say, 10, getters.

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

回答by Kevin Krumwiede

As others have said, respecting the Law of Demeter is definitely part of the solution. Another part, wherever possible, is to change those chained methods so they cannot return null. You can avoid returning nullby instead returning an empty String, an empty Collection, or some other dummy object that means or does whatever the caller would do with null.

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

回答by Hoàng Long

I'd like to add an answer which focus on the meaning of the error. Null exception in itself doesn't provide any meaning full error. So I'd advise to avoid dealing with them directly.

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

There is a thousands cases where your code can go wrong: cannot connect to database, IO Exception, Network error... If you deal with them one by one (like the null check here), it would be too much of a hassle.

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

In the code:

在代码中:

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

Even when you know which field is null, you have no idea about what goes wrong. Maybe Bar is null, but is it expected? Or is it a data error? Think about people who read your code

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

Like in xenteros's answer, I'd propose using custom unchecked exception. For example, in this situation: Foo can be null (valid data), but Bar and Baz should never be null (invalid data)

就像在 xenteros 的回答中一样,我建议使用custom unchecked exception。例如,在这种情况下:Foo 可以为 null(有效数据),但 Bar 和 Baz 永远不应为 null(无效数据)

The code can be re-written:

代码可以改写:

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