superleeyom / blog

:bookmark: 个人博客仓库,用于记录一些幼稚的想法和脑残的瞬间,欢迎 star、watch,该仓库为个人博客,请不要提 issue ,该仓库后端参考了 @yihong0618 的 gitblog 项目,前端参考了@LoeiFy 的 Mirror 项目,感谢!
https://blog.leeyom.top
221 stars 20 forks source link

Java空指针避坑指南 #35

Open superleeyom opened 3 years ago

superleeyom commented 3 years ago

把阿里巴巴的《Java开发手册》里,关于 NPE异常总结了下,预防空指针异常,从你我做起!


【强制】所有的 POJO 类属性必须使用包装数据类型。

数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。


【强制】在使用 java.util.stream.Collectors 类的 toMap()方法转为 Map 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。

List<Pair<String, double>> pairArrayList = new ArrayList<>(2);
pairArrayList.add(new Pair<>("version1", 8.3));
pairArrayList.add(new Pair<>("version2", null));
// 抛出 NullPointerException 异常
pairArrayList.stream().collect(Collectors.toMap(Pair::getKey, Pair::getValue));

因为 HashMap 的 merge 方法里,若 value 为 null,则会抛 NPE:

@Override
public V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
  if (value == null)
    // 抛出 NullPointerException 异常
    throw new NullPointerException();
  //....
}

这个问题java9已经修复,所以也可以尝试升级jdk,或者把value为null的过滤掉就行。另外注意:如果key相同也会抛异常 ,改成如下代码相同的key就不会报异常,新key的value会替换旧key的value:

pairArrayList.stream().collect(Collectors.toMap(Pair::getKey, Pair::getValue,(v1, v2) -> v2))

【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一 致、长度为 0 的空数组。

List<String> list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
// array 的值为:["guan","bao",null,null]
String[] array = list.toArray(new String[4]);

如果循环遍历 array 的时候,不注意,就有可能抛 NPE:

for (String s : array) {
  // 抛出 NullPointerException 异常
  System.out.println(s.length());
}

所以建议数组空间大小的 length 设置为0,动态创建与 list size 相同的数组,性能最好。

String[] array = list.toArray(new String[0]);

【强制】在使用 Collection 接口任何实现类的 addAll()方法时,都要对输入的集合参数进行 NPE 判断。

List<String> list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
List<String> listSecond = null;
// 抛出 NullPointerException 异常
list.addAll(listSecond);

观察 ArrayList 的 addAll 方法的源码:

public Boolean addAll(Collection<? extends E> c) {
  // 若c为null,这里会抛NPE
  Object[] a = c.toArray();
  int numNew = a.length;
  ensureCapacityInternal(size + numNew);
  // Increments modCount
  System.arraycopy(a, 0, elementData, size, numNew);
  size += numNew;
  return numNew != 0;
}

所以正确的做法是,对输入参数做判空化处理:

// CollUtil.emptyIfNull(List<T> set)方法是 hutool里面的对集合null设置为empty的方法,方法内实际为:
// (null == set) ? Collections.emptyList() : set
list.addAll(CollUtil.emptyIfNull(listSecond));

【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况。


【强制】三目运算符(condition? 表达式 1 : 表达式 2) 中,高度注意表达式 1 和 2 在类型对齐时,可能抛出因自动拆箱导致的 NPE 异常。

Integer a = 1;
Integer b = 2;
Integer c = null;
Boolean flag = false;
// a*b的结果是int类型,那么c会强制拆箱成int类型,抛出NPE异常
Integer result = (flag ? a * b : c);

a*b 包含了算术运算,因此会触发自动拆箱过程(会调用 intValue 方法),此时a*b的结果是 int 类型,那么c会强制拆箱成 int 类型,以下两种场景会触发类型对齐的拆箱操作:

  1. 表达式 1 或表达式 2 的值只要有一个是原始类型。

  2. 表达式 1 或表达式 2 的值的类型不一致,会强制拆箱升级成表示范围更大的那个类型。


【强制】当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为 NULL,因此使用 sum()时需注意 NPE 问题。

可以使用如下方式来避免 sum 的 NPE 问题:SELECT IFNULL(SUM(column), 0) FROM table;


【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说 明什么情况下会返回 null 值。

防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。


【强制】当 switch 括号内的变量类型为 String 并且此变量为外部参数时,必须先进行 null 判断。

public static void method(String param) {
    switch(param) {
        case "sth":
            System.out.println("it's sth");
            break;
        case "null":
            System.out.println("it's null");
            break;
        default:
            System.out.println("default");
    }
}

public static void main(String[] args) {
    // 会报NPE异常
    method(null);
}

【强制】字符串判断相等,常量一定要写在前面「这个手册上没有,我自己加的,哈哈」

String str1 = null;
String str2 = "hello world";
// 错误的写法,会抛NPE
str1.equals(str2);
// 正确写法
str2.equals(str1);

其他的可能产生 NPE 的场景:

  1. 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE:
public int f() {
  Integer a = null;
  // 抛出NPE异常
  return a;
}

    另外这种情况也会自动拆箱产生NPE:

Integer num = null;
// 抛出NPE异常
if(num > 1) {
  // do someing
}
  1. 数据库的查询结果可能为 null:
// 数据库查询用户信息
User user = userMapper.getById(123);
// user可能为null,抛出NPE异常
String userName = user.getUserName();
  1. 远程调用返回对象时,一律要求进行空指针判断,防止 NPE:
ApiResponse<UserInfoDTO> apiResponse = userFeign.getById(123);
// user可能为null
User user = apiResponse.getData();
if(user!=null){
  String userName = user.getUserName();
}
  1. 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。

  2. 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。

  3. 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null,因为 List 里面也可以存 null。

可以考虑使用 JDK8 的 Optional 类来防止 NPE 问题。