什么是JAVA中的空指针(NullPointerExceptions),如何避免程序中的空指针?

空指针异常可以说是java中最常见的异常之一了,你对它又了解多少呢?

什么是空指针

众所周知,JAVA中有两大类类型:基本类型和引用类型。基本类型可以由编译器保证使用前必须初始化为一个有效值(但是不一定是业务的有效值),使用它不会导致空指针异常;而引用类型可以被初始化为一个特殊的值null,说明该引用未引用任何值
当使用一个null对象的属性,或调用其方法时,会触发空指针异常(NullPointerExceptions)。下面是官方文档给出的抛出异常的情况:

  • Calling the instance method of a null object.(调用null的实例方法)
  • Accessing or modifying the field of a null object.(调用或者修改null的属性)
  • Taking the length of null as if it were an array.(获取null数组的长度)
  • Accessing or modifying the slots of null as if it were an array.(访问或修改null数组中的元素)
  • Throwing null as if it were a Throwable value.(常规抛出null异常)

调用null的实例方法 / 调用或者修改null的属性

可以看下面的代码示例:

// 类型定义
class SomeClass{
    // 仅作演示,尽量不要定义public的属性。
    public int someField;
    public void someMethod(){
    }
}

SomeClass someClass = null;
someClass.someMethod();
someClass.someField = 10;

输出:

Exception in thread "main" java.lang.NullPointerException
    ...

SomeClass someClass = null;声明了一个引用someClass并赋值为nullsomeClass.someMethod();尝试调用null的实例方法(java8+也可能通过 someclass::somemethod 语法来调用),会抛出空指针异常;someClass.someField = 10;修改null的属性,也会抛出空指针异常。

获取null数组的长度 / 访问或修改null数组中的元素

可以看下面的代码示例:

int[] nullArray = null;
int length = nullArray.length;
nullArray[0] = 10;

输出:

// 获取长度空指针
Exception in thread "main" java.lang.NullPointerException
    at ...

int[] nullArray = null;声明了一个引用nullArray并赋值为nullint length = nullArray.length;获取null数组的长度,会抛出空指针异常;nullArray[0] = 10;修改null数组中的元素,也会抛出空指针异常。

常规抛出null异常

出了"被动"触发空指针异常外,我们还可以手动抛出空指针异常,比如这样:throw new NullPointerException("null!");

编程中一些空指针异常触发场景以及规避方法

方法参数中的空指针

方法参数中的空指针触发场景

当我们对外开放一个方法时,方法的入参就有可能会被调用方传入null,比如下列方法:

int someMethod(Object someObj){
    someObj.xxx();
    return 0;
}

如果调用方传入null:int res = someMethod(null);,那么在下面就会导致空指针异常了。

方法参数中的空指针防范方法

对于这类对外开放的(public/protected)方法来说,保护自己就很重要,防范方法一般是提前检查入参是否满足要求,如果不满足要求则抛出异常,可以通过类库中的Objects.requireNonNull()来完成,比如:

int someMethod(Object someObj){
    Objects.requireNonNull(someObj, "someObj must not be null");
    someObj.xxx();
}

这一条当然不只适用于我们自己编写程序的时候,在使用第三方的类库时,也要注意使用的类库的方法参数是否支持null,对方也许粗心大意未处理这类异常,自己要多加小心。

方法返回值中的空指针异常

方法返回值空指针的触发场景

使用别人的程序时,除了要注意入参以外,返回值也要额外留意,确认该方法的返回值是否会返回null,防止造成不必要的麻烦。比如:

Integer getArrayLength(int[] num){
    if (num == null){
        return null;
    } else{
        return num.length;
    }
}

上述方法会返回传入的数组长度,当传入数组为空时,会返回null。如果调用时,传入了一个null数组,并且尝试用它返回的值进行加减,那么就会导致空指针异常了,如下所示:

int[] array = null;
......很长的代码之后
Integer length = getArrayLength(array);
// 空指针!
int res = length + 10;
.....

这里的空指针看起来没有那么明显,实际上,在getArrayLength返回一个装箱类型Integer的时候,这个异常就埋下了伏笔,在int res = length + 10;时,返回的length被自动拆箱,导致了空指针异常。
这也是一种比较罕见的情况,但是出现这类问题可能会有漫长的debug等着我们了。
当返回值是装箱类型的时候,务必要特别留意。

除了上面这种以外,还有一类obj.getXX().getXXX()..的调用方法需要额外小心,一旦出现问题很难定位,最好别用。

方法返回值空指针的防范方法

对于方法的设计者来说,尽量不要返回null,返回数组和集合时,尽量返回一个空数组new SomeObj[0]或者是空集合Collections.emptyXXX(),如果是因为入参存在问题无法正常返回,及时抛出参数异常,不要默默返回null;遇到必须返回null的时候,可以返回Optional<T>。 尽量不要返回装箱类型,除非迫不得已。

对于方法的使用方来说,最好确定方法会不会返回null并做对应处理。实在无法确定的,最好自己判断一下空指针。遇到装箱返回值的,要警惕自动拆箱。

语句块中的空指针

这类空指针比较少见,语句块可以是for/switch/synchronized等。下面依次举例:

// for语句块中,iterable为null会导致空指针
for (element : iterable) 
// switch语句块中,表达式xxx结果为空会导致空指针
switch (xxx) { ... }
// synchronized语句块中,传入null会导致空指针异常
synchronized (someNullReference) { ... }

防范方法主要是提前检查。

一些其他的防范方法

string比较时,常量在前面

见代码:

if ("some string".equals(xxx))

使用SonarLint插件(idea)

SonarLint插件不止可以检查空指针,还可以检查很多的常见编程问题。idea中可以直接在插件市场搜索安装。
提示: idea中代码被标注成背景是黄色时,通常说明可能存在问题,可以把鼠标挪上去查看详情。

jdk14中的空指针异常增强

jdk14中添加了对于空指针异常友好的提示,便于开发者快速定位空指针的对象。示例代码:

int[] nullArray = null;
nullArray[0] = 10;

输出如下:

Exception in thread "main" java.lang.NullPointerException: Cannot store to int array because "nullArray" is null

可以看到比之前的提示友好了很多,可以直接定位问题所在。