新手业务编码中常见的null值和空指针处理
背景
大家都知道,使用对象的时候,由于对象的默认值为null, 如果没有及时判空就去调用对象的方法,可能会带来空指针异常的问题。本篇将会讲解空指针异常容易在哪些情况下出现,新手应该如何去避免无处不在的null值问题,又应该如何修复。主要举一些常见的例子来配合说明。
1、自动拆箱导致的空指针异常
首先,自动拆箱有两种场景,第一种是包装类型的变量赋值给基本类型的变量时,会出现自动拆箱;
Integer num = 10; // 自动装箱
int value = num; // 自动拆箱
System.out.println(value); // 输出:10
第二种是基本类型作为方法的形参,而这个方法被调用时,传入的参数是包装类的变量,就会发生自动拆箱,如下所示:
public class AutoUnboxExample {
public static void printInt(int num) {
System.out.println(num);
}
public static void main(String[] args) {
Integer number = new Integer(20); // 创建一个Integer对象
printInt(number); // 自动拆箱:将Integer对象转换为int类型
}
}
那自动拆箱什么时候会发生空指针异常呢?异常的原因是什么?
首先,赋值的时候,
Integer num = null; // 自动装箱
int value = num; // 自动拆箱, 将null 值赋给了value。此时必定会报空指针异常。
javap 得到字节码文件
Compiled from "NullpointerExceptionTest.java"
public class com.example.demo3.commonpitfalls.service.NullpointerExceptionTest {
public com.example.demo3.commonpitfalls.service.NullpointerExceptionTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: aconst_null
1: astore_1
2: aload_1
3: invokevirtual #2 // Method java/lang/Integer.intValue:()I
6: istore_2
7: return
}
可以发现,字节码文件中有一个Integer.intValue() 这样的调用,对象为null, 用null调用这个方法, 所以才会产生空指针异常。
这边给出规避自动拆箱引发空指针的建议:
1、包装类类型和基本数据类型都可以的业务场景下,优先考虑使用基本类型。
2、对于不确定的包装类类型,一定要对NULL情况做检验和判断。
3、对于值为NULL的包装类类型,建议可以试着赋值为0;当然了,也要注意NULL和0代表的业务含义是否是一样的。
2、字符串比较出现空指针异常
String str1 = "hello";
String str2 = null;
// 下面这行代码会导致空指针异常,因为str2为null
int result = str1.compareTo(str2);
因为compareTo()方法会调用length(), 当传入的对象为null, 会引发空指针异常。
3、并发容器规定不允许put null值时,强行put会引发空指针异常
如ConcurrentHashMap 这样的容器不支持 Key 和 Value 为 null,强行 put null 的 Key 或 Value 会出现空指针异常。
ConcurrentHashMap map = new ConcurrentHashMap();
map.put(null, null); // 必然引发空指针异常, 因为多线程并发环境下,put(null,null)会引发二义性问题。
其实,规定键不能为null 以及 值不能为null, 是为了避免二义性的问题。null 是一个特殊的值,表示没有对象或没有引用。拿 get 方法取值来说,返回的结果为 null 会存在两种情况:
值没有在集合中;
值本身就是 null。
如果你用 null 作为键,那么就无法区分这个键是否是存在于 ConcurrentHashMap 中或者根本没有这个键。同样,如果你用 null 作为值,那么就无法区分这个值是真正存储在 ConcurrentHashMap中的,还是因为找不到对应的键而返回了null。
另外,多线程环境下,无法使用containKey(key)来判断是否存在这个key-value, 因为判断的同时,会有其他线程对键值进行修改。
需要注意的是,HashMap 可以存储 null 的 key 和 value,但 null 作为键也只能有一个,null 作为值可以有多个。
如果传入null 作为参数,就会返回 hash 值为 0 的位置的值。单线程环境下,不存在一个线程操作该 HashMap 时,其他的线程将该 HashMap 修改的情况,所以可以通过 contains(key)来做判断是否存在这个键值对,从而做相应的处理,也就不存在二义性问题。
4、对象级联调用可能导致NPE
这种情况就是A类中包含了B类。有时候忘记对B类的实例进行判空,而导致NPE问题。
class B {
public void methodB() {
System.out.println("Method B called");
}
}
@Data
class A {
private B b;
}
public class Main {
public static void main(String[] args) {
B b = new B();
A a = new A(b);
// 没有对字段进行空值检查,直接尝试级联调用 B 对象的方法
a.getB().methodB(); // 这里可能会导致空指针异常
}
}
5、返回的List为null时,后调用了size(), 导致NPE
import java.util.List;
public class RemoteService {
public List<String> getData() {
// 模拟远程服务返回的 List 为 null
return null;
}
}
public class Main {
public static void main(String[] args) {
RemoteService remoteService = new RemoteService();
List<String> data = remoteService.getData();
// 没有对返回的 List 进行空指针检查,直接调用方法可能导致空指针异常
int size = data.size(); // 这里可能导致空指针异常
System.out.println("Size of data: " + size);
}
}
以上是一些常见的可能会出现的空指针异常(NPE),下面通过一些实践代码来讲一下如何去修复NPE问题。
其实,最常见的方法就是先判个空,然后再进行对象的操作,这样可以直接避免NPE抛出。不过,这只能让异常不再出现,我们最好是找到程序逻辑中出现的空指针究竟是来源于入参还是 Bug:
如果是来源于入参,应进一步分析入参是否合理等;
如果是来源于Bug,那空指针不一定是纯粹的程序Bug,可能还涉及业务属性和接口调用规范等。总而言之,结合实际业务代码的上下文去修复问题。
如果时间有限,当然使用ifelse逻辑先判一下空是没问题的。不过,java8提出的Optional 类是专门用来消除这样的 if-else 逻辑,使用一行代码就能进行判空和处理!
接下来,写一个实践的代码,使用Optional类去修复上文提到的NPE问题。
实际例子
package com.example.demo3.commonpitfalls.service;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 分别用来测试java中常见的几种场景下的NPE
* 1、参数值是 Integer 等包装类型,使用时因为自动拆箱出现了空指针异常;
* 2、字符串比较出现空指针异常;
* 3、诸如 ConcurrentHashMap 这样的容器不支持 Key 和 Value 为 null,强行 put null 的
* Key 或 Value 会出现空指针异常。
* 4、A 对象包含了 B,在通过 A 对象的字段获得 B 之后,没有对字段判空就级联调用B的方法出现空指针异常。
* 5、方法或远程服务返回的 List 不是空而是 null,没有进行判空就直接调用 List 的方法出现空指针异常
*/
@Slf4j
@RestController
@RequestMapping("/nullpointer")
public class NullpointerExceptionTest {
/**
* 代码中模拟了 5 种空指针异常
* 对入参 Integer i 进行 +1 操作;
* 对入参 String s 进行比较操作,判断内容是否等于"OK";
* 对入参 String s 和入参 String t 进行比较操作,判断两者是否相等;
* 对 new 出来的 ConcurrentHashMap 进行 put 操作,Key 和 Value 都设置为 null。
* wrongMethod 的返回值, return null
*/
private List<String> wrongMethod(FooService fooService, Integer i, String s, String t) {
log.info("result {} {} {} {}",
i + 1,
s.equals("OK"), s.equals(t),
new ConcurrentHashMap<String, String>().put(null, null));
if (fooService.getBarService().bar().equals("OK"))
log.info("OK");
return null;
// 使用Optional类去做包装,为null时会返回一个默认值,而不是返回null,从而避免抛异常。
private List<String> rightMethod(FooService fooService, Integer i, String s, String t) {
// i为null 时,i的值设置为0;i不为null时,返回i的值;再进行加1。 OK放在前面进行equals,就不会出现NPE。
log.info("result {} {} {} {}", Optional.ofNullable(i).orElse(0) + 1, "OK".equals(s), "OK".equals(t));
// 使用Optional包装起来。如果不为null, 执行流的后续操作;如果为null, 返回空的Optional对象。
Optional.ofNullable(fooService)
.map(FooService::getBarService)
.filter(barService -> "OK".equals(barService.bar()))
.ifPresent(result -> log.info("OK"));
// 返回一个空的List, 比起返回null的好处是可以避免NPE。
return new ArrayList<>();
}
// 会抛NPE的错误使用
@GetMapping("wrong")
public int wrong(@RequestParam(value = "test", defaultValue = "1111") String test) {
return wrongMethod(
test.charAt(0) == '1' ? null : new FooService(),
test.charAt(1) == '1' ? null : 1,
test.charAt(2) == '1' ? null : "OK",
test.charAt(3) == '1' ? null : "OK").size();
}
// 不再抛NPE的正确使用
@GetMapping("right")
public int right(@RequestParam(value = "test", defaultValue = "1111") String test ) {
return Optional.ofNullable(rightMethod(
test.charAt(0) == '1' ? null : new FooService(),
test.charAt(1) == '1' ? null : 1,
test.charAt(2) == '1' ? null : "OK",
test.charAt(3) == '1' ? null : "OK"))
.orElse(Collections.emptyList()).size(); // 如果返回为null,则返回空列表。然后获取空列表的size。
}
class FooService {
@Getter
private BarService barService;
}
class BarService {
String bar() {
return "OK";
}
}
}
测试结果
wrong接口
right接口