什么是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
并赋值为null
,someClass.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
并赋值为null
,int 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
可以看到比之前的提示友好了很多,可以直接定位问题所在。