说明
多年业务开发经验总结一篇关于 Java 空指针异常(NullPointerException,简称 NPE)隐藏最深的场景,这对于 Java 开发者特别有帮助!空指针异常通常是在不小心引用了 null
对象时发生的,在一些潜在的地方,我们可能没有及时捕捉到异常,导致它隐藏得比较深。下面我会帮你列出一些常见的但容易忽视的场景,并附上一些代码示例
1. 自动拆箱(Auto-unboxing)
当你尝试将 null
值拆箱为基本数据类型时,Java 会抛出空指针异常。
Integer number = null;
int n = number; // 会抛出 NullPointerException
解释:Integer
是一个包装类,null
是无效的,拆箱时会抛出空指针异常。
2. 方法链调用
多个方法链调用时,如果其中某个方法返回了 null
,后续方法调用会抛出空指针异常。
class Person {
private Address address;
public Address getAddress() {
return address;
}
}
class Address {
private String street;
public String getStreet() {
return street;
}
}
Person person = null;
String street = person.getAddress().getStreet(); // 会抛出 NullPointerException
解释: person
是 null
,直接调用 getAddress()
会导致空指针异常。
3. 集合操作中的空指针异常
当操作集合时,某个元素为 null
,对该元素调用方法时也会引发空指针异常。
List<String> list = new ArrayList<>();
list.add(null);
int length = list.get(0).length(); // 会抛出 NullPointerException
解释:当集合中包含 null
值时,直接对其调用方法会导致异常。
4. 遍历集合 出现 null
集合List支持 foreach 遍历,但是如果List变量为null,则一定会发生空指针异常 NPE。如下代码所示,当ids为空时,会发生NPE。
正确做法,在遍历List之前,一定要进行空值检查。
List<Integer> ids = extractFromRequest();
//如果ids为 null,则发生 NPE
for(Integer id: ids){
//do something
}
5. Stream 操作中的空指针异常
在使用 Java 8 的 Stream
API 时,如果流中的某个元素为 null
,而后续操作对其进行方法调用时,会导致空指针异常。
List<String> list = Arrays.asList("a", "b", null, "d");
list.stream().map(String::length).collect(Collectors.toList()); // 会抛出 NullPointerException
解释:map
操作中,null
元素无法调用 length
方法。
6. Null 引用作为方法参数
某些方法接收了一个 null
值参数,却没有进行 null
校验,导致后续对该参数的操作抛出空指针异常。
public void printName(String name) {
System.out.println(name.length()); // 没有 null 检查
}
printName(null); // 会抛出 NullPointerException
解释:在方法内直接调用 name.length()
,而 name
是 null
,导致异常。
7. 多线程环境中的共享变量
多个线程共享同一个变量,并且有可能其中某个线程将该变量设置为 null
,导致其他线程出现空指针异常。
class SharedResource {
private String message;
public void setMessage(String message) {
this.message = message;
}
public void printMessage() {
System.out.println(message.length()); // 可能会抛出 NullPointerException
}
}
SharedResource resource = new SharedResource();
resource.setMessage(null);
resource.printMessage(); // 会抛出 NullPointerException
解释:线程安全问题,可能导致 message
在某些情况下为 null
。
8. Optional 的使用不当
Optional
是 Java 8 引入的用于避免空指针异常的工具,但不正确使用时仍然会抛出空指针异常。
Optional<String> name = Optional.ofNullable(null);
int length = name.get().length(); // 会抛出 NoSuchElementException,实际上是包装了空指针异常
解释:Optional.get()
在 Optional
为 null
时,会抛出异常。
9. Java Reflection API
使用反射时,如果你尝试访问一个为 null
的对象字段或方法,也会引发空指针异常。
import java.lang.reflect.Method;
class Person {
private String name;
}
Person person = null;
Method method = Person.class.getMethod("getName");
String name = (String) method.invoke(person); // 会抛出 NullPointerException
解释:反射操作中的对象为 null
,直接调用方法会抛出空指针异常。
10. JSON 解析中的空指针异常
使用 JSON 序列化与反序列化时,如果解析出的对象为 null
,而后续操作尝试访问其属性,便会引发空指针异常。
import com.fasterxml.jackson.databind.ObjectMapper;
class Person {
private String name;
}
String json = "{}"; // JSON 字符串没有包含 name 字段
ObjectMapper mapper = new ObjectMapper();
Person person = mapper.readValue(json, Person.class);
String name = person.name.toLowerCase(); // 会抛出 NullPointerException
解释:反序列化出来的对象属性为 null
,对 null
调用方法抛出异常。
11. 打印日志使用 + 号出现 null
日志打印时,很多人习惯使用 + 加号拼接日志,这种情况可能导致 空指针异常。
正确做法:使用{}占位符方式,打印变量。 日志框架会进行判空,当变量出现 Null 时,不会出现空指针。
很多人在 review 代码时,不重视日志代码,容易忽略日志代码中的问题。曾经有个同事搞出的线上问题就是因为日志打印出现了 NPE。
一定要敬畏每一行代码,包括日志代码。
12. String 操作中的空指针
如果 String
对象为 null
,而我们调用其方法(例如 length()
、charAt()
等),会导致空指针异常。
String str = null;
int len = str.length(); // 会抛出 NullPointerException
String str2 = null;
System.out.println(str2.toString()); // 会抛出 NullPointerException
解释: str
是 null
,直接调用 length()
方法会抛出异常。
解释:当对象为 null
时,调用其 toString()
方法就会触发异常。
13. 使用 new
运算符与 null
有时候我们可能误以为通过 new
运算符实例化一个对象就一定有效,但如果对象已经是 null
,某些操作会导致异常。
String[] array = new String[5];
array[0] = null;
System.out.println(array[0].length()); // 会抛出 NullPointerException
解释:虽然数组被创建,但元素是 null
,对 null
元素调用方法会引发异常。
14. Lambda 表达式中的空指针
在 Java 8 的 Lambda 表达式中,若方法引用或函数式接口参数为 null
,调用时可能会导致空指针异常。
List<String> list = Arrays.asList("a", "b", null);
list.forEach(s -> System.out.println(s.length())); // 会抛出 NullPointerException
解释:forEach
方法尝试调用 null
元素的 length()
方法时会出错。
15. 容器(如 Map、Set)中的空指针异常
在使用容器类(如 Map
、Set
)时,如果尝试访问不存在的元素或将 null
放入容器,可能会导致空指针异常,尤其是在后续操作时。
Map<String, String> map = new HashMap<>();
map.put("key", null);
String value = map.get("key").toLowerCase(); // 会抛出 NullPointerException
解释:虽然 key
存在,但 null
值不能调用方法,导致空指针异常。
16. 集合和流中的空指针异常
Java 8 的 Stream
API 操作中,元素本身为 null
时,会抛出空指针异常。
List<String> list = Arrays.asList("one", "two", null);
list.stream().filter(s -> s.length() > 3).collect(Collectors.toList()); // 会抛出 NullPointerException
解释:过滤操作中,null
值不能调用 length()
方法。
17. 内部类(或匿名类)引用外部类字段为 null
当外部类的字段是 null
,而内部类或匿名类试图访问该字段时,也会导致空指针异常。
class Outer {
private String name;
public void createInner() {
Inner inner = new Inner();
inner.printName(); // 会抛出 NullPointerException
}
class Inner {
public void printName() {
System.out.println(name.length()); // 外部类的字段是 null
}
}
}
Outer outer = new Outer();
outer.createInner(); // 会抛出 NullPointerException
解释:外部类的字段 name
为 null
,内部类访问时会出错。
18. 带有 null
参数的递归方法
递归方法中,如果传递的参数为 null
,且方法没有对其进行有效的 null
检查,则可能抛出空指针异常。
public void printLength(String str) {
if (str == null) return;
System.out.println(str.length());
printLength(str.substring(1)); // 如果 str 是 null,会抛出异常
}
printLength(null); // 会抛出 NullPointerException
解释:方法没有判断 str
是否为 null
,会引发异常。
19. 数据库操作中的空指针异常
在与数据库交互时,如果从数据库返回的结果集为 null
,且代码没有进行检查,直接访问字段或属性时会抛出空指针异常。
ResultSet rs = getResultSetFromDatabase();
String name = rs.getString("name").toLowerCase(); // 如果 rs.getString 返回 null,则会抛出 NullPointerException
解释:数据库返回的 name
为 null
,调用 toLowerCase()
时会抛出空指针异常。
20. 不当的 equals
方法实现
自定义类的 equals
方法中,如果未正确处理 null
值,可能会导致空指针异常。
class Person {
private String name;
public boolean equals(Person other) {
return this.name.equals(other.name); // 没有处理其他为 null 的情况
}
}
Person p1 = new Person();
Person p2 = null;
p1.equals(p2); // 会抛出 NullPointerException
解释:other
为 null
,调用 this.name.equals(other.name)
会导致空指针异常。
结尾建议:
这些场景展示了空指针异常在不同情况下的潜在发生点。你可以通过以下方式避免这些问题:
- 及时进行
null
检查。 - 使用
Optional
类型代替直接使用null
。 - 使用
Objects.requireNonNull
来确保传入的参数不为null
。 - 在合适的地方使用断言、
try-catch
块等来捕获异常并做处理 - 另外,可以强调如何通过单元测试或集成测试来提前发现这类问题