如何在Java中避免检查null值?

4393

我使用 x != null 来避免 NullPointerException。有其他的替代方法吗?

if (x != null) {
    // ...
}

142
鼓励使用空值会使代码更难理解,也更不可靠。 - Tom Hawtin - tackline
83
不使用null比这里大多数其他建议都更好。抛出异常,不要返回或允许null。顺便说一下,“assert”关键字是无用的,因为它默认情况下是禁用的。使用一个始终启用的失败机制。 - ianpojman
4
在高级代码中应避免使用null值。发明了null引用的Tony Hoare称其为“价值数十亿美元的错误”。在此处查看一些想法:http://www.softwaregeek.net/2016/04/how-to-avoid-null-checks-in-java.html。 - Andrii Polunin
3
似乎是Java 8中的静态方法:static Objects.isNull(Object o)。可在以下链接中找到具体文档:https://docs.oracle.com/javase/8/docs/api/java/util/Objects.html - Robert R Evans
7
null最常见的误用是人们返回null而不是空集合。每次这样做都会让我发疯。停止使用null,你将生活在一个更美好的世界中。此外,通过使用"final"关键字来扩展你的代码,你将生活在一个更美好的世界中。 - Jack
显示剩余8条评论
68个回答

6

无论何时传递数组或向量,请将它们初始化为空,而不是为null。这样你就可以避免大量检查null并且一切都很好 :)

public class NonNullThing {

   Vector vectorField = new Vector();

   int[] arrayField = new int[0];

   public NonNullThing() {

      // etc

   }

}

5
在Java 8中,如果本地变量/字段/方法参数/方法返回类型从未被赋值为null(且不检查null),则可以使用类型T。如果它可能为null,则可以使用类型Optional<T>。然后,使用方法map来处理T ->,使用方法flatMap来处理T -> Optional<R>
class SomeService {
    @Inject
    private CompanyDao companyDao;

    // return Optional<String>
    public Optional<String> selectCeoCityByCompanyId0(int companyId) {
        return companyDao.selectById(companyId)
                .map(Company::getCeo)
                .flatMap(Person::getHomeAddress)
                .flatMap(Address::getCity);
    }

    // return String + default value
    public String selectCeoCityByCompanyId1(int companyId) {
        return companyDao.selectById(companyId)
                .map(Company::getCeo)
                .flatMap(Person::getHomeAddress)
                .flatMap(Address::getCity)
                .orElse("UNKNOWN");
    }

    // return String + exception
    public String selectCeoCityByCompanyId2(int companyId) throws NoSuchElementException {
        return companyDao.selectById(companyId)
                .map(Company::getCeo)
                .flatMap(Person::getHomeAddress)
                .flatMap(Address::getCity)
                .orElseThrow(NoSuchElementException::new);
    }
}

interface CompanyDao {
    // real situation: no company for such id -> use Optional<Company> 
    Optional<Company> selectById(int id);
}

class Company {
    // company always has ceo -> use Person 
    Person ceo;
    public Person getCeo() {return ceo;}
}

class Person {
    // person always has name -> use String
    String firstName;
    // person can be without address -> use Optional<Address>
    Optional<Address> homeAddress = Optional.empty();

    public String getFirstName() {return firstName;}   
    public Optional<Address> getHomeAddress() {return homeAddress;}
}

class Address {
    //  address always contains country -> use String
    String country;
    //  city field is optional -> use Optional<String>
    Optional<String> city = Optional.empty();

    String getCountry() {return country;}    
    Optional<String> getCity() {return city;}
}

4

解决“!= null”检查的另一种方法是(如果从设计上无法摆脱):

Optional.ofNullable(someobject).ifPresent(someobject -> someobject.doCalc());

或者

Optional.ofNullable(someobject).ifPresent(SomeClass::doCalc);

假设SomeClass是someobject的类型。

然而,你无法从doCalc()中获得返回值,因此仅适用于void方法。


你可以使用 map 结合 get 或者其他获取 Optional 值的方法来获取值,而不是使用 ifPresent - ThisIsNoZaku

4
您可以在方法调用之前使用拦截器。这正是面向切面编程所专注的内容。
假设M1(Object test)是一个方法,M2是一个我们在方法调用前应用切面的方法,M2(Object test2)。如果test2 != null,则调用M1,否则执行其他操作。它适用于所有您想要应用切面的方法。如果您想为实例字段和构造函数应用切面,则可以使用AspectJ。对于方法方面,Spring也可能是最佳选择。

你是在建议拦截整个应用程序的每个方法吗? - TomasMolina

3

在Java 8中,你可以像下面这样将供应商传递给一个辅助方法:

if(CommonUtil.resolve(()-> a.b().c()).isPresent()) {

}

上面的代码替换了以下的样板代码,
if(a!=null && a.b()!=null && a.b().c()!=null) {

}

//CommonUtil.java

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

乍一看,这似乎是解决我遇到的问题的好方法,但它存在“不要使用异常来控制行为”的设计缺陷。基本上,它只能工作是因为它依赖于“resolve(..)”方法有意生成NPE,捕获它,然后处理它以改变行为。那很糟糕。可能仍然会使用它,但正在寻找更好的方法。 - Jeff Bennett

3

3

您也可以使用Checker Framework(适用于JDK 7及以上版本)静态检查空值。这可能会解决很多问题,但需要运行一个额外的工具,目前只能在OpenJDK上使用。

https://checkerframework.org/

3

好的,我知道这个问题已经被技术上回答了无数次,但我必须说一下,因为这是与Java程序员讨论不休的问题。

抱歉,但我几乎不同意以上所有观点。我们在Java中必须测试null的原因是因为大多数Java程序员不知道如何处理内存。

我之所以这么说是因为我在C++中有很长时间的编程经验,我们不这样做。换句话说,你不需要这样做。请注意,在Java中,如果你遇到悬空指针,你会得到一个普通的异常;而在C++中,这个异常通常不会被捕获并终止程序。

不想这样做?那么就按照C / C ++的一些简单规则进行操作。

不要轻易实例化东西,考虑每个“new”可能会给你带来很多麻烦,并遵循这些简单的规则。

一个类只能以3种方式访问内存 ->

  1. 它可以“拥有”类成员,它们将遵循以下规则:

    1. 所有“拥有”成员都在构造函数中“new”创建。
    2. 您将在析构函数或Java中的等效close()函数中关闭/释放分配给该类的所有资源,而不是在其他任何地方。

这意味着您需要像Java一样记住谁是每个资源的所有者或父级,并尊重该所有权。只有创建它的类才能删除对象。 另外 ->

  1. 某些成员将被“使用”但不拥有或“拥有”。这些是由另一个类“拥有”的,并作为参数传递给构造函数。由于这些属于另一个类,因此我们永远不会删除或关闭它们,只有父项可以这样做。

  2. 类中的方法还可以实例化用于内部使用的本地对象,这些对象永远不会超出类的范围,否则它们应该是正常的“拥有”对象。

最后,为了使所有这些工作,您需要具有分层形式的有纪律的设计,并且不进行循环。

在这种设计下,并遵循上述规则,分层设计中的子类将永远不会访问已销毁的指针,因为这意味着父类在子类之前被销毁,而分层非循环设计将不允许这种情况发生。

最后,还要记住,在启动系统时,您应该从层次结构的顶部向下构建,并从底部向上销毁。你永远不会在任何地方遇到null指针,否则有人正在违反规则。


1
同意,尽管只是在概念上。是的,一个人应该有一个计划。但是你需要会制定计划、具备逻辑思维能力以及开发和维护一致模式的程序员。然后还有那些主要关注金钱和工资的雇主 ;-)。然后开始查看JPA规范,了解什么构成一致的模式/计划以及开发和/或记录它所需的内容。 - user1050755
3
这个答案适用于C++,但不适用于像Java这样的垃圾回收语言。 - artbristol
我非常尊重地不同意。在JAVA(或几乎任何其他语言)中良好的内存处理将带给您许多好处。从更好的稳定性,因为空指针减少了,到更好的性能,因为内存占用更低,再到更好的设计,因为关系得到更好的控制。我建议,许多JAVA应用程序的松散内存管理导致频繁的NPE和内存泄漏。 - Alex Vaz
我非常赞同你所写的内容,并认为Java没有区分封装所有权和不封装所有权的引用是非常不幸的。那些会争辩“这就是GC的作用”的人错过了一个关键点:GC避免了不可变对象需要拥有者的需求,它也消除了指向已删除对象的悬空引用变成指向其他任意对象的可能性(这在C++中可能发生)。然而,如果多个对象将可变对象的状态作为自己的一部分进行封装,编写正确的代码就很困难。 - supercat
我个人偏向于这样思考:引用可以封装标识、可变状态、两者都有或者都没有。此外,引用之所以可以封装可变状态,是因为它们要么标识一个不可变对象,要么标识的对象永远不会暴露给可能会改变它的任何东西。很遗憾,Java 没有这种区分的概念,如果有的话,像相等性检查和克隆之类的事情就可以自动处理99%了。 - supercat
显示剩余4条评论

3

有一种好的方法可以从JDK中检查空值。 它是Optional.java,有许多方法来解决这些问题。比如下面:

    /**
     * Returns an {@code Optional} describing the specified value, if non-null,
     * otherwise returns an empty {@code Optional}.
     *
     * @param <T> the class of the value
     * @param value the possibly-null value to describe
     * @return an {@code Optional} with a present value if the specified value
     * is non-null, otherwise an empty {@code Optional}
     */
    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }

    /**
     * Return {@code true} if there is a value present, otherwise {@code false}.
     *
     * @return {@code true} if there is a value present, otherwise {@code false}
     */
    public boolean isPresent() {
        return value != null;
    }

    /**
     * If a value is present, invoke the specified consumer with the value,
     * otherwise do nothing.
     *
     * @param consumer block to be executed if a value is present
     * @throws NullPointerException if value is present and {@code consumer} is
     * null
     */
    public void ifPresent(Consumer<? super T> consumer) {
        if (value != null)
            consumer.accept(value);
    }

这对于帮助Javer非常有用。


3
public class Null {

public static void main(String[] args) {
    String str1 = null;
    String str2 = "";

    if(isNullOrEmpty(str1))
        System.out.println("First string is null or empty.");
    else
        System.out.println("First string is not null or empty.");

    if(isNullOrEmpty(str2))
        System.out.println("Second string is null or empty.");
    else
        System.out.println("Second string is not null or empty.");
}

public static boolean isNullOrEmpty(String str) {
    if(str != null && !str.isEmpty())
        return false;
    return true;
}
}

输出

str1 is null or empty.
str2 is null or empty.

在上述程序中,我们有两个字符串 str1 和 str2。str1 包含 null 值,而 str2 是一个空字符串。
我们还创建了一个名为 isNullOrEmpty() 的函数,它检查字符串是否为 null 或为空,正如其名称所示。它使用 !=null 进行空检查,并使用 string 的 isEmpty() 方法进行检查。
简单地说,如果一个字符串不是 null 并且 isEmpty() 返回 false,那么它既不是 null 也不是空。否则就是。
然而,上面的程序如果一个字符串只包含空格字符(空格),它并不会返回空。从技术上讲,isEmpty() 看到它包含空格并返回 false。对于带有空格的字符串,我们使用字符串方法 trim() 来修剪掉所有前导和尾随的空格字符。

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