程序中的变量是 null,就意味着它没有引用指向或者说没有指针。这时,我们对这个变量进行任何操作,都必然会引发空指针异常,在 Java 中就是 NullPointerException。那么,空指针异常容易在哪些情况下出现,又应该如何修复呢?
空指针异常虽然恼人但好在容易定位,更麻烦的是要弄清楚 null 的含义。比如,客户端给服务端的一个数据是 null,那么其意图到底是给一个空值,还是没提供值呢?再比如,数据库中字段的 NULL 值,是否有特殊的含义呢,针对数据库中的 NULL 值,写 SQL 需要特别注意什么呢?
今天,就让我们带着这些问题开始 null 的踩坑之旅吧。
NullPointerException 是 Java 代码中最常见的异常,我将其最可能出现的场景归为以下 5 种:
参数值是 Integer 等包装类型,使用时因为自动拆箱出现了空指针异常; 字符串比较出现空指针异常; 诸如 ConcurrentHashMap 这样的容器不支持 Key 和 Value 为 null,强行 put null 的 Key 或 Value 会出现空指针异常; A 对象包含了 B,在通过 A 对象的字段获得 B 之后,没有对字段判空就级联调用 B 的方法出现空指针异常;方法或远程服务返回的 List 不是空而是 null,没有进行判空就直接调用 List 的方法出现空指针异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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 ; }
@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(); }
class FooService { @Getter private BarService barService;
}
class BarService { String bar() { return "OK" ; } } |
修复思路如下:
对于 Integer 的判空,可以使用 Optional.ofNullable 来构造一个 Optional,然后使用 orElse(0) 把 null 替换为默认值再进行 +1 操作。对于 String 和字面量的比较,可以把字面量放在前面,比如"OK".equals(s),这样即使 s 是 null 也不会出现空指针异常;而对于两个可能为 null 的字符串变量的 equals 比较,可以使用 Objects.equals,它会做判空处理。
对于 ConcurrentHashMap,既然其 Key 和 Value 都不支持 null,修复方式就是不要把 null 存进去。HashMap 的 Key 和 Value 可以存入 null,而 ConcurrentHashMap 看似是 HashMap 的线程安全版本,却不支持 null 值的 Key 和 Value,这是容易产生误区的一个地方。
对于类似 fooService.getBarService().bar().equals([OK]) 的级联调用,需要判空的地方有很多,包括 fooService、getBarService() 方法的返回值,以及 bar 方法返回的字符串。如果使用 if-else 来判空的话可能需要好几行代码,但使用 Optional 的话一行代码就够了。
对于 rightMethod 返回的 List,由于不能确认其是否为 null,所以在调用 size 方法获得列表大小之前,同样可以使用 Optional.ofNullable 包装一下返回值,然后通过.orElse(Collections.emptyList()) 实现在 List 为 null 的时候获得一个空的 List,最后再调用 size 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
private List<String> rightMethod(FooService fooService, Integer i, String s, String t) { log.info( "result {} {} {} {}" , Optional.ofNullable(i).orElse( 0 ) + 1 , "OK" .equals(s), Objects.equals(s, t), new HashMap<String, String>().put( null , null )); Optional.ofNullable(fooService) .map(FooService::getBarService) .filter(barService -> "OK" .equals(barService.bar())) .ifPresent(result -> log.info( "OK" )); return new ArrayList<>(); }
@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 是希望把姓名重置为空,允许这样的操作,使用 Optional 的 orElse 方法一键把空转换为空字符串即可。
对于年龄,我们认为如果客户端希望更新年龄就必须传一个有效的年龄,年龄不存在重置操作,可以使用 Optional 的 orElseThrow 方法在值为空的时候抛出 IllegalArgumentException。
对于昵称,因为数据库中姓名不可能为 null,所以可以放心地把昵称设置为 guest 加上数据库取出来的姓名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@PostMapping ( "right" ) public UserEntity right( @RequestBody UserDto user) { if (user == null || user.getId() == null ) throw new IllegalArgumentException( "用户Id不能为空" );
UserEntity userEntity = userEntityRepository.findById(user.getId()) .orElseThrow(() -> new IllegalArgumentException( "用户不存在" ));
if (user.getName() != null ) { userEntity.setName(user.getName().orElse( "" )); } userEntity.setNickname( "guest" + userEntity.getName()); if (user.getAge() != null ) { userEntity.setAge(user.getAge().orElseThrow(() -> new IllegalArgumentException( "年龄不能为空" ))); } return userEntityRepository.save(userEntity); } |
到此这篇关于Java中怎样处理空指针异常的文章就介绍到这了,更多相关Java 空指针异常内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
原文链接:https://juejin.cn/post/7102613824833847327