2024-03-13
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/series/article/1472795532

null 何错之有?

对于 Java 程序员而言,NullPointerException 是最令我们头疼的异常,没有之一 ,大明哥相信到这篇文章为止一定还有不少人在写下面这段代码:

if (obj != null) {
   //...
}

NullPointerException 是 Java 1.0 版本引入的,引入它的主要目的是为了提供一种机制来处理 Java 程序中的空引用错误。空引用(Null Reference)是一个与空指针类似的概念,是一个已宣告但其并未引用到一个有效对象的变量。它是伟大的计算机科学家Tony Hoare 早在1965年发明的,最初作为编程语言ALGOL W的一部分。嗯,就是这位老爷子

1965年,老爷子 Tony Hoare 在设计ALGOL W语言时,为了简化ALGOL W 的设计,引入空引用的概念,他认为空引用可以方便地表示“无值”或“未知值”,其设计初衷就是要“通过编译器的自动检测机制,确保所有使用引用的地方都是绝对安全的”。但是在2009年,很多年后,他开始为自己曾经做过这样的决定而后悔不已,把它称为“一个价值十亿美元的错误”。实际上,Hoare的这段话低估了过去五十年来数百万程序员为修复空引用所耗费的代价。因为在ALGOL W之后出现的大多数现代程序设计语言,包括Java,都采用了同样的设计方式,其原因是为了与更老的语言保持兼容,或者就像Hoare曾经陈述的那样,“仅仅是因为这样实现起来更加容易”。

在 Java 中,null 会带来各种问题(摘自:《Java 8 实战》):

  • 它是错误之源。 NullPointerException 是目前Java程序开发中最典型的异常。它会使你的代码膨胀。
  • 它让你的代码充斥着深度嵌套的null检查,代码的可读性糟糕透顶。
  • 它自身是毫无意义的。 null自身没有任何的语义,尤其是是它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模。
  • 它破坏了Java的哲学。 Java一直试图避免让程序员意识到指针的存在,唯一的例外是:null指针。
  • 它在Java的类型系统上开了个口子。 null并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题, 原因是当这个变量被传递到系统中的另一个部分后,你将无法获知这个null变量最初赋值到底是什么类型。

Java 做了哪些努力?

Java 为了处理 NullPointerException 一直在努力着。

  • Java 8 引入 Optional:减少 null而引发的NullPointerException异常
  • Java 14 引入 Helpful NullPointerExceptions:帮助我们更好地排查 NullPointerException

Java 8 的 Optional

Optional 是什么

Optional 是 Java 8 提供了一个类库。被设计出来的目的是为了减少因为null而引发的NullPointerException异常,并提供更安全和优雅的处理方式。

Java 中臭名昭著的 NullPointerException 是导致 Java 应用程序失败最常见的原因,没有之一,大明哥认为没有一个 Java 开发程序员没有遇到这个异常。为了解决 NullPointerException,Google Guava 引入了 Optional 类,它提供了一种在处理可能为null值时更灵活和优雅的方式,受 Google Guava 的影响,Java 8 引入 Optional 来处理 null 值。

在 Javadoc 中是这样描述它的:一个可以为 null 的容器对象。所以 java.util.Optional<T> 是一个容器类,它可以保存类型为 T 的值,T 可以是实际 Java 对象,也可以是 null

Optional API 介绍

我们先看 Optional 的定义:

public final class Optional<T> {

    /**
     * 如果非空,则为该值;如果为空,则表示没有值存在。
     */
    private final T value;
    
    //...
 }

从这里可以看出,Optional 的本质就是内部存储了一个真实的值 T,如果 T 非空,就为该值,如果为空,则表示该值不存在。

构造 Optional 对象

Optional 的构造函数是 private 权限的,它对外提供了三个方法用于构造 Optional 对象。

Optional.of(T value)

    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }
    
    private Optional(T value) {
        this.value = Objects.requireNonNull(value);
    }

所以 Optional.of(T value) 是创建一个包含非null值的 Optional 对象。如果传入的值为null,将抛出NullPointerException 异常信息。

Optional.ofNullable(T value)

    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }

创建一个包含可能为null值的Optional对象。如果传入的值为null,则会创建一个空的Optional对象。

Optional.empty()

    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }
    
    private static final Optional<?> EMPTY = new Optional<>();

创建一个空的Optional对象,表示没有值。

检查是否有值

Optional 提供了两个方法用来检查是否有值。

isPresent()

isPresent() 用于检查Optional对象是否包含一个非null值,源码如下:

    public boolean isPresent() {
        return value != null;
    }

示例如下:

User user = null;
Optional<User> optional = Optional.ofNullable(user);
System.out.println(optional.isPresent());
// 结果......
false

ifPresent(Consumer<? super T> action)

该方法用来执行一个操作,该操作只有在 Optional 包含非null值时才会执行。源码如下:

    public void ifPresent(Consumer<? super T> consumer) {
        if (value != null)
            consumer.accept(value);
    }

需要注意的是,这是 Consumer,是没有返回值的。

示例如下:

User user = new User("xiaoming");
Optional.ofNullable(user).ifPresent(value-> System.out.println("名字是:" + value.getName()));

获取值

获取值是 Optional 中的核心 API,Optional 为该功能提供了四个方法。

get()

get() 用来获取 Optional 对象中的值。如果 Optional 对象的值为空,会抛出NoSuchElementException异常。源码如下:

    public T get() {
        if (value == null) {
            throw new NoSuchElementException("No value present");
        }
        return value;
    }

orElse(T other)

orElse() 用来获取 Optional 对象中的值,如果值为空,则返回指定的默认值。源码如下:

    public T orElse(T other) {
        return value != null ? value : other;
    }

示例如下:

User user = null;
user = Optional.ofNullable(user).orElse(new User("xiaohong"));
System.out.println(user);
// 结果......
User(name=xiaohong, address=null)

orElseGet(Supplier<? extends T> other)

orElseGet()用来获取 Optional 对象中的值,如果值为空,则通过 Supplier 提供的逻辑来生成默认值。源码如下:

    public T orElseGet(Supplier<? extends T> other) {
        return value != null ? value : other.get();
    }

示例如下:

User user = null;
user = Optional.ofNullable(user).orElseGet(() -> {
  Address address = new Address("湖南省","长沙市","岳麓区");
  return new User("xiaohong",address);
});
System.out.println(user);
// 结果......
User(name=xiaohong, address=Address(province=湖南省, city=长沙市, area=岳麓区))

orElseGet()orElse()的区别是:当 T 不为 null 的时候,orElse() 依然执行 other 的部分代码,而 orElseGet() 不会,验证如下:

public class OptionalTest {

    public static void main(String[] args) {
        User user = new User("xiaoming");
        User user1 = Optional.ofNullable(user).orElse(createUser());
        System.out.println(user);

        System.out.println("=========================");

        User user2 = Optional.ofNullable(user).orElseGet(() -> createUser());
        System.out.println(user2);
    }

    public static User createUser() {
        System.out.println("执行了 createUser() 方法");
        Address address = new Address("湖南省","长沙市","岳麓区");
        return new User("xiaohong",address);
    }
}

执行结果如下:

是不是 orElse() 执行了 createUser() ,而 orElseGet() 没有执行?一般而言,orElseGet()orElse() 会更加灵活些。

orElseThrow(Supplier<? extends X> exceptionSupplier)

orElseThrow() 用来获取 Optional 对象中的值,如果值为空,则通过 Supplier 提供的逻辑抛出异常。源码如下:

    public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
        if (value != null) {
            return value;
        } else {
            throw exceptionSupplier.get();
        }
    }

示例如下:

User user = null;
user = Optional.ofNullable(user).orElseThrow(() -> new RuntimeException("用户不存在"));

类型转换

Optional 提供 map()flatMap() 用来进行类型转换。

map(Function<? super T, ? extends U> mapper)

map() 允许我们对 Optional 对象中的值进行转换,并将结果包装在一个新的 Optional 对象中。该方法接受一个 Function 函数,该函数将当前 Optional 对象中的值映射成另一种类型的值,并返回一个新的 Optional 对应,这个新的 Optional 对象中的值就是映射后的值。如果当前 Optional 对象的值为空,则返回一个空的 Optional 对象,且 Function 不会执行,源码如下:

    public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
        Objects.requireNonNull(mapper);
        if (!isPresent())
            return empty();
        else {
            return Optional.ofNullable(mapper.apply(value));
        }
    }

比如我们要获取 User 对象中的 name,如下:

User user = new User("xiaolan");
String name = Optional.ofNullable(user).map(value -> value.getName()).get();
System.out.println(name);
// 结果......
xiaolan

Function<? super T, Optional<U>> mapper

flatMap()map() 相似,不同之处在于 flatMap()的映射函数返回的是一个 Optional 对象而不是直接的值,它是将当前 Optional 对象映射为另外一个 Optional 对象。

    public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
        Objects.requireNonNull(mapper);
        if (!isPresent())
            return empty();
        else {
            return Objects.requireNonNull(mapper.apply(value));
        }
    }

上面获取 name 的代码如下:

String name = Optional.ofNullable(user).flatMap(value -> Optional.ofNullable(value.getName())).get();

flatMap() 内部需要再次封装一个 Optional 对象,所以 flatMap() 通常用于在一系列操作中处理嵌套的Optional对象,以避免层层嵌套的情况,使代码更加清晰和简洁。

过滤

Optional 提供了 filter() 用于在 Optional 对象中的值满足特定条件时进行过滤操作,源码如下:

    public Optional<T> filter(Predicate<? super T> predicate) {
        Objects.requireNonNull(predicate);
        if (!isPresent())
            return this;
        else
            return predicate.test(value) ? this : empty();
    }

filter() 接受 一个Predicate 来对 Optional 中包含的值进行过滤,如果满足条件,那么还是返回这个 Optional;否则返回 Optional.empty

实战应用

这里大明哥利用 Optional 的 API 举几个例子。

  • 示例一

Java 8 以前:

    public static String getUserCity(User user) {
        if (user != null) {
            Address address = user.getAddress();
            if (address != null) {
                return address.getCity();
            }
        }
        return null;
    }

常规点的,笨点的方法:

    public static String getUserCity(User user) {
        Optional<User> userOptional = Optional.of(user);
        return Optional.of(userOptional.get().getAddress()).get().getCity();
    }

高级一点的:

    public static String getUserCity(User user) {
        return Optional.ofNullable(user)
                .map(User::getAddress)
                .map(Address::getCity)
                .orElseThrow(() -> new RuntimeException("值不存在"));
    }

是不是比上面高级多了?

  • 示例二

比如我们要获取末尾为"ming"的用户的 city,不是的统一返回 "深圳市"。

Java 8 以前

    public static String getUserCity(User user) {
        if (user != null && user.getName() != null) {
            if (user.getName().endsWith("ming")) {
                Address address = user.getAddress();
                if (address != null) {
                    return address.getCity();
                } else {
                    return "深圳市";
                }
            } else {
                return "深圳市";
            }
        }

        return "深圳市";
    }

Java 8

    public static String getUserCity2(User user) {
        return Optional.ofNullable(user)
                .filter(u -> u.getName().endsWith("ming"))
                .map(User::getAddress)
                .map(Address::getCity)
                .orElse("深圳市1");
    }

这种写法确实是优雅了很多。其余的例子大明哥就不一一举例了,这个也没有其他技巧,唯手熟尔!!

Java 14 的 Helpful NullPointerExceptions

我们先看如下报错信息:

Exception in thread "main" java.lang.NullPointerException
  at com.skjava.java.feature.Test.main(Test.java:6)

从这段报错信息中你能看出什么? Test.java 中的第 6 行产生了 NullPointerException。还能看出其他什么吗?如果这段报错的代码是这样的:

public class Test {
    public static void main(String[] args) {
        User user = new User();
        System.out.println(user.getAddress().getProvince().length());
    }
}

你知道是哪里报空指针吗? 是user.getAddress() 还是 user.getAddress().getProvince() ?看不出来吧?从这个报错信息中,我们确实很难搞清楚具体是谁导致的 NullPointerException

在 Java 14 之前,当发生 NullPointerException 时,错误信息通常很简单,仅仅只指出了出错的行号。这会导致我们在排查复杂表达式时显得比较困难,因为无法确定是表达式中的哪一部分导致了 NullPointerException,我们需要花费额外的时间进行调试,特别是在长链式调用或者包含多个可能为空的对象的情况下。

为了解决这个问题,Java 14 对 NullPointerException 的提示信息进行了改进,当发生 NullPointerException 时,异常信息会明确指出哪个具体的变量或者表达式部分是空的。例如,对于表达式 a.b().c().d(), 如果 b() 返回的对象是 null,异常信息将明确指出 b() 返回的对象为 null。例如上面的信息:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "*****.Address.getProvince()" because the return value of "*****.User.getAddress()" is null
  at com.skjava.java.feature.Test.main(Test.java:6)

他会明确告诉你 User.getAddress() 返回的对象为 null

这样的提示信息将会让我们能够快速准确地定位导致 NullPointerException 的具体原因,无需逐步调试或猜测,有助于快速修复问题,减少维护时间和成本。

阅读全文