Java中的Optional:从入门到精通

为啥需要Optional

null在编程中常常是一个令人头疼的问题,处理不好就会导致NullPointerException(空指针异常)。

为了避免空指针异常,代码中就会充斥着繁琐的null检查和条件嵌套。

Optional的出现帮我们解决了这个问题。它让我们可以更安全、优雅地处理可能为空的值。

问题引入

模拟一个从数据库中根据 userName 查找对应用户的一个例子。

需要一个 User Bean:

java 复制代码
/**
 * @author TheSea
 * @description 用户对象
 * @createDate 2025/12/27
 */
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    String name;
    String age;
}

上面的代码其实还给出了一种非常的编程习惯:

把常量写在 equals 左边是一种防御式编程习惯,可以在参数为 null 的情况下避免 NullPointerException,提升代码健壮性,因此在工程实践中被广泛采用。

如果常量在右边:

java 复制代码
if (name.equals("TheSea")) {
	//.......
}

如果 name == null,则会:

java 复制代码
Exception in thread "main" java.lang.NullPointerException

原因很简单:

  • equals 是 实例方法

  • null 不能调用任何方法

而常量在左边的时候:

java 复制代码
if ("TheSea".equals(name)) {
	//.......
}

即使 name == null,也 不会报错,原因是:

  • "TheSea" 是一个确定存在的 String 对象

  • String.equals(Object obj) 内部逻辑是安全的

String.equals 源码核心逻辑(简化版):

java 复制代码
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        // 逐字符比较
    }
    return false;
}

当 anObject == null 时:

java 复制代码
null instanceof String  // false

直接返回 false,不会抛异常。

或者另外一种写法,也是非常安全的写法,使用 Objects.equals(JDK 7+):

java 复制代码
if (Objects.equals(name, "TheSea")) {
	//......
}

特点:

  • 双方都可以是 null

  • 可读性强

  • 非字符串也适用

内部实现其实就是:

java 复制代码
(a == b) || (a != null && a.equals(b))

现在还需要一个 Mapper:

java 复制代码
/**
 * @author TheSea
 * @description 模拟用户表持久层
 * @createDate 2025/12/27
 */
public class UserMapper {
    public User getUserByName(String name){
        if("TheSea".equals(name)){
            return new User("TheSea","25");
        }else{
            return null;
        }
    }
}

现在我们可以在 main 里调用:

java 复制代码
/**
 * @author TheSea
 * @description 主函数
 * @createDate 2025/12/27
 */
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        User theSea = userMapper.getUserByName("TheSea");
        System.out.println(theSea.toString());
    }
}

运行结果如下:

此时当我们换一个名字时:

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        User theSea = userMapper.getUserByName("Ocean");
        System.out.println(theSea.toString());
    }
}

运行结果如下:

为了避免不抛出 NPE,我们会这样写:

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        User theSea = userMapper.getUserByName("Ocean");
        if(theSea != null){
            System.out.println(theSea.toString());
        }
    }
}

在实际项目中,我们会处理大量可能为空的值,这样一来代码中就会充斥着大量的条件判断,会让代码显得非常臃肿且难以阅读和维护。

这就是为啥要引入 optional 的原因。

optional 使用

optional 类似于一个盒子,或者说是一个容器,它可以包含某种类型的值,也可以什么都不包含,比如 null。

并且提供一系列方法来操作内部的值。

同时 optional 也考虑了函数式编程的原则,可以和 lambda 表达式或者 stream API 等特性结合使用,可以优雅的进行一些链式调用。

等会儿我们会用 optional 来重新实现一下刚刚的例子。

empty():创建一个包含空值的 optional 对象

java 复制代码
		//empty() 方法会返回一个空的 optional 对象
        Optional<Object> optionalBox = Optional.empty();
        //使用 optional 内置的方法来检测内部值的状态
        //isPresent() 检测 optional 对象内部是否存在值,存在返回 true,否则 false
        System.out.println(optionalBox.isPresent()); //输出 false

        //注意,创建的是一个空的 optional 对象,所以内部的类型不一定是 object
        Optional<String> optionalBox = Optional.empty();
        //使用 optional 内置的方法来检测内部值的状态
        //isPresent() 检测 optional 对象内部是否存在值,存在返回 true,否则 false
        System.out.println(optionalBox.isPresent());

isPresent():检测 optional 对象内部是否有值

java 复制代码
		//empty() 方法会返回一个空的 optional 对象
        Optional<Object> optionalBox = Optional.empty();
        //使用 optional 内置的方法来检测内部值的状态
        //isPresent() 检测 optional 对象内部是否存在值,存在返回 true,否则 false
        System.out.println(optionalBox.isPresent()); //输出 false

        //注意,创建的是一个空的 optional 对象,所以内部的类型不一定是 object
        Optional<String> optionalBox = Optional.empty();
        //使用 optional 内置的方法来检测内部值的状态
        //isPresent() 检测 optional 对象内部是否存在值,存在返回 true,否则 false
        System.out.println(optionalBox.isPresent());

of():创建一个包含普通值的 optional 对象

java 复制代码
		String value = "Ocean";
        //将我们的 value 放到 optional 对象中包装起来,也就是放进这个盒子当中
        //使用 of() 方法就是返回一个包含字符串对象 "Ocean" 的 optional 对象
        Optional<String> optionalBox = Optional.of(value);

        //使用 optional 内置的方法来检测内部值的状态
        //isPresent() 检测 optional 对象内部是否存在值,存在返回 true,否则 false
        System.out.println(optionalBox.isPresent()); //输出 true

注意,用 of() 方法创建的对象必须包含值,所以要确保传递给 of() 方法的值不为 null。

我们来试一下传入值为 null 的场景:

java 复制代码
		String value = null;
        //将我们的 value 放到 optional 对象中包装起来,也就是放进这个盒子当中
        //使用 of() 方法会返回一个为 null 的 optional 对象
        Optional<String> optionalBox = Optional.of(value);

        //使用 optional 内置的方法来检测内部值的状态
        //isPresent() 检测 optional 对象内部是否存在值,存在返回 true,否则 false
        System.out.println(optionalBox.isPresent()); // 抛出NPE

ofNullable():创建一个可包含null值的 optional 对象

如果传入的值要确保不为 null 的话,那这样的话不是还没有解决我们说的总是要判断值为 null 的问题吗?

因此当我们如果不确定传入的值是否为 null 时,应当使用 ofNullable 这个方法来包装值:

java 复制代码
		String value = null;
        //将我们的 value 放到 optional 对象中包装起来,也就是放进这个盒子当中
        //使用 ofNullable() 方法会返回一个可以将 null 也给视为无值状态的 optional 对象
        Optional<String> optionalBox = Optional.ofNullable(value);

        //使用 optional 内置的方法来检测内部值的状态
        //isPresent() 检测 optional 对象内部是否存在值,存在返回 true,否则 false
        System.out.println(optionalBox.isPresent()); //输出 false

传入不为 null 值的测试:

java 复制代码
		String value = "Ocean";
        //将我们的 value 放到 optional 对象中包装起来,也就是放进这个盒子当中
        //使用 ofNullable() 方法会返回一个可以将 null 也给视为无值状态的 optional 对象
        Optional<String> optionalBox = Optional.ofNullable(value);

        //使用 optional 内置的方法来检测内部值的状态
        //isPresent() 检测 optional 对象内部是否存在值,存在返回 true,否则 false
        System.out.println(optionalBox.isPresent()); //输出 true

get():从 optional 中取值

java 复制代码
		String value = "Ocean";
        //将我们的 value 放到 optional 对象中包装起来,也就是放进这个盒子当中
        //使用 ofNullable() 方法会返回一个可以将 null 也给视为无值状态的 optional 对象
        Optional<String> optionalBox = Optional.ofNullable(value);
        //使用 get() 从 optional 对象中取出存入的值
        String value2 = optionalBox.get();
        System.out.println(value2); //输出 Ocean
        //使用 optional 内置的方法来检测内部值的状态
        //isPresent() 检测 optional 对象内部是否存在值,存在返回 true,否则 false
        System.out.println(optionalBox.isPresent()); //输出 true

虽然使用 get 方法来获取 optional 的值非常简单和直观,但是一般不推荐这么做,这违背了 optional 设计的初衷,稍后会讲到。

使用 optional 改写demo

改写如下:

java 复制代码
public class UserMapper {
    public Optional<User> getUserByName(String name){
        if("TheSea".equals(name)){
            return Optional.of(new User("TheSea","25"));
        }else{
            return Optional.empty();
        }
    }
}
java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optioanl = userMapper.getUserByName("TheSea");
        User user = optioanl.get();
        System.out.println(user);
    }
}

运行输出:

当我们使用一个不存在的用户名时:

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optioanl = userMapper.getUserByName("Ocean");
        User user = optioanl.get();
        System.out.println(user);
    }
}

如果会出现这样的问题,那说明就还是得判断一下是否 optional 内部是否有值才可以,这显然背离了 optional 设计的初衷。

orElse():为空时执行默认操作

因此我们需要使用 orElse() 方法来消除 if 的判断:

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optional = userMapper.getUserByName("TheSea");
        // orElse() 方法在 optional 对象内部有值时返回存储的值,否则返回传入 orElse 内的值
        User user = optional.orElse(new User("默认用户名", "默认年龄"));
        System.out.println(user);
    }
}

当我们传入数据库中不存在的用户名时:

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optional = userMapper.getUserByName("Ocean");
        // orElse() 方法在 optional 对象内部有值时返回存储的值,否则返回传入 orElse 内的值
        User user = optional.orElse(new User("默认用户名", "默认年龄"));
        System.out.println(user);
    }
}

orElseGet():懒加载形式的为空时执行默认操作

与 orElse() 相似的还有一个方法 orElseGet():

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optional = userMapper.getUserByName("Ocean");
        // orElse() 方法在 optional 对象内部有值时返回存储的值,否则返回传入 orElse 内的值
        User user = optional.orElse(new User("默认用户名", "默认年龄"));
        // orElseGet() 方法在语义上与 orElse 效果相同
        // 唯二区别在于:
        // 1、如果 optional 对象内部有值,则不会执行获取传入的默认参数(orElse 不管 optional 对象内部有值无值都会 new 默认对象)
        // 2、orElseGet 方法传入的是一个 Supplier 对象,可以用 lambda 表达式创建,而 orElse 方法直接传入对象即可
        User user1 = optional.orElseGet(() -> new User("默认用户名2", "默认年龄2"));

        System.out.println(user);
        System.out.println(user1);
    }
}

orElseThrow():optional对象内部值为空时抛异常

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optional = userMapper.getUserByName("TheSea");
        // orElseThrow() 方法传入一个 Supplier 参数,可以用 lambda 表达式创建,那么在 optional 对象内部值为空时会抛出对应异常,比如NPE
        User user = optional.orElseThrow(() -> new RuntimeException("一个自定义异常"));
        System.out.println(user);
    }
}

此时如果查的值为 null :

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optional = userMapper.getUserByName("Ocean");
        // orElseThrow() 方法传入一个 Supplier 参数,可以用 lambda 表达式创建,那么在 optional 对象内部值为空时会抛出对应异常,比如NPE
        User user = optional.orElseThrow(() -> new RuntimeException("一个自定义异常"));
        System.out.println(user);
    }
}

ifPresent():值存在时执行的操作

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optional = userMapper.getUserByName("TheSea");
        //ifPresent() 方法接收一个 Consumer 参数,可以用lambda表达式创建,在 optional 对象内部存在值的时候才会执行对应的函数式操作
        optional.ifPresent(user -> System.out.println(user));
    }
}

如果为空的话 ifPresent 将什么也不做。

filter():根据给定条件过滤 optional 对象中的值

filter() 方法接受一个 Predicate 的函数式接口作为参数 ,该接口用于定义过滤条件。如果值存在且满足条件,filter 会返回一个包含原始值的新的 optional 对象,否则就返回一个空的 optional 对象。

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optional = userMapper.getUserByName("TheSea");
        Optional<User> user1 = optional.filter(user -> Objects.equals(user.getName(), "TheSea"));
        //"TheSea" 存在,因此应该打印
        user1.ifPresent(user -> System.out.println(user));
    }
}
java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optional = userMapper.getUserByName("Ocean");
        Optional<User> user1 = optional.filter(user -> Objects.equals(user.getName(), "TheSea"));
        //"Ocean" 不存在,因此不执行打印
        user1.ifPresent(user -> System.out.println(user));
    }
}

由于 filter() 方法返回的是一个 optional 对象,因此我们还可以使用链式编程:

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optional = userMapper.getUserByName("Ocean");
        optional.filter(user -> Objects.equals("TheSea", user.getName()))
                .ifPresent(user -> System.out.println(user));
    }
}

map():用于对 optional 对象中的值进行转换

map() 方法接收一个 Function 参数,可以用 lambda 参数创建,这个参数会被应用到 optional 对象存储的值上,用于对 optional 对象中存储的值进行转换,并返回一个新的 optional 对象,其中包含了转换后的值。

如果 optional 是空的,那么 map 什么也不做。

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optional = userMapper.getUserByName("TheSea");

        Optional<User> user1 = optional.map(user -> {
            if (Objects.equals("TheSea", user.getName())) {
                user.setName("TheSea2");
            }
            return user;
        });

        System.out.println(user1.orElseThrow(() -> new RuntimeException("TheSea不存在")));
    }
}

flatMap():用于扁平化嵌套的 optional 的情况

flatMap() 转换函数返回的必须是另一个 Optional 对象,这就意味着 flatMap() 方法可以用于嵌套的 Optional 的情况。

它会将两个 optional 对象合并成一个。

如果原始的 optional 对象为空,或者转换函数返回的 optional 对象为空,那么最终得到的也是一个空的 Optional 对象。

来举例说明一下。

首先改造一个嵌套的结构,我们将 User 对象的 Age 熟悉用 Optional 包装一下:

java 复制代码
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
    String name;
    String age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Optional<String> getAge() {
        return Optional.ofNullable(age);
    }

    public void setAge(String age) {
        this.age = age;
    }
}

此时我们调用一下:

java 复制代码
public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = new UserMapper();
        Optional<User> optional = userMapper.getUserByName("TheSea");

        //可以看到返回的 age 是一个 optional 的嵌套结构
        Optional<Optional<String>> userAge = optional.map(user -> user.getAge());

        //如果使用 flatMap 的话,它会合并嵌套的 Optional 对象
        Optional<String> userAge2 = optional.flatMap(user -> user.getAge());
    }
}

通常情况下,如果只需要对 Optional 对象中的值进行转换,而不需要嵌套的 Optional,那么使用 map() 方法是更合适的选择。但是如果需要进行一些操作,可能会返回另一个 Optional 对象,那么 flatMap 方法更适合。

不过感觉这个用的蛮少的,知道怎么回事就可以。

Optional 推荐使用场景

用于方法返回类型

首先普遍用于方法的返回类型,表示方法可能不返回结果,比如:

当你有一个方法可能返回一个值或者也可能什么都不返回(即返回 null )时就可以使用 optional 包装返回。

比如当我们设计一个 API 的时候,它能引导 API 的使用者告诉他们这个值可能不存在,并强调调用者处理这种结果的可能性:

这样可以大幅度减少错误的发生。

Optional 不推荐使用场景

不应该用于类的字段

由于 Optional 对象的创建和管理有一定的开销,因此并不适用于用作类的字段。

使用 Optional 来作为字段类型会增加内存消耗,并且会使得对象的序列化变得复杂。

不应该用做方法参数

将 Optional 用做方法参数,会导致方法的使用变得难以理解和复杂化:

同理,也不应该用作构造器参数。

不应该用作集合的参数类型

如果你的方法返回一个集合,而且这个集合可能为空,不应该用 optional 来包装它,因为集合已经可以很好地处理空集合的情况了:

不建议使用 Optionl 的 get() 方法

因为调用 Optional 的 get() 方法前,没有确认值是否存在的话会导致 NoSuchElementException。

即使是使用 isPresent() 和 get() 的组合也不是一个最好的选择,这样做和直接调用可能为 null 的引用没多大区别,因为这仍是在进行显式的检查,背离了 Optional 设计的初衷。

而应该转为使用下列方法:

总结







工程上最常用的方法:ofNullable + map + filter + orElseThrow / orElseGet

Optional 的工程级常用方法就是:

ofNullable + map + filter + orElseThrow / orElseGet

其余方法更多是辅助或特定场景使用。





相关推荐
程序员侠客行2 小时前
Mybatis入门到精通 一
java·架构·mybatis
糕......2 小时前
Java异常处理完全指南:从概念到自定义异常
java·开发语言·网络·学习
御水流红叶2 小时前
第七届金盾杯(第一次比赛)wp
开发语言·python
Lhan.zzZ2 小时前
Qt跨线程网络通信:QSocketNotifier警告及解决
开发语言·c++·qt
小徐Chao努力2 小时前
【Langchain4j-Java AI开发】04-AI 服务核心模式
java·人工智能·python
superman超哥2 小时前
仓颉性能优化秘籍:内联函数的优化策略与深度实践
开发语言·后端·性能优化·内联函数·仓颉编程语言·仓颉·仓颉语言
Wang's Blog2 小时前
Lua: 元表机制实现运算符重载与自定义数据类型
开发语言·lua
我找到地球的支点啦2 小时前
Matlab系列(006) 一利用matlab保存txt文件和读取txt文件
开发语言·算法·matlab
-森屿安年-2 小时前
STL中 Map 和 Set 的模拟实现
开发语言·c++