新手业务编码中常见的null值和空指针处理

背景

大家都知道,使用对象的时候,由于对象的默认值为null, 如果没有及时判空就去调用对象的方法,可能会带来空指针异常的问题。本篇将会讲解空指针异常容易在哪些情况下出现,新手应该如何去避免无处不在的null值问题,又应该如何修复。主要举一些常见的例子来配合说明。

1、自动拆箱导致的空指针异常

首先,自动拆箱有两种场景,第一种是包装类型的变量赋值给基本类型的变量时,会出现自动拆箱;

Integer num = 10; // 自动装箱
int value = num; // 自动拆箱
System.out.println(value); // 输出:10

第二种是基本类型作为方法的形参,而这个方法被调用时,传入的参数是包装类的变量,就会发生自动拆箱,如下所示:

public class AutoUnboxExample {
    public static void printInt(int num) {
        System.out.println(num);
    }

    public static void main(String[] args) {
        Integer number = new Integer(20); // 创建一个Integer对象
        printInt(number); // 自动拆箱:将Integer对象转换为int类型
    }
}

那自动拆箱什么时候会发生空指针异常呢?异常的原因是什么?
首先,赋值的时候,

Integer num = null; // 自动装箱
int value = num; // 自动拆箱, 将null 值赋给了value。此时必定会报空指针异常。

javap 得到字节码文件

Compiled from "NullpointerExceptionTest.java"
public class com.example.demo3.commonpitfalls.service.NullpointerExceptionTest {
  public com.example.demo3.commonpitfalls.service.NullpointerExceptionTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: aconst_null
       1: astore_1
       2: aload_1
       3: invokevirtual #2                  // Method java/lang/Integer.intValue:()I
       6: istore_2
       7: return
}

可以发现,字节码文件中有一个Integer.intValue() 这样的调用,对象为null, 用null调用这个方法, 所以才会产生空指针异常。
这边给出规避自动拆箱引发空指针的建议:
1、包装类类型和基本数据类型都可以的业务场景下,优先考虑使用基本类型。
2、对于不确定的包装类类型,一定要对NULL情况做检验和判断。
3、对于值为NULL的包装类类型,建议可以试着赋值为0;当然了,也要注意NULL和0代表的业务含义是否是一样的。

2、字符串比较出现空指针异常

        String str1 = "hello";
        String str2 = null;
        // 下面这行代码会导致空指针异常,因为str2为null
        int result = str1.compareTo(str2);

因为compareTo()方法会调用length(), 当传入的对象为null, 会引发空指针异常。

3、并发容器规定不允许put null值时,强行put会引发空指针异常

如ConcurrentHashMap 这样的容器不支持 Key 和 Value 为 null,强行 put null 的 Key 或 Value 会出现空指针异常。

        ConcurrentHashMap map = new ConcurrentHashMap();
        map.put(null, null); // 必然引发空指针异常, 因为多线程并发环境下,put(null,null)会引发二义性问题。

其实,规定键不能为null 以及 值不能为null, 是为了避免二义性的问题。null 是一个特殊的值,表示没有对象或没有引用。拿 get 方法取值来说,返回的结果为 null 会存在两种情况:
值没有在集合中;
值本身就是 null。
如果你用 null 作为键,那么就无法区分这个键是否是存在于 ConcurrentHashMap 中或者根本没有这个键。同样,如果你用 null 作为值,那么就无法区分这个值是真正存储在 ConcurrentHashMap中的,还是因为找不到对应的键而返回了null。
另外,多线程环境下,无法使用containKey(key)来判断是否存在这个key-value, 因为判断的同时,会有其他线程对键值进行修改。
需要注意的是,HashMap 可以存储 null 的 key 和 value,但 null 作为键也只能有一个,null 作为值可以有多个。
如果传入null 作为参数,就会返回 hash 值为 0 的位置的值。单线程环境下,不存在一个线程操作该 HashMap 时,其他的线程将该 HashMap 修改的情况,所以可以通过 contains(key)来做判断是否存在这个键值对,从而做相应的处理,也就不存在二义性问题。

4、对象级联调用可能导致NPE

这种情况就是A类中包含了B类。有时候忘记对B类的实例进行判空,而导致NPE问题。

class B {
    public void methodB() {
        System.out.println("Method B called");
    }
}
@Data
class A {
    private B b;
}

public class Main {
    public static void main(String[] args) {
        B b = new B();
        A a = new A(b);

        // 没有对字段进行空值检查,直接尝试级联调用 B 对象的方法
        a.getB().methodB(); // 这里可能会导致空指针异常
    }
}

5、返回的List为null时,后调用了size(), 导致NPE

import java.util.List;

public class RemoteService {
    public List<String> getData() {
        // 模拟远程服务返回的 List 为 null
        return null;
    }
}

public class Main {
    public static void main(String[] args) {
        RemoteService remoteService = new RemoteService();
        
        List<String> data = remoteService.getData();

        // 没有对返回的 List 进行空指针检查,直接调用方法可能导致空指针异常
        int size = data.size(); // 这里可能导致空指针异常
        System.out.println("Size of data: " + size);
    }
}

以上是一些常见的可能会出现的空指针异常(NPE),下面通过一些实践代码来讲一下如何去修复NPE问题。
其实,最常见的方法就是先判个空,然后再进行对象的操作,这样可以直接避免NPE抛出。不过,这只能让异常不再出现,我们最好是找到程序逻辑中出现的空指针究竟是来源于入参还是 Bug:
如果是来源于入参,应进一步分析入参是否合理等;
如果是来源于Bug,那空指针不一定是纯粹的程序Bug,可能还涉及业务属性和接口调用规范等。总而言之,结合实际业务代码的上下文去修复问题。
如果时间有限,当然使用ifelse逻辑先判一下空是没问题的。不过,java8提出的Optional 类是专门用来消除这样的 if-else 逻辑,使用一行代码就能进行判空和处理!
接下来,写一个实践的代码,使用Optional类去修复上文提到的NPE问题。

实际例子

package com.example.demo3.commonpitfalls.service;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
 /**
     * 分别用来测试java中常见的几种场景下的NPE
     * 1、参数值是 Integer 等包装类型,使用时因为自动拆箱出现了空指针异常;
     * 2、字符串比较出现空指针异常;
     * 3、诸如 ConcurrentHashMap 这样的容器不支持 Key 和 Value 为 null,强行 put null 的
     * Key 或 Value 会出现空指针异常。
     * 4、A 对象包含了 B,在通过 A 对象的字段获得 B 之后,没有对字段判空就级联调用B的方法出现空指针异常。
     * 5、方法或远程服务返回的 List 不是空而是 null,没有进行判空就直接调用 List 的方法出现空指针异常
*/
@Slf4j
@RestController
@RequestMapping("/nullpointer")
public class NullpointerExceptionTest {
    /**
     * 代码中模拟了 5 种空指针异常
     * 对入参 Integer i 进行 +1 操作;
     * 对入参 String s 进行比较操作,判断内容是否等于"OK";
     * 对入参 String s 和入参 String t 进行比较操作,判断两者是否相等;
     * 对 new 出来的 ConcurrentHashMap 进行 put 操作,Key 和 Value 都设置为 null。
     * wrongMethod 的返回值, return null
     */
    private List<String> wrongMethod(FooService fooService, Integer i, String s, String t) {
            log.info("result {} {} {} {}",
                    i + 1,
                    s.equals("OK"), s.equals(t),
                    new ConcurrentHashMap<String, String>().put(null, null));
            if (fooService.getBarService().bar().equals("OK"))
                log.info("OK");
            return null;
    // 使用Optional类去做包装,为null时会返回一个默认值,而不是返回null,从而避免抛异常。
    private List<String> rightMethod(FooService fooService, Integer i, String s, String t) {
         // i为null 时,i的值设置为0;i不为null时,返回i的值;再进行加1。 OK放在前面进行equals,就不会出现NPE。
         log.info("result {} {} {} {}", Optional.ofNullable(i).orElse(0) + 1, "OK".equals(s), "OK".equals(t));
         // 使用Optional包装起来。如果不为null, 执行流的后续操作;如果为null, 返回空的Optional对象。
         Optional.ofNullable(fooService)
                    .map(FooService::getBarService)
                    .filter(barService -> "OK".equals(barService.bar()))
                    .ifPresent(result -> log.info("OK"));
        // 返回一个空的List, 比起返回null的好处是可以避免NPE。
         return new ArrayList<>();
    }
    // 会抛NPE的错误使用
    @GetMapping("wrong")
    public int wrong(@RequestParam(value = "test", defaultValue = "1111") String test) {
            return wrongMethod(
                test.charAt(0) == '1' ? null : new FooService(),
                test.charAt(1) == '1' ? null : 1,
                test.charAt(2) == '1' ? null : "OK",
                test.charAt(3) == '1' ? null : "OK").size();
    }
    // 不再抛NPE的正确使用
    @GetMapping("right")
    public int right(@RequestParam(value = "test", defaultValue = "1111") String test ) {
            return Optional.ofNullable(rightMethod(
                test.charAt(0) == '1' ? null : new FooService(),
                test.charAt(1) == '1' ? null : 1,
                test.charAt(2) == '1' ? null : "OK",
                test.charAt(3) == '1' ? null : "OK"))
                .orElse(Collections.emptyList()).size(); // 如果返回为null,则返回空列表。然后获取空列表的size。
    }

    class FooService {
        @Getter
        private BarService barService;
    }
    class BarService {
        String bar() {
            return "OK";
        }
    }
}

测试结果
wrong接口

right接口

posted @ 2024-09-20 16:43  heyhy  Views(61)  Comments(0Edit  收藏  举报
Title