如何在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个回答

84

有时候,你拥有一些对参数进行对称操作的方法:

a.f(b); <-> b.f(a);

如果您知道b永远不可能为null,那么您可以直接交换它。 这对于equals方法非常有用: 与其使用foo.equals("bar");,不如使用"bar".equals(foo);


2
但是你必须假设 equals (可以是任何方法) 正确处理 null。实际上,所有这些都只是将责任转移到其他人(或另一种方法)。 - Supericy
5
基本上是这样,但 equals 方法(或其他方法)必须检查 null。或者明确说明它不检查。 - Angelo Fuchs

83

与其使用Null对象模式(它有其用处),您可以考虑空对象是一个错误的情况。

当异常被抛出时,检查堆栈跟踪并解决错误。


19
问题在于通常情况下,当出现空指针异常时,并不会明确指出哪个变量为空,而且你可能在该行上有多个"."操作。使用 "if (foo == null) throw new RuntimeException("foo == null")" 可以明确指出是哪个变量有问题,使得堆栈跟踪对于那些需要修复问题的人更有价值。 - Thorbjørn Ravn Andersen
1
使用Andersen - 我希望Java的异常系统能够包含正在处理的变量名称,这样NullPointerException不仅可以指示异常发生的行,还可以指示变量名称。在未混淆的软件中,这应该可以正常工作。 - cthulhu
3
我曾经有一位教授反对方法调用链。他的理论是,应该注意不要超过两个方法的调用链。我不知道这是否是一个严格的规则,但这确实可以避免大多数NPE栈跟踪问题。 - RustyTheBoyRobot
@RustyTheBoyRobot 你可以将每个调用放在自己的一行上。这样,你仍然可以链式调用,但是你知道出了什么问题。 - Haakon Løtveit

81

谷歌集合框架提供了一种优雅的方法来实现空值检查。

一个库类中有一个如下的方法:

static <T> T checkNotNull(T e) {
   if (e == null) {
      throw new NullPointerException();
   }
   return e;
}

使用方法如下(使用import static导入):

...
void foo(int a, Person p) {
   if (checkNotNull(p).getAge() > a) {
      ...
   }
   else {
      ...
   }
}
...

或者以你的示例为例:

checkNotNull(someobject).doCalc();

75
嗯,有什么区别吗?p.getAge()会抛出相同的NPE,但开销较小且堆栈跟踪更清晰。我错过了什么吗? - mysomic
16
在你的例子中,最好抛出一个IllegalArgumentException("e == null")异常,因为它清楚地表明这是程序员预期的异常(同时提供足够的信息让维护者能够识别问题)。NullPointerException应该保留给JVM使用,因为这样清楚地表明这是无意的异常(通常发生在难以识别的地方)。 - Thorbjørn Ravn Andersen
6
现在这已经成为了Google Guava的一部分。 - Steven Benitez
36
在我看来,这似乎是过度工程化。只需让JVM抛出NPE,不要用这些垃圾来弄乱你的代码。 - Alex Worden
6
我喜欢并且会在绝大多数的方法和构造函数中,使用显式检查来验证参数;如果有错误发生,方法总是在最前面几行失败,这样我就可以知道出错的引用,而不必像“getThing().getItsThing().getOtherThing().wowEncapsulationIsBroken().setLol("hi");” 这样去寻找。 - Cory Kendall
显示剩余3条评论

77
Null不是一个“问题”,它是完整建模工具集的一个重要组成部分。软件旨在建模世界的复杂性,而null承载着这个任务。在Java等语言中,null表示“无数据”或“未知”。因此,对于这些目的使用null是适当的。我不喜欢“Null对象”模式;我认为它引发了“谁来守卫守卫者”的问题。如果你问我女朋友的名字是什么,我会告诉你我没有女朋友。在Java语言中,我会返回null。另一种选择是抛出有意义的异常,以指示无法(或不想)在那里解决的某些问题,并将其委托给堆栈中更高的位置重试或向用户报告数据访问错误。
  1. For an 'unknown question' give 'unknown answer'. (Be null-safe where this is correct from business point of view) Checking arguments for null once inside a method before usage relieves multiple callers from checking them before a call.

    public Photo getPhotoOfThePerson(Person person) {
        if (person == null)
            return null;
        // Grabbing some resources or intensive calculation
        // using person object anyhow.
    }
    

    Previous leads to normal logic flow to get no photo of a non-existent girlfriend from my photo library.

    getPhotoOfThePerson(me.getGirlfriend())
    

    And it fits with new coming Java API (looking forward)

    getPhotoByName(me.getGirlfriend()?.getName())
    

    While it is rather 'normal business flow' not to find photo stored into the DB for some person, I used to use pairs like below for some other cases

    public static MyEnum parseMyEnum(String value); // throws IllegalArgumentException
    public static MyEnum parseMyEnumOrNull(String value);
    

    And don't loathe to type <alt> + <shift> + <j> (generate javadoc in Eclipse) and write three additional words for you public API. This will be more than enough for all but those who don't read documentation.

    /**
     * @return photo or null
     */
    

    or

    /**
     * @return photo, never null
     */
    
  2. This is rather theoretical case and in most cases you should prefer java null safe API (in case it will be released in another 10 years), but NullPointerException is subclass of an Exception. Thus it is a form of Throwable that indicates conditions that a reasonable application might want to catch (javadoc)! To use the first most advantage of exceptions and separate error-handling code from 'regular' code (according to creators of Java) it is appropriate, as for me, to catch NullPointerException.

    public Photo getGirlfriendPhoto() {
        try {
            return appContext.getPhotoDataSource().getPhotoByName(me.getGirlfriend().getName());
        } catch (NullPointerException e) {
            return null;
        }
    }
    

    Questions could arise:

    Q. What if getPhotoDataSource() returns null?
    A. It is up to business logic. If I fail to find a photo album I'll show you no photos. What if appContext is not initialized? This method's business logic puts up with this. If the same logic should be more strict then throwing an exception it is part of the business logic and explicit check for null should be used (case 3). The new Java Null-safe API fits better here to specify selectively what implies and what does not imply to be initialized to be fail-fast in case of programmer errors.

    Q. Redundant code could be executed and unnecessary resources could be grabbed.
    A. It could take place if getPhotoByName() would try to open a database connection, create PreparedStatement and use the person name as an SQL parameter at last. The approach for an unknown question gives an unknown answer (case 1) works here. Before grabbing resources the method should check parameters and return 'unknown' result if needed.

    Q. This approach has a performance penalty due to the try closure opening.
    A. Software should be easy to understand and modify firstly. Only after this, one could think about performance, and only if needed! and where needed! (source), and many others).

    PS. This approach will be as reasonable to use as the separate error-handling code from "regular" code principle is reasonable to use in some place. Consider the next example:

    public SomeValue calculateSomeValueUsingSophisticatedLogic(Predicate predicate) {
        try {
            Result1 result1 = performSomeCalculation(predicate);
            Result2 result2 = performSomeOtherCalculation(result1.getSomeProperty());
            Result3 result3 = performThirdCalculation(result2.getSomeProperty());
            Result4 result4 = performLastCalculation(result3.getSomeProperty());
            return result4.getSomeProperty();
        } catch (NullPointerException e) {
            return null;
        }
    }
    
    public SomeValue calculateSomeValueUsingSophisticatedLogic(Predicate predicate) {
        SomeValue result = null;
        if (predicate != null) {
            Result1 result1 = performSomeCalculation(predicate);
            if (result1 != null && result1.getSomeProperty() != null) {
                Result2 result2 = performSomeOtherCalculation(result1.getSomeProperty());
                if (result2 != null && result2.getSomeProperty() != null) {
                    Result3 result3 = performThirdCalculation(result2.getSomeProperty());
                    if (result3 != null && result3.getSomeProperty() != null) {
                        Result4 result4 = performLastCalculation(result3.getSomeProperty());
                        if (result4 != null) {
                            result = result4.getSomeProperty();
                        }
                    }
                }
            }
        }
        return result;
    }
    

    PPS. For those fast to downvote (and not so fast to read documentation) I would like to say that I've never caught a null-pointer exception (NPE) in my life. But this possibility was intentionally designed by the Java creators because NPE is a subclass of Exception. We have a precedent in Java history when ThreadDeath is an Error not because it is actually an application error, but solely because it was not intended to be caught! How much NPE fits to be an Error than ThreadDeath! But it is not.

  3. Check for 'No data' only if business logic implies it.

    public void updatePersonPhoneNumber(Long personId, String phoneNumber) {
        if (personId == null)
            return;
        DataSource dataSource = appContext.getStuffDataSource();
        Person person = dataSource.getPersonById(personId);
        if (person != null) {
            person.setPhoneNumber(phoneNumber);
            dataSource.updatePerson(person);
        } else {
            Person = new Person(personId);
            person.setPhoneNumber(phoneNumber);
            dataSource.insertPerson(person);
        }
    }
    

    and

    public void updatePersonPhoneNumber(Long personId, String phoneNumber) {
        if (personId == null)
            return;
        DataSource dataSource = appContext.getStuffDataSource();
        Person person = dataSource.getPersonById(personId);
        if (person == null)
            throw new SomeReasonableUserException("What are you thinking about ???");
        person.setPhoneNumber(phoneNumber);
        dataSource.updatePerson(person);
    }
    

    If appContext or dataSource is not initialized unhandled runtime NullPointerException will kill current thread and will be processed by Thread.defaultUncaughtExceptionHandler (for you to define and use your favorite logger or other notification mechanizm). If not set, ThreadGroup#uncaughtException will print stacktrace to system err. One should monitor application error log and open Jira issue for each unhandled exception which in fact is application error. Programmer should fix bug somewhere in initialization stuff.


6
捕获NullPointerException并返回null会让调试工作变得更加困难。最终你仍然会遇到NPE,而且很难弄清楚最初是哪个变量为null。 - artbristol
25
如果我的声望足够高,我会将这个帖子点踩。null不仅是不必要的,它还会在类型系统中留下漏洞。将Tree分配给List是一种类型错误,因为Tree不是List类型的值;按照同样的逻辑,将null分配也应该是一种类型错误,因为null既不是Object类型的值,也不是任何有用的类型。即使是发明null的人 Tony Hoare 也认为这是他的“亿万美元的错误”。"可能是T类型值或者没有值"的概念是自己的类型,并且应该被表示为这样的类型(例如Maybe<T>或Optional<T>)。 - Doval
2
就“Maybe<T>或Optional<T>”而言,您仍然需要编写类似于if (maybeNull.hasValue()) {...}的代码,那么与if (maybeNull != null)) {...}有什么区别呢? - Mike
1
就“捕获NullPointerException并返回null非常难以调试。最终你还是会得到NPE,并且真的很难弄清楚最初是什么为空。”我完全同意!在这些情况下,您应该编写一打“if”语句或者如果业务逻辑指示数据就地存在,则抛出NPE,或者从新Java中使用空安全运算符。但是在某些情况下,我不关心哪个确切的步骤给我null。例如,在屏幕上显示用户之前计算一些值时,可能会缺少期望的数据。 - Mike
3
Maybe<T>Optional<T>的好处不在于T可能为空的情况,而是在于它永远不应该为空的情况。如果你有一个类型明确表示“这个值可能为空——谨慎使用”,并且你一直使用和返回这样的类型,那么无论何时在你的代码中看到一个普通的T,就可以假设它永远不为null。(当然,如果编译器可以强制执行,这将更加有用。) - cHao
显示剩余13条评论

72
Java 7有一个新的java.util.Objects实用类,其中有一个requireNonNull()方法。这个方法只是在其参数为null时抛出NullPointerException,但它可以使代码更加简洁。例子:
Objects.requireNonNull(someObject);
someObject.doCalc();

这种方法最适合在构造函数中的赋值之前检查,每次使用它可以节省三行代码:
Parent(Child child) {
   if (child == null) {
      throw new NullPointerException("child");
   }
   this.child = child;
}

变成

Parent(Child child) {
   this.child = Objects.requireNonNull(child, "child");
}

9
实际上,你的例子存在代码膨胀:第一行是多余的,因为在第二行会抛出NPE异常。;-) - user1050755
1
真的。更好的例子是如果第二行是 doCalc(someObject) - Stuart Marks
如果您不是doCalc()的作者,并且它在给定null时没有立即抛出NPE,那么您需要检查null并自己抛出NPE。这就是Objects.requireNonNull()的作用。 - Stuart Marks
7
不仅仅是代码膨胀。最好在方法执行一半之前检查,以免引起副作用或使用过多时间/空间。 - Rob Grant
Objects.requireNonNull(object, string) 版本(其中您可以将错误消息作为第二个参数传递)比单参数版本更有用(后者可能只是为了完整性而添加),特别是当您使用它来检查多个参数值时。假设您必须检查 3 个参数是否为空,这将使您的代码从 3 个 if/then-throw 块(由空行分隔)减少到 3 个不需要空行的单行。 - Cornel Masson
显示剩余2条评论

53

最终,彻底解决这个问题的唯一方法是使用不同的编程语言:

  • 在Objective-C中,您可以相当于在 nil 上调用一个方法,而绝对什么都不会发生。这使得大多数空值检查是不必要的,但也会使错误的诊断变得更加困难。
  • Nice(一种基于Java的语言)中,每种类型都有两个版本:可能为空的版本和非空的版本。只能在非空类型上调用方法。可以通过显式检查 null 来将可能为空的类型转换为非空类型。这样可以更容易地知道何时需要进行空值检查以及何时不需要。

5
我不熟悉尼斯,但Kotlin实现了相同的思想,在语言类型系统中具有可为空和非空类型。这比Optionals或null对象模式更简洁。 - mtsahakis

40

在 Java 中这确实是一个普遍的问题。

首先,我对此的想法是:

当 NULL 不是一个有效值时,"吃掉"传入的 NULL 是不好的。如果你没有使用某种错误退出方法,那么就意味着你的方法中没有出现任何问题,但事实并非如此。然后你可能会返回 null,在接收方法中再次检查 null,这个过程永无止境,最终会得到 "if != null" 等等代码...

因此,我认为 null 必须是一个关键的错误,它会阻止进一步的执行(也就是说,null 不是一个有效值的情况下)。

我解决这个问题的方式是:

首先,我遵循以下约定:

  1. 所有公共方法/API 都要检查其参数是否为 null
  2. 所有私有方法都不检查 null,因为它们是受控制的方法(如果上面没有处理,就让它们因 nullpointer 异常而崩溃)
  3. 唯一不检查 null 的其他方法是实用方法。它们是公共的,但如果你因某些原因调用它们,你知道传递了什么参数。这就像试图在没有提供水的情况下在壶里煮水...

最后,在代码中,公共方法的第一行应该像这样:

ValidationUtils.getNullValidator().addParam(plans, "plans").addParam(persons, "persons").validate();
请注意,addParam()返回自身,这样您就可以添加更多要检查的参数。
如果任何参数为null,则方法validate()会抛出已检查的ValidationException(已检查或未检查是设计/品味问题,但我的ValidationException是已检查的)。
void validate() throws ValidationException;
如果例如"plans"为空,那么该消息将包含以下文本:
"参数[plans]遇到非法的参数值null"
正如您所看到的,addParam()方法中的第二个值(字符串)是用户消息所必需的,因为即使使用反射也无法轻松检测传入变量的名称(不是本文的主题)。
是的,我们知道在此行之后我们将不再遇到空值,因此我们只需安全地调用这些对象的方法。
这种方式使代码清晰、易于维护和阅读。

3
当然。只是直接抛出错误并导致崩溃的应用程序更加高质量,因为当它们不能正常工作时就没有疑问。吞下错误的应用程序可能会以最好的方式优雅地降级,但通常以难以注意到且无法修复的方式运行。一旦问题被发现,调试起来就更加困难。 - Paul Jackson

39

提出这个问题表明您可能对错误处理策略感兴趣。如何和在哪里处理错误是一个普遍的架构问题。有几种方法可以解决这个问题。

我最喜欢的方法是允许异常传递 - 在“主循环”或其他具有适当职责的函数中捕获它们。检查错误条件并适当地处理它们可以被视为一种专业化的职责。

确实也要看看面向方面的编程(AOP) - 它们有巧妙的方法将if( o == null ) handleNull()插入到您的字节码中。


38

除了使用 assert,您还可以使用以下内容:

if (someobject == null) {
    // Handle null here then move on.
}

这略微比以下内容更好:

if (someobject != null) {
    .....
    .....



    .....
}

2
嗯,为什么呢?请不要有任何防御心理,我只是想更多地了解Java :) - Matthias Meid
9
作为一般的原则,我更喜欢if语句中的表达式是一个更“积极”的陈述,而不是一个“否定”的陈述。所以,如果我看到if (!something) { x(); } else { y(); },我会倾向于将其重构为if (something) { y(); } else { x(); }(尽管有人可能会认为!= null是更积极的选项...)。但更重要的是,代码的重要部分没有被包裹在{}中,对于大多数方法来说,你可以少一层缩进。我不知道这是否是fastcodejava的想法,但这是我的想法。 - Tyler
2
这也是我倾向于做的事情...在我看来,这可以保持代码的整洁。 - Koray Tugay
@MatrixFrog 是的,这样可以使代码更清晰易读,因为你首先看到重要的代码(当所有函数都没有错误时)。 - Skaldebane

37

永远不要使用null。不要允许它存在。

在我的课程中,大多数字段和局部变量都具有非null的默认值,并且我在代码中添加契约语句(始终开启断言),以确保这一点得到执行(因为它比让其成为NPE并解决行号等更简洁、更表达)。

一旦我采用了这种做法,我注意到问题似乎自己解决了。你会意外地更早地捕获到开发过程中的问题,并意识到你有一个弱点……更重要的是,它有助于封装不同模块的关注点,不同模块可以“信任”彼此,而且不再用if = null else构造来污染代码!

这就是防御性编程,在长期运行中会产生更干净的代码。始终对数据进行清理,例如通过强制执行严格标准,在这里问题就会消失。

class C {
    private final MyType mustBeSet;
    public C(MyType mything) {
       mustBeSet=Contract.notNull(mything);
    }
   private String name = "<unknown>";
   public void setName(String s) {
      name = Contract.notNull(s);
   }
}


class Contract {
    public static <T> T notNull(T t) { if (t == null) { throw new ContractException("argument must be non-null"); return t; }
}

合同就像迷你单元测试,即使在生产环境中也一直运行,当事情失败时,你知道原因,而不是一个你不得不想办法解决的随机NPE。


为什么会被踩?根据我的经验,这种方法比其他方法要好得多,我很想知道为什么不行。 - ianpojman
我同意,这种方法可以避免与 null 相关的问题,而不是通过在代码中到处添加 null 检查来修复它们。 - ChrisBlom
9
这种方法存在的问题在于,如果名称从未设置,它将具有值“<unknown>”,这与设置了一个值类似。现在假设我需要检查名称是否从未设置(即为未知),我必须执行与特殊值“<unknown>”的字符串比较。 - Steve Kuo
1
非常好的观点,史蒂夫。我经常做的是将该值作为常量,例如public static final String UNSET="__unset" ... private String field = UNSET ... 然后private boolean isSet() { return UNSET.equals(field); } - ianpojman
在我看来,这是使用自己实现的Optional(合同)实现Null Object模式的一种方法。它在持久化类中的行为如何?我认为在那种情况下不适用。 - TomasMolina
@Kurapika - 我同意。我认为在Java中“永远不要使用null”并不完全实用。但99%的情况下,它是实用的,并且应该得到鼓励。我喜欢像Kotlin这样解决这个问题的语言。 - ianpojman

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