开发规范 - 空指针异常等低级问题注意点
影响范围
可能造成一整个接口瘫痪,如果是核心接口瘫痪那么后果不堪设想,涉及到数据记录的接口则会导致客户丢失异常时间范围内的数据且无法恢复(大数据得不到推送)
字符串
空字符判断空
参考org.apache.commons.lang3.StringUtils
public static boolean isEmpty(final CharSequence cs) {return cs == null || cs.length() == 0;
}
字符串相等判断
通过String类型做方法调用,比如做equals判断时,一定要保证方法左边的字符串对象非null,右边的可以省略判断
- 有常量的话常量一定是要放到左边的
- 如果是两个变量做判断,那么左边的变量要做非空判断
用于判断对象(枚举)时,可能两个对象不是同一个包下的,导致预期应该相等但实际不相等
- 尽量避免这种同名不同包的情况
- 如果无法避免,开发中作为敏感点对待
if (channelCode.equals(com.lazada.ad.media.common.enums.ChannelCode.FACEBOOK_MPA)) {
字符串拼接
字符串拼接null值情况
- 直接用+号的话,如果被+的是一个为null的对象,那么会直接把null作为字符串拼接上
- 如果是用append的话,拼接一个null值会出现控制字异常
转字符串
- 非String类型的基本数据类型和常量使用toString做转换时也要注意非空判断
- String.valueOf(null);会报空指针异常,底层原因是该方法内存没有对null的特殊处理
字符串转为数值类型
标准代码
if (StringUtils.isNumeric(accountId) && part.compareTo(Long.parseLong(hashPart.toString())) == 0) {FbMpaSellerTokenEntity sellerToken = new FbMpaSellerTokenEntity(); }
对null操作注意点
对null强转不会空指针,强转后的结果依然是null
for循环/遍历注意点
集合为空不报错,集合为null会报错,对集合做循环前需要判断集合非空非null
循环前对集合做了非空非null判断并不意味着已经没有风险了,因为集合中如果存在为null的对象,我们从集合取出对象后对该对象做操作会被空指针异常。
集合操作注意点
集合判空
对集合操作前,一定要先做集合判空,如果使用的集合类型的引用,引用没有被初始化,默认值为null,那么一定也不要忘记后续对集合做初始化赋值
参考org.springframework.util.CollectionUtils
public static boolean isEmpty(@Nullable Collection<?> collection) {return (collection == null || collection.isEmpty());}
其中最常用的集合HashMap和ArrayList均是
public boolean isEmpty() {return size == 0;}
所以,往集合放元素的时候,一定要注意不能放入值为null的元素
存入集合元素的时机
存入对象元素,数据源来源可能如下,如果把这些数据源存入集合前,一定要做非null判断
- 从mysql,es数据库取数据
- 从redis 缓存取数据
- 从threadlocal,hashmap本地缓存取数据
- 通过依赖注入注入属性(Spring中取数据)
- new/反射/克隆/序列化反序列化创建一个对象
对象操作
对一个对象操作时,对象可能是作为入参、从redis或者本地集合缓存取出的,对其读写前一定要做对象非空判断Objects.nonNull()
尽量不要使用基本数据类型作为对象的属性类型,使用基本数据类型的封装类型,例如
public class CpasAdSetEntity {/*** 主键*/@TableId(value = "id", type = IdType.AUTO)private Long id;private Date gmtCreate;private Date gmtModified;private String accountId;private String adSetId;private String campaignId;private String productSetId;private long startTime;
}
private Long toTimestamp(String dateTimeStr) {if (StringUtils.isBlank(dateTimeStr)) {return null;}// 定义日期时间字符串的解析器,指定偏移量格式为 +HHmmDateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");// 将字符串解析为 OffsetDateTime 对象OffsetDateTime offsetDateTime = OffsetDateTime.parse(dateTimeStr, formatter);// 转换为时间戳return offsetDateTime.toInstant().toEpochMilli();}public static void main(String[] args) {cpasAdSetDO.setStartTime(toTimestamp(adSetDTO.getStartTime()));}
上述代码,如果toTimestamp方法的合法性校验失败返回null,那么cpasAdSetDO调用setStartTime方法时就会出现空指针异常
mysql注意点
单值返回可以用pojo,非单值一定要用集合
pojo
从接口入参中取出的对象
- 尽量使用pojo来做接收,事实上目前标准接口的入参都是json类型的,必须用pojo类型,入参如果是pojo类型的话默认情况下不会出现null,除非用@RequestParam/@RequestBody(required = false) 注解表示允许接受值为null的pojo,这种情况下才要对入参做判断
- String类型来获取参数,参数可能为null,对String类型的入参操作时需要提前做非空判断,对pojo类型对象如果有需要提前校验的,由此特征衍生出规范即
标准操作(读写前的判断)
if(Objects.nonNull(pojo)){}
易错:对pojo中的某个属性做非空判断前做setter和getter前一定要做非null判断
if(Objects.nonNull(pojo) && pojo.getXxx() != null)) {}
从属性中取值可以用如下写法
String fileValue = obj== null ? "" : obj.getFileValue();
List/map
自定义集合场景
集合在使用前一定要提前初始化,否则直接往里添加元素直接就会报空指针异常
从已有方法中获取集合
如果是标准方法包括mybatis中的方法中(redis方法待验证),如果集合为空那么返回的是一个空集合对象而非null对象,如果是null对象,那么放外层还得加一个集合非空的判断,嫌啰嗦不加非空判断,那么在对集合做遍历、取值的时候会容易出现空指针
集合操作
- 生成集合Array.asLsit(“xxx”)
- 集合stream操作
- 集合addAll操作
- 集合循环
如果集合为null时,无论集合是操作树还是被操作数,都会爆空指针
场景:对集合遍历或从集合中取值的时候
注意点:先判断非空,再去判断集合中是否没有元素
if(ocrmUserList != null && ocrmUserList.size() != 0)
官方写法
public static boolean isEmpty(Collection coll) {return coll == null || coll.isEmpty();}例如list集合的isEmpty方法就是判断list.size是否为0
Arrays
if (null != arrs && arrs.length > 0) {}
总结
- 对任何对象(从request(前端、aop)中,从缓存/数据库中拿)拿出来时、做读写getter/setter处理操作的时候,都把它当成空的对待,先做非空判断和空值处理
- 不要把null作为方法参数,至少传入一个空对象,一个方法被调用十几次(复用性角度),其中只有一个参数为null,极易出现空指针异常
switch语句
- switch选择参数不能为null,需要做非空判断
- 除非特殊要求,否则每个case执行完毕要及时break
- 设置默认情况
标准switch语句
ApiConfigViewFuncEnum apiConfigViewFuncEnum = ApiConfigViewFuncEnum.get(type);if (null == apiConfigViewFuncEnum) {return null;}switch (apiConfigViewFuncEnum) {case SUB_STRING:result = subString(nodeModel, context);break;case SPLIT:result = split(nodeModel, context);break;case SORT:result = sort(nodeModel, context);break;case JOIN:result = join(nodeModel, context);break;default:break;}
mysql修改数据
修改数据时一般都会先查再改,如果某些公司测试环境和线上环境公用一套数据库,那么测试的时候除非能确认只改一个,例如对于mybatis-plus中的one筛选出唯一一个,否则要先确认查询数据范围没问题,再去做修改,防止成批量的误改