122

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

还是会被认为是反模式?

4

19 回答 19

148

捕捉NullPointerException是一件非常有问题的事情,因为它们几乎可以在任何地方发生。很容易从错误中得到一个,偶然发现它并继续,好像一切正​​常,从而隐藏一个真正的问题。处理起来非常棘手,所以最好完全避免。(例如,考虑自动拆箱 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 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可能不存在的值可以使读者清楚地看到这一事实,并且类型系统将确保您不会意外忘记它。

您还可以访问更方便地使用这些值的方法,例如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 测试可能会更好。

于 2016-06-22T08:00:19.263 回答
15

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

于 2016-06-22T07:13:25.137 回答
8

假设类结构确实超出了我们的控制范围,似乎是这样,我认为按照问题中的建议捕获 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);
于 2016-06-23T02:28:56.317 回答
5

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

以下声明违反得墨忒耳法则

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

你想要的是int,你可以从中得到它Foo得墨忒耳法则说,永远不要和陌生人说话。对于您的情况,您可以将实际实现隐藏在Fooand的幕后Bar

现在,您可以创建方法FoointBaz. 最终,Foo我们可以Bar在不直接暴露的情况下访问. 因此,空值检查可能被划分为不同的类,并且类之间只共享必需的属性。BarIntBazFoo

于 2016-06-22T09:05:44.647 回答
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;

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

于 2016-06-22T07:27:01.140 回答
4

您说某些方法“可能返回null”,但没有说明它们在什么情况下返回null。你说你抓到了,NullPointerException但你没有说你为什么抓到它。这种信息的缺乏表明您对例外的用途以及为什么它们优于替代方案没有清晰的理解。

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

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

所以,那些返回null.

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

答案之一声称链式方法调用违反了得墨忒耳法则,因此很糟糕。这种说法是错误的。

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

正如其他人所说,尊重德墨忒耳法则绝对是解决方案的一部分。另一部分,尽可能地改变那些链接的方法,使它们不能返回null。您可以null通过返回一个 empty String、一个 emptyCollection或其他一些虚拟对象来避免返回,这些对象意味着或执行调用者将使用的任何操作null

于 2016-06-22T18:01:30.413 回答
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();
于 2016-06-22T12:09:44.917 回答
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 数定义一个重载。

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

于 2016-06-22T17:30:03.167 回答
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

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

底线对于任何关键任务应用程序,处理 NPE 的成本都很高。

于 2016-06-23T12:08:39.930 回答
2

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

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

在代码中:

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

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

就像在 xenteros 的回答中一样,我建议使用custom unchecked exception。例如,在这种情况下: 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"
    }
}
于 2016-06-23T02:08:14.477 回答
2

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

您必须在要调用该方法的任何地方捕获异常(否则它将向上传播到堆栈)。不过,如果在您的情况下,您可以继续使用值为 -1 的结果,并且您确定它不会传播,因为您没有使用任何可能为空的“片段”,那么在我看来是正确的抓住它

编辑:

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

于 2016-06-22T07:01:44.073 回答
2

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

编辑:如果它违反得墨忒耳法则是有争议的,因为 WsObject 可能只是一个数据结构(检查https://stackoverflow.com/a/26021695/1528880)。

于 2016-06-22T07:58:49.257 回答
1

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

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

编辑:

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

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

如果可以接受,您就不会关心这个 null 出现在哪里。如果你这样做,你绝对不应该链接这些请求。

于 2016-06-22T07:07:21.230 回答
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;
    }
}
于 2016-06-29T07:52:47.190 回答
1

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

于 2016-06-22T07:09:24.710 回答
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 协议的限制(例如,您不能在纯 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();
    }

}
于 2016-06-23T14:47:28.340 回答
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 上发布,但如果有人觉得它有用,它就在这里(当然没有保证!)

这有点基本,但似乎可以完成这项工作。显然,对于支持安全导航或Optional.

于 2016-06-23T13:42:50.653 回答
0

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

我建议您检查NULLin ifs。

原因 :

我们不应该给我们的程序崩溃的机会。NullPointer 由系统生成。系统生成异常的行为无法预测。当您已经有自己的处理方法时,您不应该将程序留在 System 手中。并加入异常处理机制以增加安全性!!

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

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()在一些变量中。我不这样做是因为我不知道该函数的返回类型。

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

于 2016-06-22T07:18:29.237 回答