新手业务编码中常见的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接口
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~