null是个什么玩意儿!

最近由于帮公司修改NPE的问题,接触到null比较多,这货也是经常见了,于是就想详细了解一下这是个啥玩意儿。
null在很多语言里都有,但是在java中它具有以下特性:

  1. 它不是任何类型,所以使用instance of关键字去判断任意null的引用或者null其本尊的时候,返回结果总是false
  2. 它是很多未赋值的对象引用的默认值
  3. 可以简单的理解为null就是JAVA中的一个关键字而已。

Null的恶

引用null之父C. A. R. Hoare的原话就是:

I call it my billion-dollar mistake.

为什么说是billion-dollar mistake?举个栗子:

JDK的HashMap的get方法文档中有这么一段。

V get(Object key)

Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key.

If this map permits null values, then a return value of null does not necessarily indicate that the map contains no mapping for the key; it’s also possible that the map explicitly maps the key to null. The containsKey operation may be used to distinguish these two cases.

简单的说,就是一般情况下,如果你调用get方法,返回值是null,代表这个map中没有你想要的key值。但是如果你往map里面put了一个null,这时候再调用get方法,返回值是null就不一定代表map中不存在对应的key了。这种情况下如何判断key是否在map中呢?只有使用containsKey方法解决。

栗子二:
由于允许null存在,在工程中为了避免自己使用到Null的引用,我们经常使用这样的代码:

1
2
3
4
5
if(obj == null){
do something
}else{
do something else
}

先不说不断的使用这样的判断代码长的丑的问题,忙的时候一个疏忽,没有判断null,那工程运行起来代码就很有可能跑NPE异常从而造成程序崩溃等问题。

善良人类的解决方案

解决方案一: Null Object Pattern

有人说了,既然null这么可恶,我规定,编码的时候所有的代码,返回值不能返回null。于是就有了Null Object Pattern
代码写出来长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Animal {
public void makeSound();
}

public class Dog implements Animal {
public void makeSound() {
System.out.println("woof!");
}
}

public class NullAnimal implements Animal {
public void makeSound() {

}
}

总结来说,实现一个类并赋予一些初始值,以此类代替null,当Animal类型的对象无值可赋的时候,将NullAnimal赋给它,而不复制null。

1
2
3
4
5
6
public class AnimalTest{
public static void main(){
Animal nullAnimal = new NullAnimal(); //use this to init a reference
Animal nullValueAnimal = null; //don't use this as default reference value
}
}

个人觉得这是一个不错的解决办法。但是,对于一个大的工程来说,肯定会产生很多类似NullAnimal的这种类,维护这种类本身有代价,而且,对于有些业务逻辑来说,本身没有一个初始值,也就是说这种方式不适用于所有的业务逻辑。

解决方案二: Annotation

Intellij带的注解方式:org.jetbrains.annotations.NotNullorg.jetbrains.annotations.Nullable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Created by huanyu on 16/1/24.
*/

public class Test {
@NotNull
public static String helloWorldNotNull() {
return "Hello World";
}
@Nullable public static String helloWorldNullable() {
return "Hello World";
}

public String helloWorldArg(@NotNull String str){
return str;
}
public static void main(String[] args)
{

String result = helloWorldNotNull();
if(result != null) {
System.out.println(result);
}
Test test = new Test();
test.helloWorldArg(null);
}
}

在编写程序时,直接在不允许null的参数或者返回值前加@NotNull注解,在允许null的地方加@Nullable注解。
这样做的好处有二:

  1. 编码过程中,出现不符合@Nullable@NotNull规则的代码时,编译器会有警告提示。
    例如: 上述21行代码中的result的引用已经加过@NotNull注解。对其再次进行非空判断时,如22行result != null。编译器会提示:Condition: 'result != null' is always 'true'
    第26行会提示: Pass 'null' argument to parameter annotted as @NotNull
  2. 运行时,程序会检查每个引用的使用,不符合@NotNull@Nullable规则的,则抛异常。
    例如: 如果执意要像第26行那样,向一个加了@NotNull的参数传递null时,在运行时会抛异常:

    Exception in thread “main” java.lang.IllegalArgumentException: Argument for @NotNull parameter ‘str’ of Test.helloWorldArg must not be null
    at Test.helloWorldArg(Test.java)
    at Test.main(Test.java:26)

这种方法优点:

  1. 直接在编码阶段指定那些引用可是是null哪些不行,代码的可读性更强,比上一种方法更方便灵活。
  2. 在编译层次和运行时层次做双重检查,能有效避免不适当的null的传播。
    缺点:
    必须引用相应的jar包,就算用Intellij开发也需要。
    Maven中,引用方式如下:
    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.intellij</groupId>
    <artifactId>annotations</artifactId>
    <version>9.0.4</version>
    </dependency>

解决方案三: 静态代码扫描

在研究Intellij的注解的使用的过程中,我还发现了很多@NotNull@Nullable@NonNull等注解,也稍微做了了解。
发现其中,还可以用findbugs这种工具,通过静态代码扫描的方式来发现代码中不适当的null的问题。由于觉得静态代码扫描有点亡羊补牢的感觉,所以没有再进行深入研究了。
这里只将一些不错的资料,梳理后贴在这里。
which-notnull-java-annotation-should-i-use
nullable-null-notnull-notnull-nonnull
atnullable

以下是本文一些参考:
What is null?
Null Object Pattern
9 things about null
Avoiding null statements