一篇文章带你彻底掌握Optional

670 阅读15分钟

因为网上关于Optional的介绍大多仅仅只是停留在介绍Api上,缺少相关细节与总结,故在此重新整理一篇涉及Optional实现细节的详细文档,希望对大家有帮助。

我们可以带着这些问题来阅读这篇文章:

  • Optional有哪些优劣势?
  • 什么是函数接口?
  • lambda表达式原理是怎样的?
  • lambda的方法引用是什么?
  • Optional提供的API可以分为哪几类?
  • map和flatMap 有什么区别?

Optional引用效果对比

举个简单的例子:获取汽车发动机名字。 我们通过一个简单的demo来看看调用效果

类定义

汽车类 :提供获取发动机对象函数

public class Car {
    private Engine engine;
    public Engine getEngine() {
        return engine;
    }

发动机类 :提供获取发动机名函数

public class Engine {
    private final String name;
    public Engine(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }

获取发动机名

  • 普通判空调用
private static String getEngineName(Car car) {
    String name = null;
    if (car != null) {
        Engine engine = car.getEngine();
        if (engine != null) {
            name = engine.getName();
        }
    }
    return name == null ? "UnKnown" : name;
}

普通调用时,需要层层判空处理,car判空,engine判空,name也需要判空,层级越多,代码越复杂

  • Optional调用
private static String getEngineNameWithOptionalMap(Optional<Car> car) {
    return car.map(Car::getEngine)
            .map(Engine::getName)
            .orElse("UnKnown");
}

可以看到optional调用中没有一个判空处理,整个代码行数也从10行减少到5行,减少50%。单从代码编写便利性的角度看,Optional优势还是比较显著的。

什么是Optional

Optional 是 Java 8 引入的一个容器类,用于包含或不包含非空值。它的目的是提供一种更优雅的方式来处理可能为 null 的情况,从而避免 NullPointerException

在 Java 8 之前,处理 null 值通常需要使用冗长的 if 语句来检查变量是否为 nullOptional 类提供了一种更简洁、更表达性的方式来处理这种场景。

经过本人实践下来,学习Optional有如下好处:

  1. 代码判空处理确实更加优雅
  2. 学习其设计思想。其实与Kotlin的let,apply等在设计思想上是相通的。

当然,Optional并不是万能的,它也有相关缺点:

  1. 可读性变差。它带来的最直观的感受就是可读性变差了,原来直接判空的写法更直白,更利于理解。
  2. 维护风险。因为可读性变差,如果团队其他同事不了解相关语法(或者不熟练),在维护过程修改变更极有可能出错。

网上有文章说对性能会有提升,因为减少了临时变量。对此持怀疑态度,因为虽然临时变量有减少,但是增加了函数调用,函数调用也是要算性能开销的。

因为Optional操作中涉及到大量的 函数接口 和 lambda表达式 的使用,故我们先对这两者做一个基础介绍, 若对此已经了解的话可以直接跳到Optional Api分类介绍。

函数接口

在学习Optional之前,我们需要首先了解函数接口,因为Optional Api中很多参数都是函数接口对象。

Java 8 引入了2个比较重要的特性:

1. 函数接口
2. 支持在接口中通过default关键字提供默认实现。

注意:函数接口中仅有一个抽象方法。

我们首先看看都有哪些函数接口。这些函数接口被定义在 java.util.function包中,从参数数量维度可以分为3类:

  1. 无参数
  2. 1个参数
  3. 2个参数

无参数

  • Supplier<R> : 无输入参数,但返回类型为R类型的函数方法。

1个参数

  • Function<T, R> : 输入一个参数(类型为T),返回类型为R 类型的函数接口。
  • Consumer<T> : 输入一个参数(类型为T),无返回值
  • Predicate<T> : 输入一个参数(类型为T),返回类型固定为boolean类型的函数接口。
  • To[基本类型]Function<T> : 输入一个参数(类型为T),返回基本类型。比如ToIntFunction, ToLongFunction, ToDoubleFunction.
  • UnaryOperator<T> : 输入一个参数(类型为T),同时返回类型也为T。

2个参数

  • BiFunction<T, U, R> : 输入2个参数(分别为T,U类型),返回R类型的函数接口。
  • BinaryOperator<T> : 输入2个参数(都为T类型),同时返回类型也为T。
  • Comparator<T> : 输入2个参数(都为T类型),同时返回类型固定为int值。

Function<T, R>

这里我们重点介绍下 的定义和默认实现,其他函数接口实现都类似,不同的只是接口方法名而已。

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}
  1. @FunctionalInterface

    可以看到Function 接口是被 @FunctionalInterface 注解修饰的(所有的函数接口基本上都有被该注解修饰),这表示该接口只允许定义一个抽象方法(即该函数接口需要被实现的方法),若定义多个抽象方法,在编译时会报错。

  2. R apply(T t);

    Function 接口实例需要实现的抽象方法,其签名为 (T)-〉R, 接收一个T类型的参数,返回R类型的对象。

  3. compose

    compose 方法是将另一个Function实例的执行与当前Function实例的执行组合起来,先执行入参Function,再执行自身Function。

    • compose 方法是被default 修饰的,说明compose方法是Function 接口的默认实现,Function 实例在实现Function 接口时可以直接使用默认实现,也可以覆盖重写compose 方法。
    • compose表示2个函数接口实例按先后顺序执行:先执行入参Function实例的apply方法,然后执行自身Function实例的apply方法。

注意:使用compose方法的前提是入参Function的apply方法刚好返回当前Function apply方法的入参类型T, 这样才能将2者连接起来。

  1. andThen

    andThen也是结合两个Function并执行对应的apply方法,与compose的差异在于执行先后顺序,andThen 是先执行自身Function的apply方法,再执行入参Function的apply 方法。

  2. identity()

    identity返回一个Function实例,返回值即入参值。

注意:compose、anThen、identity 方法均是返回一个新的Function 实例,这就意味着调用这些方法时,入参Function 和 自身Function 的apply方法不会立即执行,只是指定了执行顺序规则,需要等返回的新Function执行apply时才会具体执行。基于此,我们可以用该语法设置回调等处理。比如等到网络请求后再更新UI。 更新网络请求的结果 刚好是更新UI的入参,此时就可以用andThen。

关于函数接口名大家不用死记硬背,在使用时多翻两遍文档就好,多使用几次就记住了。

lambda 表达式

语法

Lambda表达式由三部分组成:

  1. 参数列表
  2. 箭头
  3. 主体
image.png

两种风格,分别是:

  1. 表达式-风格

    (parameters) -> expression

  2. 块-风格

    (parameters) -> { statements; }

例:

  1. () -> {}
  2. () -> "hello"
  3. () -> { return "hello"; }

其中 () 用来描述参数列表,{} 用来描述方法体,-> 为 lambda运算符 ,读作(goes to)。

lambda表达式字节码验证

拿Function接口为例,既然可以用lambda表示Function的实例,那lambda是如何实现apply方法的呢?编译器在编译过程中做了什么呢?

  1. 我们新增一个Main.java 文件,并在main方法中定义一个 Function<Integer, Integer> 实例add对象,然后执行add 函数实例方法,并打印结果。
public class Main {
    public static void main(String[] args) {
        Function<Integer, Integer> add = (t)->2*t;
        System.out.println("add result:" + add.apply(2));
    }
}

2. 执行程序

java -Djdk.internal.lambda.dumpProxyClasses com.yyt.myapplication.Main

执行上面指令我们得到下图结果,这是符合预期的。

截屏2024-12-07 下午11.31.07.png

注意,在运行时加上vm参数-Djdk.internal.lambda.dumpProxyClasses 就可以得到lambda实例对应独立的内部类class文件,如下图所示:

截屏2024-12-07 下午11.35.49.png

  1. 查看Main$$Lambda$1.class 内部类的内容

直接通过AndroidStuido 打开class文件,内容如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.yyt.myapplication;

import java.lang.invoke.LambdaForm.Hidden;
import java.util.function.Function;

// $FF: synthetic class
final class Main$$Lambda$1 implements Function {
    private Main$$Lambda$1() {
    }

    @Hidden
    public Object apply(Object var1) {
        return Main.lambda$main$0((Integer)var1);
    }
}

可以发现,该类确实是Function 接口的一个具体实现类,尤其是实现了apply方法,在apply方法中会执行 Main.lambdamainmain0((Integer)var1) 方法, 我们可以在Main.class 字节码文件中找到它。

  1. 查看Main.class 文件(仅列出核心部分)
...
...
...
#14 = Class              #47            // com/yyt/myapplication/Main

#51 = Methodref          #14.#73        // com/yyt/myapplication/Main.lambda$main$0:(Ljava/lang/Integer;)Ljava/lang/Integer;

#73 = NameAndType        #22:#23        // lambda$main$0:(Ljava/lang/Integer;)Ljava/lang/Integer;
...
...
...
{
  public com.yyt.myapplication.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 12: 0


  private static java.lang.Integer lambda$main$0(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)Ljava/lang/Integer;
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
         0: iconst_2
         1: aload_0
         2: invokevirtual #13                 // Method java/lang/Integer.intValue:()I
         5: imul
         6: invokestatic  #8                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         9: areturn
      LineNumberTable:
        line 14: 0
}
...
...

  • 通过#51 的描述可以看出来 Main.lambdamainmain0 代表Main类中的方法 lambdamainmain0
  • 第25行定义了lambdamainmain0 方法,可以看到它是一个private的静态方法,通过descriptor 描述可以看出它就是我们的lambda表达式代码块的实现部分。

由此可见,我们在使用lambda表达式时,编译器会干下面几件事:

  1. 整个lambda表达式对应的匿名内部类仍然会被生成,并在字节码文件中体现出来,如上面的 Main$$Lambda$1.class
  2. lambda表达式的代码块实现内容会以本地静态方法的形式记录起来,如上面的 lambdamainmain0(java.lang.Integer)
  3. 匿名内部类的抽象方法中会调用静态方法(即lambda代码块内容)

由此可见,虽然我们没有显示的new Function的实例,但是通过lambda表达式也可以隐式的生成其匿名实例。

这篇文章对lambda介绍的比较详细, 上面关于语法的描述就来自这篇文章,大家可以看下:segmentfault.com/a/119000002…

Optional 内部Api介绍

Optional 实现的原理其实很简单,就是在内部维护一个泛型实例 T value 对象, 想要访问value 都得通过Optioanl来实现,这就达到封装的效果(判空处理就是封装在了Optional内部)。

我们可以将Optional 的Api分为下面几类:

  • 创建

  • 查询

  • 判断

  • 执行

  • 组合

创建

Optional 提供了3个静态方法来创建Optional对象实例。

  1. empty()

定义

private final T value;
private static final Optional<?> EMPTY = new Optional<>(null);

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

使用: Optional<Car> car = Optional.empty();

说明:通过empty快速创建Optional对象时,其内部维护的value 对象就为null(没有被赋值)。empty 返回的是一个全局唯一的实例,因为Optional 没有提供setValue 相关方法,所以不用担心出问题。

  1. of

定义

public static <T> Optional<T> of(T value) {
    return new Optional<>(Objects.requireNonNull(value));
}
    
//注意,requireNonNull 是Objects类中的方法,为了简便起见和 Optional#of方法展示在一起了。
public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

使用:Optional<Car> car = Optional.of(new Car());

说明:使用of创建Optional对象,则一定要确保传入的参数对象不为空,不然of方法内部通过 Objects.requireNonNull(value)抛出空指针。

  1. ofNullable

定义

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

使用

    Optional<Car> car = Optional.ofNullable(null); 
    或者 
    Optional<Car> car = Optional.ofNullable(new Car()); 

说明:使用ofNullable创建Optional对象时,则不用关心传入的参数是不是null,为空则返回empty Optional对象,不为空,则构建一个新的 Optional对象。

查询

  1. get

定义

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

使用

    Optional<Car> car = Optional.of(null); 
    Car value = car.get(); //此时会抛 NoSuchElementException 异常
    
    Optional<Car> car = Optional.of(new Car());
    Car value = car.get(); //正常获取新构建的Car实例。

说明:可以通过get 方法获取构建Optional对象时传入的具体实例,若在构建Optional 时没有传入值,则get方法抛 NoSuchElementException 异常。 一般不会直接通过get方法获取实例,不然就失去了Optional的意义了。

  1. orElse

定义

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

使用

    Optional<Car> car1 = Optional.empty(); 
    Car car2 = new Car();
    Car finalCar = car1.orElse(car2); // finalCar == car2

说明:当不知道Optional中的value是否为空时,可以采用防御性编程,若为空,则给其指定一个具体的实例。其实该方法效果跟 if-else 效果是一样的。

  1. orElseGet

定义


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

使用

    Optional<Car> car1 = Optional.empty(); 
    Car finalCar = car1.orElse(Car car2 = ()->new Car()); // finalCar == car2

说明:orElseGet 和 orElse 效果是一样的,不同的是:

  • orElse:通过参数直接指定具体的value实例。在执行orElse之前就已经确定。

  • orElseGet:通过传入函数接口实例,在执行orElseGet时通过函数返回值才确定具体值。

orElse 和 orElseGet 都是典型的防御式编程。两者也仅仅只是体现在参数上的不同。

  1. orElseThrow

定义

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

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

使用

    Optional<Car> car = Optional.empty(); 
    Car car1 = car.orElseThrow(); // 抛出 "No value present" 异常
    
    Car car1 = car.orElseThrow(()->{new Exception("test error")}); // 抛出 "test error" 异常
    

说明:orElseThrow 内部有重载2个方法,一个无参,另一个参数支持指定函数接口 来返回自定义的异常。若value 不为空,则直接返回。

  1. stream

定义

public Stream<T> stream() {
    if (!isPresent()) {
        return Stream.empty();
    } else {
        return Stream.of(value);
    }
}

使用

    Optional<Car> car = Optional.empty(); 
    Stream<Car> stream= car.stream(); // 返回Stream 实例

说明:可以通过stream 连续执行相关读写操作。

判断

  1. isPresent()isEmpty()

定义

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

public boolean isEmpty() {
    return value == null;
}

使用

    Optional<Car> car = Optional.empty(); 
    boolean isPresent = car.isPresent(); // 返回false
    boolean isEmpty = car.isEmpty(); // 返回true

说明:isPresent 和 isEmpty 都是判断value的值,只是判断的角度不一样,isPresent 时判断value是否有值,而isEmpty 则是判断value是否为空。

执行

  1. ifPresent

定义

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

使用

    Optional<Car> car = Optional.of(new Car()); 
    //输出打印"print it if value not null"
    car.ifPresent((t)->{System.out.println("print it if value not null"});

说明:ifPresent 表示如果value 不为空,则执行入参函数接口实例对应的代码块,注意函数接口的入参也必须和Optional的value类型一致,即上面的参数t 其实也是Car类型,从源码中可以看出,传入的就是value值, 我们可以在lambda代码块中继续引用它。

  1. ifPresentOrElse

定义

public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {
    if (value != null) {
        action.accept(value);
    } else {
        emptyAction.run();
    }
}

使用

    Optional<Car> car = Optional.of(new Car()); 
    //输出打印"value not null"
    car.ifPresentOrElse((t)->{System.out.println("value not null"} 
                        ,()->{System.out.println("value is null"} );
或者
    Optional<Car> car = Optional.empty(); 
    //输出打印"value is null"
    car.ifPresentOrElse((t)->{System.out.println("value not null"} 
                        ,()->{System.out.println("value is null"} );

说明:ifPresentOrElse 表示如果value 不为空,则执行入参函数接口实例对应的代码块;若value为空,则执行指定的Runnable接口实例。ifPresentOrElse 是ifPresent的升级款,考虑value 存在和不存在2种情况,并且均给出处理方案。效果就是 ifPresent + orElse 组合的效果。

  1. ifPresentOrElse

定义

public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {
    if (value != null) {
        action.accept(value);
    } else {
        emptyAction.run();
    }
}

使用

    Optional<Car> car = Optional.of(new Car()); 
    //输出打印"value not null"
    car.ifPresentOrElse((t)->{System.out.println("value not null"} 
                        ,()->{System.out.println("value is null"} );
或者
    Optional<Car> car = Optional.empty(); 
    //输出打印"value is null"
    car.ifPresentOrElse((t)->{System.out.println("value not null"} 
                        ,()->{System.out.println("value is null"} );

说明:ifPresentOrElse 表示如果value 不为空,则执行入参函数接口实例对应的代码块;若value为空,则执行指定的Runnable接口实例。ifPresentOrElse 是ifPresent的升级款,考虑value 存在和不存在2种情况,并且均给出处理方案。效果就是 ifPresent + orElse 组合的效果。

组合

所有的组合Api均返回 一个Optional对象, 返回的Optional 对象有3种可能:

  1. 返回自身。跟当前引用的Optional是同一个对象。
  2. 返回empty Optional。不满足条件时则返回空Optional对象。
  3. 返回入参函数接口返回的Optional对象。
  1. filter

定义

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

使用

    Optional<Car> car = Optional.of(new Car()); 
    car.filter((t)->{t != null}); //返回 car 对象(因为满足条件,则返回自身)
    car.filter((t)->{t == null}); //返回 empty Optional 对象(因为不满足条件,则返回empty对象)

说明:filter用来对当前Optional对象做过滤处理,只要记住这一点即可:只要一个条件不满足就会返回empty Optional对象

  1. map

定义

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));
    }
}

使用

    Optional<Car> car = Optional.of(new Car()); 
    car.map((t)->t.getEngine()); //返回 Optional(Engine) 实例
    car.map(Car::getEngine); //返回 Optional(Engine) 实例

说明:map 用来映射一个新的Optional 对象。如上面例子,通过car 对象映射到 Engine对象了。例子中传入的参数 (t)->t.getEngine() 和 Car::getEngine 的效果是一样的,我们在最后一小节来说明。

  1. flatMap

定义

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

使用

    Optional<Car> car = Optional.of(new Car()); 
    car.flatMap((t)->new Optional(t.getEngine())); //返回 Optional(Engine) 实例

说明:flatMap 和 map的效果是一模一样的,它们均是在Optional value不为空时才能生效。

不同点有下面3个方面:

不同点\类型mapflatMap
入参函数返回类型不同返回具体的value对象返回Optional对象
生成Optional的时机不一样map内部生成Optional对象入参函数接口返回Optional对象
Optional是否可空?返回的Optional对象是可空的返回的Optional对象一定不是empty的,否则会报空指针

基于以上不同点,当你无法确定函数中返回的对象是否为空时,最好使用map,不然flatMap内部可能就会抛出空指针了。

  1. or

定义

public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) {
    Objects.requireNonNull(supplier);
    if (isPresent()) {
        return this;
    } else {
        @SuppressWarnings("unchecked")
        Optional<T> r = (Optional<T>) supplier.get();
        return Objects.requireNonNull(r);
    }
}

使用

    Optional<Car> car = Optional.empty(); 
    car.or(()->new Optional(new Car())); //返回 Optional<Car> 实例

说明:or还是防御性编程,当前Optional为empty时,会尝试通过函数接口返回的相同类型的Optional对象。注意2点:

  1. 入参函数返回类型必须跟当前引用的Optional是同一类型。如上处例子中是 Optional 类型。
  2. 入参函数返回对象一定不能为空,否则会抛空指针。

根据上面描述,组合函数有下面特点:

  1. 所有的组合Api 传入的函数接口实例一定不能为null,否则第一时间就会报空指针。
  2. 除了map 和 filter返回的Optional对象可能为empty, 其他的返回对象一定不能为空,否则跑空指针。

再谈lambda表达式

    Optional<Car> car = Optional.of(new Car()); 
    car.map((t)->t.getEngine()); //返回 Optional(Engine) 实例
    car.map(Car::getEngine); //返回 Optional(Engine) 实例

为什么 (t)->t.getEngine() 和 Car::getEngine 效果是一样的?

当在Lambda表达式中仅调用引用实例的一个方法时可以使用方法引用,其写法为目标引用::方法名称

  1. 指向静态方法的方法引用

    Function<String, Integer> fun = s -> Integer.parseInt(s);
    Function<String, Integer> fun = Integer::parseInt;
    
  2. 指向任意类型实例方法的方法引用

    Function<String, Integer> fun = s -> s.length();
    Function<String, Integer> fun = String::length;
    
  3. 指向现存外部对象实例方法的方法引用

    String s = "hello";
    Supplier<Integer> len = () -> s.length();
    Supplier<Integer> len = s::length;
    

从上可以看出,我们上面案例中是使用的第2种引用方式。 第2种和第3种 方式的区别在于定义的函数签名方式,若是无参数则用第3种方式,若是需要有参数,则使用第2种(此处参数就是实例自身)。

总结

我们首先介绍了Optional 编写代码带来的简洁性,然后介绍了Java 8 开始引入的函数接口和lambda表达式,并且对lambda的生效过程进行了字节码验证;最后详细介绍了Optional的Api,对所有Api进行了分类归档,可以作为编码时的参考。

参考文档:lambda表达式介绍