空指针异常是导致java程序运行中断最常见的原因,相信每个程序猿都碰见过,也就是NullPointException,我们通常简称为NPE,为了提高我们写的代码的健壮性,本文告诉大家如何优雅避免NPE。
出现空指针异常的情况
访问空对象的属性或调用空对象的方法
当一个对象是null时,试图访问一个对象的属性或调用其方法,就会触发空指针异常。
代码示例
String text = null;
int length = text.length();
User user = null;
String userName = user.getUserName();
数组为null或者数组元素为null
当尝试访问数组中的某个索引处的元素,而该元素为null
时,同样会导致空指针异常。
String[] strs = null;
int length = strs.length;
String[] strs = new String[3];
int length = strs[2].length();
集合中null元素访问
当集合中存在null元素,当我们遍历集合,访问到这个元素的属性或者方法时也会抛出NPE,这种情况也会出现在我们的日常开发中,有时候就会因为数据问题导致这种情况发生,常常也莫名其妙。。。。
List<String> list = Lists.newArrayList();
list.add(null);
System.out.println(list.get(0).length());
调用的方法返回null
调用某个方法,期望其返回一个非null的对象,但实际返回了null。当然这种情况等同于访问空对象的属性或者方法。这在实际开发过程中极易出现的一种情况。比如我们使用Mybatis
从数据库中查询一条记录时,数据不存在,就会返回null。这种情况尤为注意。
使用基本数据类型的包装类
在使用基本数据类型的包装类时,如果未正确初始化,再转成int时,可能导致空指针异常。
Integer i = null;
int num = i;
避免NPE的几种方式
访问对象前要谨慎
在使用对象之前,始终检查它是否为null。这包括方法参数、返回值以及对象的属性。在访问对象的方法或属性之前,使用条件语句判断对象是否为null。比如我们在访问User对象前,一定要判null。
如果对象访问不可避免时,我们也要遵循以下规则:
在使用对象之前,始终检查它是否为null
包括方法参数、返回值以及对象的属性。在访问对象的方法或属性之前,使用条件语句判断对象是否为null。比如我们在访问User对象前,一定要判null。
User user = new User();
if (user != null){
String userName = user.getUserName();
Address address = user.getAddress();
if (address != null){
String coutry = address.getCountry();
}
}
或者我们的user是从一个方法中获取的,例如数据库中查询,那么我们在访问这个对象前,一定要判null,如果为null要抛出对应的业务异常,然后我们就可以在接口响应中对应返回错误的信息即可,此时就算是一个正常的流程了。这点尤为重要,一定要注意。
User user = userManager.getUserById(Long userId);
if (user == null){
throw new ServiceException(""当前查询的对象不存在);
}
当然如果使我们在写User getUserById(Long id)
返回对象或者List<User> listUserByIds(List<Long> idList)
时我们可以不返回null
,可以返回一个对象默认信息或者一个空集合,这样调用方就不会出现NPE风险,当然我们不强制返回一个对象或者空集合,但是必须添加注释充分 说明什么情况下会返回null值。这也是阿里巴巴开发手册规约的建议。
从已知的String对象中调用equals()和equalsIgnoreCase()方法,而非未知对象。
总是从已知的非空String对象中调用equals()方法。因为equals()方法是对称的,调用a.equals(b)和调用b.equals(a)是完全相同的,这也是为什么程序员对于对象a和b这么不上心。如果调用者是空指针,这种调用可能导致一个空指针异常
Object unknownObject = null ;
//错误方式 – 可能导致 NullPointerException
if (unknownObject.equals( "knownObject" )){
System.err.println( "This may result in NullPointerException if unknownObject is null" );
}
//正确方式 - 即便 unknownObject是null也能避免NullPointerException
if ( "knownObject" .equals(unknownObject)){
System.err.println( "better coding avoided NullPointerException" );
}
当valueOf()和toString()返回相同的结果时,宁愿使用前者。
因为调用null对象的toString()会抛出空指针异常,如果我们能够使用valueOf()获得相同的值,那宁愿使用valueOf(),传递一个null给valueOf()将会返回“null”,尤其是在那些包装类,像Integer、Float、Double和BigDecimal。
BigDecimal bd = getPrice();
System.out.println(String.valueOf(bd)); //不会抛出空指针异常
System.out.println(bd.toString()); //抛出 "Exception in thread "main"
使用Optional类
JDK8以上版本提供了Optional
类,它是一个容器对象,可用于包装可能为null的值。我们可以使用它判断null问题,同时也解决了多层级访问问题,配合使用orElse时,会先执行orElse方法,然后执行逻辑代码,不管是否出现了空指针。
//获取user对象中的Country属性,如果为空则会存储一个空字符串到country中
String country = Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCountry)
.orElse("");
//获取user对象中的Country属性,如果Country属性为空则会存储一个空字符串到country中
String country = Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCountry)
.orElseGet(() -> defaultContry());
private String defaultContry(){
return "CN";
}
我们还可以使用orElseThrow()方法,当Optional中的对象是一个null时我们直接抛出异常:
String userName = Optional
.ofNullable(user)
.map(User::getUserName)
.orElseThrow(() -> new ServiceException("当前用户信息不存在"));
定义数据库中的字段是否可为空。
如果你在使用数据库来保存你的域名对象,如Customers,Orders 等,你需要在数据库本身定义是否为空的约束。因为数据库会从很多代码中获取数据,数据库中有是否为空的检查可以确保你的数据健全。在数据空中维护null约束同样可以帮助你减少Java代码中的空指针检查。当从数据库中加载一个对象是你会明确,哪些字段是可以为null的,而哪些不能,这可以使你代码中不必要的!= null检查最少化。
使用断言避免空指针
使用Java断言(assert)来检查变量是否为null。但要注意,断言通常在开发和测试阶段启用,而在生产环境中可能被禁用(在生产环境中,通常不会启用断言以避免不必要的性能开销以及防止潜在的错误信息泄漏)。
User user = new User();
//使用断言确保 user 对象不为 null。如果 user 为 null,则会抛出 AssertionError,
//并显示消息 "user should not be null"。
assert user != null : "user should not be null";
Address address = user.getAddress();
assert address != null : "address should not be null";
String coutry = address.getCountry();
使用@Nullable注解
使用javax.annotation.Nullable
注解,@Nullable
注解通常用于标记一个方法的参数、返回值或者字段可能为null。这个注解并非Java标准库的一部分,但在一些第三方库(如JSR 305库中的javax.annotation.Nullable
,以及Google Guava和JetBrains的Kotlin标准库等)中广泛使用,并且被许多IDE和静态分析工具支持。以便在编译期或开发工具中提示可能的NPE风险。
@Nullable
private static User getUserById(Long userId){
return null;
}
private static void handlerUser(@Nullable User user){
System.out.println(user.getUserName());
}
public static void main(String[] args) {
Long userId = 0L;
User user = getUserById(userId);
String userName = user.getUserName();
handlerUser(user);
}
此时IDEA就会警告会出现NPE风险
避免从方法中返回空指针,而是返回空collection或者空数组。
这个Java最佳实践或技巧由Joshua Bloch在他的书Effective Java中提到。这是另外一个可以更好的使用Java编程的技巧。通过返回一个空collection或者空数组,你可以确保在调用如size(),length()的时候不会因为空指针异常崩溃。Collections类提供了方便的空List,Set和Map: Collections.EMPTY_LIST,Collections.EMPTY_SET,Collections.EMPTY_MAP。这里是实例。
public List getOrders(Customer customer){
List result = Collections.EMPTY_LIST;
return result;
}
使用null安全的方法和库 有很多开源库已经为您做了繁重的空指针检查工作。
其中最常用的一个的是Apache commons 中的StringUtils。你可以使用StringUtils.isBlank(),isNumeric(),isWhiteSpace()以及其他的工具方法而不用担心空指针异常。
//StringUtils方法是空指针安全的,他们不会抛出空指针异常
System.out.println(StringUtils.isEmpty( null ));
System.out.println(StringUtils.isBlank( null ));
System.out.println(StringUtils.isNumeric( null ));
System.out.println(StringUtils.isAllUpperCase( null ));
Output:
true
true
false
false
补充
在JDK 17中引入的Helpful NullPointerExceptions特性确实增强了空指针异常信息的准确性与可用性。当发生NullPointerException时,JVM现在能够提供更精确的位置信息,特别是在链式调用场景下,它会指出导致空指针异常的具体对象引用。这有助于开发者更快地定位到代码中的问题所在,无需通过堆栈跟踪逐层分析来判断哪个对象引用为null。假如我们访问user.getAddress().getCountry().length()
时,在JDK17以前,如果发生了空指针异常,他只会打印出来发生了空指针异常,但是并没有告知到底是user对象还是address对象还是coutnry发生了异常:
Exception in thread "main" java.lang.NullPointerException
at com.study.base.core.base.NpeTest.main(NpeTest.java:23)
但是在JDK17以后,借助Helpful NullPointerExceptions特性,异常信息将更加精确,可能会类似打印这样的信息,精确到哪个值发生了空指针异常:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Address.getCountry()" because "user.address" is null
at com.study.base.core.base.NpeTest.main(NpeTest.java:23)