Java:避免在嵌套类中检查null(深度空值检查)

70

假设我有一个名为Family的类。它包含一个Person列表。每个Person类都包含一个Address类,而每个Address类都包含一个PostalCode类。任何“中间”类都可能为空。

那么,是否有一种简单的方法可以在不必在每个步骤中检查null的情况下获取PostalCode?也就是说,是否有一种方法可以避免以下嵌套代码?我知道没有“本地”的Java解决方案,但希望如果有人知道库或其他东西。(检查了Commons和Guava,但没找到)

if(family != null) {
    if(family.getPeople() != null) {
        if(family.people.get(0) != null) {
            if(people.get(0).getAddress() != null) {
                if(people.get(0).getAddress().getPostalCode() != null) {
                    //FINALLY MADE IT TO DO SOMETHING!!!
                }
            }
        }
    }
}

不能改变结构,因为来源是我无法控制的服务。

不能使用Groovy及其方便的“Elvis”运算符。

不想等待Java 8 :D

我无法相信我是第一个厌烦编写像这样代码的开发者,但我一直没有找到解决方案。


6
我简直不能相信我是第一个对编写这种代码感到病态和厌烦的开发人员。” 嗯,你并不是。 - user1329572
当然可以。但我不相信你能让代码更美观!抱歉! - Paul Vargas
4
尽管所有答案都告诉你忽略空值检查,直接尝试捕获“NullPointerException”,但不要这样做!虽然你的代码可能看起来很丑陋,但抛出异常是一个昂贵的操作,如果可以避免就要尽量避免。 - Jeffrey
1
此外,如果您在“else”子句中添加了错误消息、备用代码路径等功能,那么它们可以发挥很好的作用。考虑到这些因素,情况并不会看起来那么糟糕。 - mazaneicha
1
如果你能将Brototype移植到Java就好了... https://github.com/letsgetrandy/brototype - jonS90
显示剩余2条评论
13个回答

48

您可以用于:

product.getLatestVersion().getProductData().getTradeItem().getInformationProviderOfTradeItem().getGln();

可选等效:

Optional.ofNullable(product).map(
            Product::getLatestVersion
        ).map(
            ProductVersion::getProductData
        ).map(
            ProductData::getTradeItem
        ).map(
            TradeItemType::getInformationProviderOfTradeItem
        ).map(
            PartyInRoleType::getGln
        ).orElse(null);

需要API 24及以上的版本 :( - sandpat
1
如果你看到了 .map 的实现,它使用 Objects.requireNonNull(mapper); 这个方法会在 mapper 为空时抛出 nullpointerexception 异常。 - Anirudh
2
@Anirudh 在这里是指方法引用,永远不会为 null。由 mapper 返回的值可能为空,在这种情况下,map 将返回 Optional.empty()。 - Andrea Polci
1
这应该是被接受的答案。 - Taher
这可能并没有增加任何好处。传统的 || 和 if-else 有什么问题吗? - visc

22

你的代码的行为与

if(family != null &&
  family.getPeople() != null &&
  family.people.get(0) != null && 
  family.people.get(0).getAddress() != null &&
  family.people.get(0).getAddress().getPostalCode() != null) { 
       //My Code
}

由于短路求值,这种做法也是安全的,因为如果第一个条件不成立,则不会计算第二个条件,如果第二个条件不成立,则不会计算第三个条件......并且您也不会因此获得NPE。


8

如果你使用的是Java8,那么你可以使用以下代码:

resolve(() -> people.get(0).getAddress().getPostalCode());
    .ifPresent(System.out::println);

:
public static <T> Optional<T> resolve(Supplier<T> resolver) {
    try {
        T result = resolver.get();
        return Optional.ofNullable(result);
    }
    catch (NullPointerException e) {
        return Optional.empty();
    }
}

参考: 避免在Java中使用null检查


2
这个解决方案依赖于捕获NPE,这将表现非常糟糕。 - mojoken
我支持这个。有人能解释一下为什么它不好吗?是因为 getters 可能会由于其他 bug 导致内部 NPE,从而被掩盖了吗? - daltonfury42

6
您可以尝试利用条件语句中的快捷规则,但这已经是最接近的方法了。
if(family != null && family.getPeople() != null && family.people.get(0) != null  && family.people.get(0).getAddress() != null && family.people.get(0).getAddress().getPostalCode() != null) {
                    //FINALLY MADE IT TO DO SOMETHING!!!

}

顺便提一下,捕获异常而不是提前测试条件是一个可怕的想法。

你有一些多余的 }(在重构时忘记删除它们了)。 - amit

5
您可以使用Java 8中的Optional类型来消除所有空值检查。
流方法 "map()"接受类型为Function的lambda表达式,并自动将每个函数结果包装成Optional。这使我们能够将多个map操作连接起来。在底层自动处理了空值检查。
Optional.of(new Outer())
  .map(Outer::getNested)
  .map(Nested::getInner)
  .map(Inner::getFoo)
  .ifPresent(System.out::println);

我们还有另一种选项来实现相同的行为,那就是利用供应商函数来解决嵌套路径的问题:
public static <T> Optional<T> resolve(Supplier<T> resolver) {
  try {
      T result = resolver.get();
      return Optional.ofNullable(result);
  }
  catch (NullPointerException e) {
      return Optional.empty();
  }
}

如何调用新方法?请看下面:

Outer obj = new Outer();
obj.setNested(new Nested());
obj.getNested().setInner(new Inner());

resolve(() -> obj.getNested().getInner().getFoo())
    .ifPresent(System.out::println);

4
我个人更喜欢类似于以下的内容:
nullSafeLogic(() -> family.people.get(0).getAddress().getPostalCode(), x -> doSomethingWithX(x))

public static <T, U> void nullSafeLogic(Supplier<T> supplier, Function<T,U> function) {
    try {
        function.apply(supplier.get());
    } catch (NullPointerException n) {
        return null;
    }
}

或类似的东西

nullSafeGetter(() -> family.people.get(0).getAddress().getPostalCode())

public static <T> T nullSafeGetter(Supplier<T> supplier) {
    try {
        return supplier.get();
    } catch (NullPointerException n) {
        return null;
    }
}

最棒的部分是静态方法可以与任何函数一起重复使用 :)


1

你可以使用“null对象”设计模式的某个版本,而不是使用null。例如:

public class Family {
    private final PersonList people;
    public Family(PersonList people) {
        this.people = people;
    }

    public PersonList getPeople() {
        if (people == null) {
            return PersonList.NULL;
        }
        return people;
    }

    public boolean isNull() {
        return false;
    }

    public static Family NULL = new Family(PersonList.NULL) {
        @Override
        public boolean isNull() {
            return true;
        }
    };
}


import java.util.ArrayList;

public class PersonList extends ArrayList<Person> {
    @Override
    public Person get(int index) {
        Person person = null;
        try {
            person = super.get(index);
        } catch (ArrayIndexOutOfBoundsException e) {
            return Person.NULL;
        }
        if (person == null) {
            return Person.NULL;
        } else {
            return person;
        }
    }
    //... more List methods go here ...

    public boolean isNull() {
        return false;
    }

    public static PersonList NULL = new PersonList() {
        @Override
        public boolean isNull() {
            return true;
        }
    };
}

public class Person {
    private Address address;

    public Person(Address address) {
        this.address = address;
    }

    public Address getAddress() {
        if (address == null) {
            return Address.NULL;
        }
        return address;
    }
    public boolean isNull() {
        return false;
    }

    public static Person NULL = new Person(Address.NULL) {
        @Override
        public boolean isNull() {
            return true;
        }
    };
}

etc etc etc

那么你的if语句可以变成:

if (!family.getPeople().get(0).getAddress().getPostalCode.isNull()) {...}

这种方法并不是最优的,因为:

  • 你需要为每个类创建 NULL 对象,
  • 很难将这些对象变成通用的,所以你需要为想要使用的每个 List、Map 等都创建一个 null-object 版本,而且
  • 在子类化和使用哪个 NULL 的问题上可能会出现一些有趣的问题。

但如果你真的讨厌你的 == null,这是一种解决方法。


1
尽管这篇文章已经发布了近五年,但我可能有另一种解决方案来处理“NullPointerException”的老问题。
简而言之:
end: {
   List<People> people = family.getPeople();            if(people == null || people.isEmpty()) break end;
   People person = people.get(0);                       if(person == null) break end;
   Address address = person.getAddress();               if(address == null) break end;
   PostalCode postalCode = address.getPostalCode();     if(postalCode == null) break end;

   System.out.println("Do stuff");
}

由于仍然存在大量的遗留代码,因此使用Java 8和Optional并不总是可行的。

每当涉及到深度嵌套的类(JAXB、SOAP、JSON等)且未应用迪米特法则时,您基本上必须检查所有内容,看看是否有潜在的NPE潜伏。

我的解决方案旨在提高可读性,如果没有至少涉及3个或更多嵌套类,则不应使用该解决方案(当我说嵌套时,我不是指正式上下文中的嵌套类)。由于代码的阅读次数多于编写次数,快速扫视代码左侧部分将使其含义比使用深度嵌套的if-else语句更加清晰。

如果需要else部分,则可以使用此模式:

boolean prematureEnd = true;

end: {
   List<People> people = family.getPeople();            if(people == null || people.isEmpty()) break end;
   People person = people.get(0);                       if(person == null) break end;
   Address address = person.getAddress();               if(address == null) break end;
   PostalCode postalCode = address.getPostalCode();     if(postalCode == null) break end;

   System.out.println("Do stuff");
   prematureEnd = false;
}

if(prematureEnd) {
    System.out.println("The else part");
}

某些集成开发环境会破坏此格式,除非您告诉它们不要这样做(请参见this question)。
您的条件语句必须被反转 - 您告诉代码何时应该中断,而不是何时应该继续。
还有一件事 - 您的代码仍然容易出错。您必须将if(family.getPeople() != null && !family.getPeople().isEmpty())作为代码中的第一行,否则一个空列表将抛出NPE。

0

不是很酷的想法,但是捕获异常怎么样:

    try 
    {
        PostalCode pc = people.get(0).getAddress().getPostalCode();
    }
    catch(NullPointerException ex)
    {
        System.out.println("Gotcha");
    }

我想不出有什么情况可以捕获NullPointerException是可以接受的(也许有一些例外),这是一种不好的做法,因为通常NPE是一个编程错误,正确的程序不应该生成它,当程序员只是捕获NPE时,感觉他正在试图掩盖它。 - Dragos Geornoiu

0

如果很少发生,您可以忽略null检查,并依赖于NullPointerException。 "很少"是由于可能的性能问题(取决于通常会填充堆栈跟踪,这可能很昂贵)。

除此之外,1) 使用特定的帮助程序方法检查null以清理代码或2)使用反射和字符串进行通用处理:

checkNonNull(family, "people[0].address.postalcode")

实现留作练习。


反射也不是很便宜。 - Paul Tomblin
确实,它可能会很慢,@paul。特别是方法查找等。不过实际的方法调用可以相当快,但这又取决于不同情况(其他优化可能对虚拟机更难)。因此,如果需要的话,缓存方法/字段查找通常非常重要,根据我的经验。最重要的是,这当然取决于代码被使用的频率。 - Mattias Isegran Bergander

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接