Java程序员必看:17种触发NullPointerException的场景

原文标题:我有一个朋友写出了17种触发NPE的代码!避免这些坑

原文作者:阿里云开发者

冷月清谈:

NullPointerException (NPE) 是Java程序员经常遇到的异常。本文总结了17种常见的NPE触发场景,包括访问空对象的实例变量或方法、访问或修改空数组元素、未初始化对象数组元素、抛出null、在null引用上同步、自动拆箱时遇到null、集合/Map中获取null元素、方法链调用中遇到null、枚举valueOf方法使用null、集合操作不支持null元素、多线程环境下无适当同步、依赖注入的对象为null、Lambda表达式或方法引用中目标引用为null、Stream API处理时遇到null元素、使用增强for循环遍历null集合、在Junit中使用Mockito时错误使用any()匹配基本数据类型参数、以及Optional类的错误使用等。

怜星夜思:

1、除了文章中提到的场景,你还遇到过哪些容易引发NPE的情况?
2、在实际开发中,你是如何避免NPE的呢?有哪些好的实践经验可以分享?
3、你认为Java语言在设计上是否存在一些缺陷,导致NPE如此常见?

原文内容

阿里妹导读


我有一个朋友,写代码的时候常常遭到NPE背刺,痛定思痛,总结了NPE出没的17个场景,哪一个你还没有遇到过?
虽然无法统计出Java程序员写的最多的异常是哪一个,NullPointerException(NPE)它一定会榜上有名。无论是初次涉足编程世界的新手,还是在代码海洋中久经风浪的老鸟,几乎都写过NPE异常。要防范它,不在高超的编码技巧,在细。
我有一个朋友,写代码的时候常常遭到NPE背刺,痛定思痛,总结了NPE出没的17个场景,哪一个你还没有遇到过?

1.访问空对象的实例变量或者调用空对象的实例方法

从其它方法(如远程hsf方法或者从DB)返回的结果,不做控制判断,直接访问变量或者方法,可能会NPE。
反例
public class Test {
public String a;

public String getA() {
return a;
}

public static void main(String args) {
Test test = getFromSomeMethod();
//访问属性 NPE
System.out.println(test.a);
//访问方法 NPE
System.out.println(test.getA());
}

private static Test getFromSomeMethod() {
return null;
}
}

2.访问或修改空数组的元素

反例
public class Test {
public static void main(String[] args) {
int[] numbers = getFromSomeMethod();
//访问 NPE
int number = numbers[0];
//修改 NPE
numbers[0] =1;
}
private static int[] getFromSomeMethod() {
return null;
}
}

3.未初始化的对象数组中的元素默认是null

反例
public class Test {
private String a;

public String getA() {
return a;
}

public static void main(String args) {
Test testArr = new Test[2];
//NPE
testArr[0].getA();
}

}

4.throw一个null 出去

在Java中,通常不会故意使用 throw null 这种表达,因为它实际上没有任何有用的用途。根据Java语言规范,throw 关键字后面应该跟随一个可抛出的对象(Throwable 类或其子类的实例),而 null 并不是一个可抛出的对象。如果你执行 throw null ,将会得到一个NPE
猜想可能出现的场景:
  • 代码错误:throw null 可能是代码编写错误或者不完整的异常处理。例如,可能打算抛出一个实际的异常对象,但误写成了 null

  • 测试代码:在单元测试中,有时可能会故意使用throw null , 来确保他们的异常处理代码能够妥善处理意外情况。(不推荐)

反例
public class Test {
public static void main(String[] args) {
getFromSomeMethod();
}

private static int getFromSomeMethod() {
try {
int a = 1/0;
}catch (Exception e){
//NPE
throw null;
}
return 0;
}
}

5.在null 引用上进行synchronized同步

反例
public class Test {
public static void main(String[] args) {
Test test = getFromSomeMethod();
//NPE
synchronized (test){

}
}
private static Test getFromSomeMethod() {
return null;
}
}

6.在自动拆箱过程中遇到null

自动装箱不会遇到Null的问题,因为这个过程是把基本类型的值转换成包装类对象,基本类型的值是没有Null的。
反例
public class Test {
public static void main(String[] args) {
Integer integer = getFromSomeMethod();
//NPE
if (integer > 1) {

}
}
private static Integer getFromSomeMethod() {
return null;
}

}

定义方法返回一个int 作为出参,但实际return 一个null,也会NPE。
反例
public static void main(String[] args) {
getFromSomeMethod();

}
private static int getFromSomeMethod() {
Integer a = null;
// NPE
return a;
}

7.从集合/Map中获取null元素并直接使用

从集合/map中获取元素并使用时,建议对Null进行检查,以避免潜在的NPE,特别是在那些隐式触发自动拆箱的场景中。
反例
Map<String, String> map = new HashMap<>();
String value = map.get("key");
//NPE
int length = value.length();

8.方法链调用中上一步骤返回null

图片

反例
Test test = new Test().getA().getB().getC();

9.枚举的valueOf方法使用null

反例
Enum enum = Enum.valueOf(null);

10.集合操作不支持null元素

HashSet 、LinkedHashSet 都只允许添加一个null。后续无论添加多少null元素,都会被忽视。
TreeSet 不允许添加null值,排序集合依赖元素直接的比较操作,而null元素不能与其它对象进行比较,会抛出NPE;
反例
Set<String> set = new TreeSet<>();
set.add(null);

11.多线程环境中无适当同步可能导致不一致状态

示例
public class Test implements Runnable {
private static String sharedResource;

public void run() {
sharedResource = “sharedResource”;
}

public static void main(String args) throws InterruptedException {
Thread thread = new Thread(new Test());
thread.start();
// 在多线程环境中,如果没有适当的同步,这里可能导致NPE
//thread.join();
System.out.println(sharedResource.length());
}
}

12.依赖注入:注入的对象为null

required属性为false,启动过程中如果没有找到合适的bean,service 会被设置为null。在调用service的任何方法之前都需要判断service是否为null。
示例
@Autowired(required = false)
private SomeService service;

13.Lambda表达式或方法引用中目标引用为null

反例
Test test = null;
Consumer<Test> todoSomething = test::someMethod;

14.Stream API处理时遇到null元素

反例
list = Arrays.asList("a", null);
> lengths = list.stream().map(String::length).collect(Collectors.toList());

正例

List<String> list = Arrays.asList("a", null);
List<Integer> lengths = list.stream()
.filter(Objects::nonNull) // 过滤null
.map(String::length).collect(Collectors.toList());

15.使用增强for循环遍历集合时,没有判空

反例
List<String> list = null;
for (String item : list) { }

16.在Junit中使用Mockito时,

错误地使用any()匹配基本数据类型参数

在JUnit4中,使用Mockito框架时,any() 是一个参数匹配器,当与基本数据类型一起使用时,需要使用相应的类型特定的匹配器,例如使用anyInt() 而不是any()。因为any()实际上返回的是null,而null不能自动转换为基本数据类型。
反例
when(service.doSomething(any())).thenReturn("Success");
// service.doSomething(int a)
正例
when(service.doSomething(anyInt())).thenReturn("Success");
// service.doSomething(int a)

17.Optional类的正确使用

Optional类在 Java 8 中被引入,其设计初衷是为了提供一种更优雅的方式来处理可能为Null的值,从而减少空指针异常NPE的发生。
但Optional类设计为减少NPE的可能性,却并不是万能的,比如开发者在使用Optional,不检查是否存在,直接调用Optional.get(),那么会得到一个NoSuchElementException。也仍然存在即使使用了Optional,也可能出现NPE的情况。你看:
反例
Optional<String> optional = Optional.of(null);
正例
Optional<String> optional = Optional.ofNullable(null);


Java语言中的null是一个特殊的值,它可以赋值给任何引用类型的变量,这就导致了NPE的可能性。相比之下,像Kotlin这样的语言就引入了可空类型系统,可以有效避免NPE。

数据库查询返回结果为空,没做非空判断就进行操作,结果喜提NPE一枚。

有一次我用第三方库的时候,由于对API不熟悉,传入了一个null参数,结果导致程序崩溃了,调试了半天发现是NPE。

null的设计初衷是为了表示一种空状态,但它确实给程序员带来了很多麻烦。未来的编程语言可能会探索更好的方式来处理空值。

在团队开发中,可以制定一些代码规范,例如强制要求对方法的返回值进行非空判断,这样可以从源头上减少NPE的发生。

我推荐使用静态代码分析工具,例如FindBugs或PMD,它们可以帮助我们识别代码中潜在的NPE风险。

我个人比较喜欢使用Optional类来处理可能为null的值,这样可以避免很多NPE。另外,在代码中多加一些非空判断也是很有必要的。

我曾经遇到过在使用反射获取类的字段或方法时,如果传入的字段名或方法名错误,就会导致NPE。

NPE的问题其实更多是程序员的责任,Java语言本身提供了很多工具来帮助我们避免NPE,例如Optional类、断言机制等,关键是要正确地使用它们。