Java 空指针场景

230 阅读13分钟

1. 自动拆箱出现 null

包装器类型自动拆箱为基础类型时极容易出现NPE。如下图示例,方法void initTask(int taskId),调用时taskId如果为 null,则会出现NPE。正确做法是 可能为 null 的属性,一律声明为包装器,此外从外部获取的变量一定要检查 null,进行防御式编程。

2. 遍历集合 出现 null

集合List支持 foreach 遍历,但是如果List变量为null,则一定会发生空指针异常 NPE。如下代码所示,当ids为空时,会发生NPE。

正确做法,在遍历List之前,一定要进行空值检查。


List<Integer> ids = extractFromRequest();
//如果ids为 null,则发生 NPE
for(Integer id: ids){
   //do something
}

3. 集合数组中出现 null

此外集合 List 中对象可能为空,在遍历集合时,要检查集合中的对象是否为 Null,否则可能发生 NPE。

 
List<Integer> ids = extractFromRequest();
//如果ids为 null,则发生 NPE
for(Integer id: ids){
   // id可能为 null,则会发生 NPE
   System.out.println(id.toString());
}

4. Optional.of() 出现 null

Optional 是 Java 8 提供的一个工具类,它可以以更加优雅的方式来处理空值。在使用 Optional 类的时候,有两种初始化方式,分别是 Optional.of(object) Optional.ofNullable(object)。需要注意的是,在使用 Optional.of(object) 的时候,如果 object 为 null,就会发生空指针异常NPE。

正确做法:使用 Optional.ofNullable();

5. Stream和 Lamada中出现 null

若在lambda表达式中出现了Null,就可能会发生空指针异常 NPE。具体示例如下图所示,当使用Stream.map方式进行映射时,可能会导致返回值为Null。

正确做法:filter(Objects::nonNull) 过滤为 Null 对象

6. json 解析出现 null

使用 fastjson 可能遇到解析的对象为 null 的情况。很多业务系统使用 json 存储扩展字段,一般情况下mysql 字段默认值为""或 null,这种情况使用 FastJson 解析时,就会解析出 null对象,不判空就会出现空指针异常。

如下代码所示,当传入的json 字符串为空值""时,解析出的 json 对象为 Null。

7. 打印日志使用 + 号出现 null

日志打印时,很多人习惯使用 + 加号拼接日志,这种情况可能导致 空指针异常。

正确做法:使用{}占位符方式,打印变量。 日志框架会进行判空,当变量出现 Null 时,不会出现空指针。

很多人在 review 代码时,不重视日志代码,容易忽略日志代码中的问题。曾经有个同事搞出的线上问题就是因为日志打印出现了 NPE。

一定要敬畏每一行代码,包括日志代码。

8. 返回值异常处理

如下所示,业务系统在调用 Rpc 后,会对结果判空和检查异常码。如果调用失败,则会上报异常码。然而这两者不能完全放在一起,因为当 result 为 null 时,上报异常码会导致空指针异常的发生。

 
RpcResult result = invokeRpcMethod();
if(result==null || result.getCode!=SUCCESS){
     log.error("调用失败 result:{}", result);
     //当 result 为空时,则一定发生空指针。
     reportCode(result.getCode());
}

一、 对象判空

if (obj != null) { // 进行对象非空判断 }

Object obj = null; // 或者 obj = new Object(); if (obj == null) { // 对象为空 } 另外,Guava 库还提供了一个更方便的方法,使用方式如下 import com.google.common.base.Objects;

if (Objects.isNull(obj)) { // 对象为空 }

二、 字符串判空

List list = new ArrayList<>(); if (list == null || list.size() == 0) { // list 为空 } 简单,但需要写太多代码判断,会显得有些麻烦。或 使用 Apache Commons Lang3 提供的 判断字符串是否为空或者只包含空格字符。这种方式使用起来非常方便: import org.apache.commons.lang3.StringUtils;

if (StringUtils.isBlank(str)) { // 字符串为空 } 还可采用  Java 8 引入的 String#isEmpty() 方法判断字符串是否为空字符串: if (str == null || str.isEmpty()) { // 字符串为空 } 还有 Java 11 引入的 String#isBlank() 方法判断字符串是否为空或者只包含空格字符: if (str == null || str.isBlank()) { // 字符串为空 } 若还要 判断字符串是否为空或null: String str = null; // 或者 str = ""; if (str == null || str.length() == 0 || str.trim().length() == 0) { // 字符串为空或null } 也可以使用 Apache Commons Lang3 库中的 StringUtils 工具类中的 isBlank() 方法进行判断: import org.apache.commons.lang3.StringUtils;

if (StringUtils.isBlank(str)) { // 字符串为空或null或只包含空白字符 } 直接使用 == 判断是否为 null,或使用 equals() 判断是否等于空字符串 "": String str = null; if (str == null || str.equals("")) { // 字符串为空 } 使用 isEmpty() 方法判断字符串是否为空字符串 "": String str = ""; if (str.isEmpty()) { // 字符串为空 } 使用 isBlank() 方法判断字符串是否为空或全是空格: String str = " "; if (StringUtils.isBlank(str)) { // 字符串为空或全是空格 } 使用正则表达式判断字符串是否为空或只包含空格: String str = " "; if (str.matches("\s*")) { // 字符串为空或只包含空格 } 需要注意的是,第三种方法需要导入 StringUtils 类,而第四种方法虽然不需要导入任何类,但比较麻烦,建议使用前三种方法中的一种。 三、 List 判空

    使用 if (list == null || list.isEmpty()) 判断 List 是否为 null 或者是否为空列表。或使用 Java 8 引入的 isEmpty() 方法判断 List 是否为 null 或者是否为空列表:但注意 isEmpty() 方法只会判断集合是否为空,而不会判断集合是否为 null。因此,使用 Collection#isEmpty() 方法之前,要先判断集合是否为 null。如果为 null,则该方法会抛出空指针异常。正确的使用方法如下:

List list = ...; if (list != null && !list.isEmpty()) { // 先判断 list 是否为 null,再判断 list 是否为空 // 进行操作 }

if (list == null || list.isEmpty()) { // List 为空 } 上述代码中,先使用 list!=null 来判断 list 是否为 null,如果是,则不会进入条件语句中。如果不是,则使用 !list.isEmpty() 来判断 list 是否为空,如果不为空,则进入条件语句中进行操作。 ## 四、Map 判空 Map<String, String> map = null; // 或者 map = new HashMap<>(); if (map == null || map.isEmpty()) { // Map为空或null } private void putInfoToMap(Map<String,String> map ,String key ,String value){
if(StringUtils.isEmpty(key)){ return; } if(StringUtils.isEmpty(value)){ map.put(key, " "); }else{ map.put(key, value); } }

4069ca39a980bf201ebe47900ba4655e.png ## 五、数组判空 String[] array = null; // 或者 array = new String[10]; if (array == null || array.length == 0) { // 数组为空或null }

六、Set判空

Set<String> set = null; // 或者 set = new HashSet<>();

if (set == null || set.isEmpty()) { // Set为空或null } 在判断集合、数组、对象是否为空时,进行非空判断时最好先判断对象是否存在,否则在对象不存在的情况下操作对象可能会导致空指针异常的发生。 ## 七、文件判空 public SalaryExcelOperatVo uploadExcel(MultipartFile multipartFile) {

    if (multipartFile==null) {
        log.error("文件不能为空");
         throw new RuntimeException("上传Excel文件内容为空,请重新上传!");
    }
    String fileName = multipartFile.getOriginalFilename();
    //判断文件是否是excel文件
    assert fileName != null;
    if (!fileName.endsWith("xls") && !fileName.endsWith("xlsx")) {
        log.error(fileName + "不是Excel文件!");
        throw new RuntimeException(fileName + "不是Excel文件!");
    }

    //保存文件到本地
    File dir1 = new File("/roots/uploadFile/xzExcel");
    if (!dir1.exists()) {
        dir1.mkdirs();
    }
    //统一日期格式
    LocalDateTime current = LocalDateTime.now();
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    String formatted = current.format(formatter);
    //加上三位随机数
    Random random = new Random();
    int end3 = random.nextInt(999);

    File file1 = new File(dir1.getAbsolutePath() + File.separator + formatted + "-" + end3 + "-" + multipartFile.getOriginalFilename());
    try {
        multipartFile.transferTo(file1);
    } catch (IOException e) {
        e.printStackTrace();
    }
    log.info("【上传薪资Excel文件已保存到本地:{}】",file1.getAbsolutePath());

    //创建返回对象SalaryExcelOperatVo的实例化对象: result
    SalaryExcelOperatVo result = new SalaryExcelOperatVo();
    //获取excel文件sheet1 的内容
    ArrayList<InSalary> inSalaries1 = readExcel1(file1.getAbsolutePath());

    ArrayList<SalaryStaffPerOneListVo> vo1 = new ArrayList<>();

    SalaryStaffPerOneListVo oneListVo ;

    for(InSalary inSalary1:inSalaries1){
        oneListVo = new SalaryStaffPerOneListVo();
        BeanUtils.copyProperties(inSalary1,oneListVo);
        vo1.add(oneListVo);

} result.setSheetOne(vo1);

    //获取excel文件sheet2 的内容
    ArrayList<InSalary> inSalaries2 = readExcel2(file1.getAbsolutePath());

    ArrayList<SalaryStaffPerTwoListVo> vo2 = new ArrayList<>();

    SalaryStaffPerTwoListVo twoListVo ;
    for(InSalary inSalary2:inSalaries2){
        twoListVo = new SalaryStaffPerTwoListVo();
        BeanUtils.copyProperties(inSalary2,twoListVo);
        vo2.add(twoListVo);
    }

    result.setSheetTwo(vo2);

    return result;
}
八、StringUtils 工具类

    StringUtils 提供了许多字符串操作相关的方法,其中比较常用的包括 isEmpty、isNotEmpty、isBlank 和 isNotBlank,它们的作用如下:

isEmpty:判断字符串是否为 null 或长度是否为 0;
isNotEmpty:判断字符串是否不为 null 且长度不为 0;
isBlank:判断字符串是否为 null、空字符串或全为空格字符;
isNotBlank:判断字符串是否不为 null、不为空字符串且不全为空格字符。

isEmpty 和 isNotEmpty 是根据字符串的长度来判断的,而 isBlank 和 isNotBlank 还包括对空格字符的处理。另外,如果字符串为 null,无论使用哪个方法都会返回 true。

以下是 StringUtils.isEmpty、StringUtils.isNotEmpty、StringUtils.isBlank 和 StringUtils.isNotBlank 的源码实现(相对简化),可以更好地理解它们的区别: public class StringUtils { // 判断字符串是否为空(长度是否为 0) public static boolean isEmpty(CharSequence cs) { return cs == null || cs.length() == 0; }

// 判断字符串是否不为空(长度是否不为 0)
public static boolean isNotEmpty(CharSequence cs) {
    return !isEmpty(cs);
}

// 判断字符串是否为空白(包括 null、空字符串和全为空格字符)
public static boolean isBlank(CharSequence cs) {
    int length;
    if (cs == null || (length = cs.length()) == 0) {
        return true;
    }
    for (int i = 0; i < length; i++) {
        if (!Character.isWhitespace(cs.charAt(i))) {
            return false;
        }
    }
    return true;
}

// 判断字符串是否不为空白(不包括 null、空字符串和全为空格字符)
public static boolean isNotBlank(CharSequence cs) {
    return !isBlank(cs);
}

} private void validChoiceInfo(InSalary data, AnalysisContext context) {

if(isBlank(data.getUserName())){
    throw new ExcelAnalysisException(String.format("上传失败:第%s行员工姓名为空",context.readRowHolder().getRowIndex()));
}
if(isBlank(data.getIdNumber())){
    throw new ExcelAnalysisException(String.format("上传失败:第%s行员工身份证信息为空",context.readRowHolder().getRowIndex()));
}

}

private void validChoiceInfo(InSalary data, AnalysisContext context) {

if(StrUtil.isBlank(data.getUserName())){ throw new ExcelAnalysisException(String.format("上传失败:第%s行员工姓名为空",context.readRowHolder().getRowIndex())); } if(StrUtil.isBlank(data.getIdNumber())){ throw new ExcelAnalysisException(String.format("上传失败:第%s行员工身份证信息为空",context.readRowHolder().getRowIndex())); } } 这两段代码实现的功能是一样的,都是对员工姓名和身份证信息进行非空判断,并在为空的情况下抛出异常。不同之处在于第一段代码使用了 isBlank() 方法,而第二段则使用了 StrUtil.isBlank() 方法。isBlank() 方法是在 JDK 11 中新增的,能够判断字符串是否为空或全为空格,因此不需要再使用 trim() 方法进行判断。而 StrUtil.isBlank() 方法是在 Hutool 库中提供的,功能与 isBlank() 类似,但需要将 Hutool 库引入项目中才能使用。另外,两段代码还有一个细微的差别。第一段代码使用了 if(isBlank(data.getUserName())) 的形式进行判断,而第二段代码使用了 if(StrUtil.isBlank(data.getUserName())) 的形式。这两种写法的效果是一样的,只是调用方法的方式略有不同。

或者自定义一个判空工具类(根据具体具体业务逻辑可以进行修改) import java.util.Collection; import java.util.Map;

public class NotNullCheck {

public NotNullCheck() {
}

public static boolean str(String str) {
    return str != null && !str.isEmpty() && str.length() >= 1 && !"".equals(str.replaceAll(" ", ""));
}

public static boolean array(Collection list) {
    return list != null && !list.isEmpty() && list.size() != 0;
}

public static boolean map(Map map) {
    return map != null && !map.isEmpty() && map.size() != 0;
}

}


### 一、数据判空

开发中判空时推荐使用工具

库:StringUtils、CollectionUtils、ArrayUtils、Objects、NumberUtils

1、字符串判空

// 方式1:判断是否为 null 或空字符串
if (str == null || str.isEmpty()) {}

// 方式2:判断是否为 null 或空字符串
if (str == null || str.length() == 0) {}

// 方式3:判断是否为 null 或空字符串
if (str == null || str.equals("")) {}

// 方式3:判断是否为 null 或空字符串或空格或空白符
if (str == null || str.trim().length() == 0) {}

推荐使用:

// 方式1:判断是否为 null 或空字符串
if (StringUtils.isEmpty(str)) {}

// 方式2:判断是否不为 null 或空字符串
if (StringUtils.isNotEmpty(str)) {}

// 方式3:判断是否有任意一个为 null 或空字符串
if (StringUtils.isAnyEmpty(str, str2, str3)) {}

// 方式4:判断是否全部都不为 null 或空字符串,跟 isAnyEmpty 相反,可以用来做表单必填参数校验
if (StringUtils.isNoneEmpty(str, str2, str3)) {}

// 方式5:判断是否为 null 或空字符串或空格或空白符
if (StringUtils.isBlank(str)) {}

// 方式6:判断是否不为 null 或空字符串或空格或空白符
if (StringUtils.isNotBlank(str)) {}

// 方式7:判断是否有任意一个为 null 或空字符串或空格或空白符

if (StringUtils.isAnyBlank(str, str2, str3)) {}

// 方式8:判断是否全部都不为 null 或空字符串或空格或空白符,跟 isAnyBlank 相反,可以用来做表单必填参数校验
if (StringUtils.isNoneBlank(str, str2, str3)) {}

2、Integer 判空

基本数据类型 int 是不能为 null 的,只有包装类型 Integer 才能赋值为 null

if (integer == null || integer.equals(0)) {}

if (integer == null || integer.intValue() == 0) {}

推荐使用:

if (NumberUtils.isNullorZero(number)) {}

3、对象判空

if (obj == null) {}

if (obj != null) {}

推荐使用:

if (Objects.isNull(obj)) {}

if (Objects.nonNull(obj)) {}

4、List判空

// 方式一
if (list == null || list.size() == 0) {}

// 方式二
if (list == null || list.isEmpty) {}

推荐使用:

if (CollectionUtils.isEmpty(list)) {}

5、Map判空

// 方式一
if (map == null || map.size() == 0) {}

// 方式二
if (map == null || map.isEmpty) {}

推荐使用:

if (CollectionUtils.isEmpty(map)) {}

6、Set判空

// 方式一
if (set == null || set.size() == 0) {}

// 方式二
if (set == null || set.isEmpty) {}

推荐使用:

if (CollectionUtils.isEmpty(set)) {}

7、数组判空

注意 java 中数组长度是不可变的,而且只能储存同一种类型的数据,length 是数组的一个长度属性并不是方法,表示当前数组可以储存多少个长度的数据。字符串 String 的 length() 是一个方法,返回的是字符串的长度。集合类型的长度是要用 size 方法去获取,集合没有 length 属性。

if (array == null || array.length == 0) {}

推荐使用:

if (ArrayUtils.isEmpty(array)) {}

常见疑问:

  • CollectionUtils.isEmpty 和 Objects.isNull 的区别

前者判断集合是否为 null 或空集合,后者仅判断是否为 null

  • CollectionUtils.isEmpty 和 == null 的区别

前者判断集合是否为 null 或空集合,后者仅判断是否为 null

  • Objects.isNull 和 == null 的区别

两者是等价的,可以看 Objects.isNull 的源码

public static boolean isNull(Object obj) {
    return obj == null;
}

二、数据比较

基本数据类型比较

double、float、long、int、short、byte、char、boolean 这8种基本数据类型比较,可以直接用 == 进行比较

int age = 10;
double myAge = 18.5;
if (myAge > age) {} // true

引用数据类型比较

引用数据类型直接用 == 或用 equals 方法比较的都是引用地址是否相等,不过注意字符串 String 因为重写了 equals 方法是个例外。

字符串比较

java 中字符串是引用数据类型,并不能直接像 javascript 中那样直接用 == 判断是否相等,而是需要使用 equals 方法去判断是否相等:

String xiaoMing = "xiaoming";
String xiaoHong = "xiaoming";

if (xiaoMing == xiaoHong) {} // true
if (xiaoMing.equals(xiaoHong)) {} // true

上面的比较很多人会纳闷为什么用 == 直接比较两个字符串也是 true,那是不是说字符串我们也是可以直接用 == 去比较的,这个就涉及字符串常量池

了,实际我们换一种赋值方式你就能发现又不等了:

String xiaoMing = "xiaoming";
String xiaoHong = new String("xiaoming");

if (xiaoMing == xiaoHong) {} // false
if (xiaoMing.equals(xiaoHong)) {} // true

只用记住字符串你就用 equals 去比较值是否相等就行了。

三、类型转换

自动类型转换(显示转换)

范围小的转成大的,不会有任何问题,java 会自动为我们进行转换:double -> float -> long -> int -> short -> byte,注意byte、short和char不能相互转换

int age = 18;
double money = age;

强制类型转换

隐式转换

范围大的转成小的,直接赋值是会报错的,但是我们可以用小括号语法强制类型转换,但是这可能会导致数据精度损失或溢出:

double pi = 3.1415926;
int num = (int) pi; // 3,小数部分会丢失

数值类型转字符串

一般用于给前端返回的数据时常用到,可以用 String.valueOf 方法,或者直接用 + 加上空字符串:

int age = 18;
String userAge = String.valueOf(age);
String userAge2 = age + "";

字符串转数值

一般需要用于数学运算时,比如我们从其他地方获取到的数据是数字字符串,我们需要对起进行加减乘除必须要先转成数值类型,这时可以利用数值类型提供的方法来进行转换 Double.parseDouble、Float.parseFloat、Long.parseLong、Integer.parseInt()、Short.parseShort、Byte.parseByte,不过实际开发中要注意自己判空和捕获转换异常,推荐使用第三方库来转换:

String userAge = "18";
int age = Integer.parseInteger(age) * 3;