Day33 | Java中的Optional

Java异常体系中有一个我们常见的空指针异常。

为了避免空指针异常,在写代码时我们通常都会开启防御性编程。

代码库里充斥着大量繁琐的类似if (obj != null)这样的判断。

代码看起来很不好看。

典型的代码:

java 复制代码
public String getCityOfUser(User user) {
    if (user != null) {
        Address address = user.getAddress();
        if (address != null) {
            String city = address.getCity();
            if (city != null) {
                return city;
            }
        }
    }
    return "未知";
}

Java8开始引入的java.util.Optional<T>类,就是为了提供一种更优雅、更安全的解决方案。

但是他不是要消灭null,而是用一种容器的设计方案,来明确地表示一个值可能缺失的情况。

从而在编译层面就提醒开发人员处理这种可能性。

一、什么是Optional

你可以把Optional<T>想象成一个盒子,如果盒子里有一个T类型的对象,我们称这个Optional包含一个值。

如果盒子里是空的,我们就称这个Optional为空。

它的核心思想是,通过将可能为空的对象包装在这个盒子里,强制调用者在开箱的时候必须思考如果箱子是空的怎么办?,从而从根本上减少NPE的发生。

它是一个设计工具,能让你的API意图更加明确。

二、创建Optional对象

一般我们不会直接new Optional(),而是使用它提供的三个静态工厂方法来创建实例:

2.1 Optional.of(T value)

如果你百分百确定value不为null的时候使用。如果value是null,它会马上抛出一个NullPointerException。

java 复制代码
Optional<String> name = Optional.of("lazysnail");

// 抛NPE
// Optional<String> nullName = Optional.of(null);

2.2 Optional.ofNullable(T value)

这是最常用,也是最灵活的方法。

如果value不为null,它会创建一个包含这个值的Optional;

如果value为null,它会创建一个空的Optional。

java 复制代码
String username = getUserFromDatabase();
Optional<String> optUsername = Optional.ofNullable(username);

getUserFromDatabase方法可能为空。

2.3 Optional.empty()

这个方法直接创建一个明确的、空的Optional实例。

java 复制代码
Optional<String> emptyOptional = Optional.empty();

三、Optional值的消费

最开始用Optional的时候,有些人会写成这样:

java 复制代码
Optional<String> optName = ...;
if (optName.isPresent()) {
    String name = optName.get();
    System.out.println(name);
}

这种写法本质上和if (name != null)没什么区别,只是换了个更啰嗦的方式,完全没有发挥出Optional的优势。

get()方法在Optional为空的时候会抛出NoSuchElementException,还是很危险。

以下几种消费方式才是推荐的:

3.1 ifPresent(Consumer<T> action)

如果值存在,就对它执行给定的操作;如果不存在,就什么也不做。

java 复制代码
optUsername.ifPresent(name -> System.out.println("用户名: " + name));
optUsername.ifPresent(System.out::println);

3.2 ifPresentOrElse(Consumer<T> action, Runnable emptyAction)

如果值存在,执行action;否则,执行emptyAction。

java 复制代码
optUsername.ifPresentOrElse(
    name -> System.out.println("欢迎, " + name),
    () -> System.out.println("游客你好,请先登录。")
);

3.3 orElse(T other)

如果值存在,返回值;否则,返回一个预设的默认值。

java 复制代码
String finalUsername = optUsername.orElse("游客");

3.4 orElseGet(Supplier<? extends T> supplier)

如果值存在,返回值;否则,通过一个Supplier函数来动态生成一个默认值。

java 复制代码
String finalUsername = optUsername.orElseGet(() -> expensiveDefault());

只有在optUsername为空的时候,才会执行expensiveDefault()方法。

如果生成默认值的开销很大,orElseGet性能相比于orElse要好点,因为它只有在需要的时候才执行。

orElse不管Optional是不是为空,都会先计算好默认值。

3.5 orElseThrow(Supplier<? extends X> exceptionSupplier)

如果值存在,返回值;否则,抛出一个由Supplier函数生成的异常。

这是处理值必须存在场景最好的方式。

java 复制代码
String username = optUsername.orElseThrow(
    () -> new IllegalStateException("用户名不能为空")
);

四、链式操作

Optional还有一个比较强大的地方就是他可以像Stream一样进行链式操作。

4.1 map(Function<T, U> mapper)

如果Optional不为空,它会把内部的值应用mapper函数进行转换,并返回一个新的Optional<U>。

如果为空,就直接返回一个空的Optional。

java 复制代码
Optional<String> optName = Optional.of("lazysnail");
Optional<Integer> optLength = optName.map(String::length);

4.2 filter(Predicate<T> predicate)

如果值存在且满足predicate条件,就返回该Optional;

否则,返回一个空的Optional。

java 复制代码
Optional<String> longName = optName.filter(name -> name.length() > 5);
Optional<String> shortName = optName.filter(name -> name.length() < 5);

4.3 flatMap(Function<T, Optional<U>> mapper)

这个我们在前文中讲到过,flatMap用于处理当你的转换函数本身就返回一个Optional的场景。

如果用map,你会得到一个Optional<Optional>这样的双层盒子。

flatMap则会把结果压平成一个Optional。

java 复制代码
public Optional<Integer> getAgeByName(String name) { ... }

Optional<String> optName = Optional.of("lazysnail");

Optional<Optional<Integer>> nestedAge = optName.map(this::getAgeByName);

Optional<Integer> age = optName.flatMap(this::getAgeByName);

案例中假设我们有一个方法getAgeByName,它根据名字查找年龄,但可能找不到,所以返回Optional<Integer> 。

如果用map,会得到一个双层Optional,Optional<Optional<Integer>>。

如果用flatMap,结果会被压平,用起来更简单。

还记得本文最开始的那个典型的代码案例吗?

如果用flatMap和map来重写,代码就会变成这样:

java 复制代码
public String getCityOfUser(User user) {
    return Optional.ofNullable(user)
                   .flatMap(User::getAddress)
                   .map(Address::getCity)
                   .orElse("未知");
}

对于初学者或者不熟悉Optional使用的人来说,改写的代码可读性确实没有老式代码那么直观。

但是熟练使用之后,你会喜欢上这种编码方式。

结语

本文通过一些小案例,讲解了Optional这个工具在现代Java开发中的使用方法。

合理的使用这些工具,可以让你写的代码更安全,更简洁。

但是要注意,Optional只是提供了一种强大的类型系统层面的约束,而不是为了取代null的。

下一篇预告

Day34 | Java中的日期时间API

如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!

更多文章请关注我的公众号《懒惰蜗牛工坊》

相关推荐
啃火龙果的兔子2 小时前
IntelliJ IDEA社区版下载安装
java·ide·intellij-idea
ckm紫韵2 小时前
Cursor 与 IDEA 互相跳转教程
java·ide·intellij-idea·cursor·ai工具
天若有情6732 小时前
我发明的PROTO_V4协议:一个让数据“穿上迷彩服”的发明(整数传输协议)
网络·c++·后端·安全·密码学·密码·数据
渡过晚枫2 小时前
[蓝桥杯/java/算法]攻击次数
java·算法·蓝桥杯
游浪踏2 小时前
003_AI Agent(模拟实现)
后端·agent
ByteX2 小时前
Java8-Function创建对象替代Builder
java·开发语言
飞火流星020272 小时前
【Arthas工具】使用Trace命令分析Java JVM方法调用链路及耗时
java·jvm·arthas·jvm性能调优·java方法调用链路分析及耗时·jvm实时分析·jvm方法调用实时分析
PieroPC2 小时前
nicegui 3.4.0 + sqlite3 做一个简单维修登记系统
后端
用户7543888677152 小时前
HarmonyOS BLE 快速上手
后端