万恶的NPE差点让我半个月工资没了
防止 NPE ,是程序员的基本修养
NPE(Null Pointer Exception)
一直是开发中最头疼的问题,也是最容易忽视的地方。记得刚开始工作的时候所在的项目组线上出现最多的bug
不是逻辑业务bug
而是NPE
,所以后面项目组出了一个奇葩的规矩,线上如果谁出现一个NPE的问题就罚款100元
,用作团建费用。如果项目组每个人一个月都出现个两三个NPE
的话。那么项目组是不是每个月都可以去团建下(自己掏钱海吃海喝,心不心疼)。不过自从这个规矩实施以来,线上的NPE
就渐渐的少了,从最初的一个月团建一次到最后的半年团建一次。大家写代码都比较谨慎了,只要用到对象或者集合的时候二话不说上来先判空,所以产生的NPE
就少了。
业务中返回结果的空值
在我们常见的业务开发中是不是经常会有这样的接口:
package com.workit.demo.nullexcption;
import com.workit.demo.proxy.User;
import java.util.List;
public interface IUserSearchService {
/**
* 查询用户列表
* @return
*/
List<User> listUser();
}
这个接口是不是存在两个潜在的问题?
-
listUser
这个方法 如果没有数据,那它是返回空集合还是null呢? -
getUserById
如果根据ID
没有找到用户,是抛异常还是返回null呢?首先我们先看下listUser
这个方法的实现:
public List<User> listUser() {
List<User> userList = userRepository.listUser();
if (null == userList || userList.size() == 0) {
return null;
}
return userList;
}
这种实现如果调用者是一个严谨的人或者像我这样被NPE
罚款买过单的人,是会对返回结果进行null
的判断。如果调用者并非谨慎的人或者刚刚入门的人,他就会按照自己的理解去调用接口,拿到结果就不管三七二十一上来对结果就是一顿循环操作,而不进行是否为null
的条件判断,如果这样的话,是非常危险的,它很有可能出现空指针异常!这就是在代码中埋了一个定时炸弹,不知道什么时候就会爆炸。
由于存在这种不安全的隐患我们可以看下第二种实现:
public List<User> listUser() {
List<User> userList = userRepository.listUser();
if (null == userList || userList.size() == 0) {
return new ArrayList<>();
}
return userList;
}
对于这种实现它一定会返回List
,即使没有数据,也会返回一个空集合。通过以上的修改,我们成功的避免了有可能发生的空指针异常,这样的写法更安全!那针对于上面的两种实现,一个是需要调用者进行判空,一个是提供接口的人返回默认值。那我们到底应该用哪种方式呢?这种情况《阿里巴巴开发手册》也有明确规定:
所以还是那句话使用任何对象或者集合之前记得先判空。
业务中请求参数空值
/**
* 根据用户ID查询当前用户
* @param id
* @return
*/
User getUserById(Integer id);
这个接口的描述,你能确定入参id
一定是必传的吗?我觉得答案应该是:不能确定。除非接口的文档注释上加以说明。那么我们应该怎样来约束入参呢?
- 强制约束
@Override
public User getUserById(Integer id) {
if (Objects.isNull(id)){
throw new IllegalArgumentException("id不能为空");
}
return null;
}
通过jsr 303
进行严格的约束声明配合AOP的操作进行验证。
User getUserById(@NotNull Integer id);
其他需要注意的NPE
switch中的空指针异常
看下面的列子妥妥的NPE
public static void main(String[] args) {
eat(null);
}
enum EatType{
Breakfast,Lunch,Dinner;
}
public static void eat(EatType eatType){
switch(eatType){
case Breakfast:
System.out.println("吃早饭");
break;
case Lunch:
System.out.println("吃中饭");
break;
case Dinner:
System.out.println("吃晚饭");
break;
default:
System.out.println("输入错误");
break;
}
}
数据库的sum函数
如果price
对应的所有的值为null
,那么算出来的和为null
。
如果采用ifnull
函数就可以求和就是0这样就可以避免空指针。
使用Map类集合时需要注意存储值为null
的时候
笔者就是由于存储了null
值造成生产事故,差点被开除了!详细介绍可以阅读以前文章《Java采坑记》
使用 java.util.stream.Collectors 类的 toMap()方法注意value为空时
如果项目里面就是有null值怎么办呢?可以用下面几种方法来解决:
- 过滤值为null
- 换一种写法
- 据说这个问题
java9
就修复了,所以也可以尝试升级jdk
List<Pair<String, Double>> pairArrayList = new ArrayList<>(2);
pairArrayList.add(new Pair<>("version1", 4.22));
pairArrayList.add(new Pair<>("version2", null));
// 第一种过滤值为null的
Map<String, Double> map = pairArrayList.stream().filter(p-> Objects.nonNull(p.getValue())).collect(
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
System.out.println(map.toString());
// 换一种实现方式
LinkedHashMap<Object, Object> collect = pairArrayList.stream().collect(LinkedHashMap::new, (m, v) -> m.put(v.getKey(), v.getValue()), LinkedHashMap::putAll);
System.out.println(collect.toString());
输出结果
{version1=4.22}
{version1=4.22, version2=null}
这个方法还有一个坑如果key相同也会抛异常,感兴趣的同学可以动手试试。
使用 Collection 接口任何实现类的 addAll()方法时,都要对输入的集合参数进行NPE 判断。
三目运算符可能产生NPE
那么如何有效的避免NPE呢
- 使用对象或者集合之前记得先判空。
- 使用JDK一些API的方法记得要点进源码去大概看看,不要随便拿来就用。
- 单元测试要对空值进行测试,保证程序的健壮性。
- 合理的使用
JDK1.8
提供的Optional
来避免NPE
。 - 提供接口时候需要对非空参数进行说明,并且对非空参数进行校验,不要太相信调用者。
- 调用接口的时候一定要对接口返回值进行判空,不要太相信接口提供者。(这个肯定会有值的)。
- 小心使得万年船
结束
- 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
- 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
- Serverless 应用开发指南:serverless 的 hello, world
- 通过一组RESTful API暴露CQRS系统功能
- 通过使用结构化数据 JSON-LD,我为网站带来了更多的流量
- 使用 OWIN Self-Host ASP.NET Web API 2
- c#开源消息队列中间件EQueue 教程
- Serverless 框架 OpenWhisk 开发指南:使用 Node.js 编写 hello, world
- GOTO Berlin: Web API设计原则
- 使用 ServiceStack 构建跨平台 Web 服务
- 使用 OpenWhisk 自建 Serverless 服务
- 如何在 8 小时内开发上线一个在线表单系统
- 让Response.Redirect页面重定向更有效率
- 使用 adr 轻松创建 “程序员友好” 的轻量级文档
- 在Linux和Windows平台上操作MemoryMappedFile(简称MMF)
- android来电归属地提醒
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 详解Linux文件系统:ext4及更高版本
- Linux设置虚拟内存的教学与实战教程
- 详解Linux服务器状态、性能相关命令
- 【s3cmd】给s3cmd加点debug日志再编一个
- Linux获取当前脚本真实路径的方法
- 短视频商城源码,商城左侧菜单栏网页模板
- 谈一谈Linux系统重要的子目录问题
- Tensorflow 2.x Java api的maven包怎么找
- Linux上也有10个流行的Windows应用程序
- (译)SDL编程入门(14)动画精灵和VSync
- 在Ubuntu Linux上安装和使用Git和GitHub
- 10个好用的 HTML5 特性
- Linux下升级python和安装pip的详解
- Linux中使用top命令的技巧
- Linux中查找工具的友好替代方案