JDK8新特性-Optional

发布时间:2026/7/30 18:18:53
JDK8新特性-Optional 从 JDK7 的痛点理解 Optional一、JDK7 时代我们是怎么处理可能为空的值的假设你有一个方法根据用户 ID 查询用户名// JDK7 风格 public String getUserName(Long userId) { User user userDao.findById(userId); // 可能查不到返回 null return user.getName(); // 如果 user 是 null这里直接抛 NullPointerException }这段代码在运行时如果userId不存在findById返回null然后user.getName()就会抛出NullPointerExceptionNPE。所以 JDK7 的写法必须加上 null 检查public String getUserName(Long userId) { User user userDao.findById(userId); if (user ! null) { return user.getName(); } return 未知用户; }这看起来还行对吧但如果层级变深呢痛点 1多层嵌套的 null 检查金字塔灾难假设你要获取用户的收货地址中的城市名称public String getUserCity(Long userId) { User user userDao.findById(userId); if (user ! null) { Address address user.getAddress(); if (address ! null) { String city address.getCity(); if (city ! null) { return city; } } } return 未知城市; }这种代码被称为箭头代码或死亡金字塔缩进越来越深可读性极差核心业务逻辑return city被埋在层层判断的最深处痛点 2null 的含义模糊不清在 JDK7 中一个方法返回null调用者根本搞不清楚是查不到数据所以返回 null还是内部出错了所以返回 null还是参数传错了所以返回 nullnull 成了一个万能返回值但每个调用者都得猜它的含义然后自己写防御代码。痛点 3容易忘记检查 null人不是机器总会忘记写if (x ! null)。一个项目里几百个接口只要有一个人忘记检查线上就可能出现 NPE。而且NPE 是运行时异常编译器不会提醒你只有跑到那行代码、恰好那个值是 null 时才会爆炸。这意味着很多 NPE bug 要上线后才发现。二、Optional 的设计初衷让可能为空这件事无法被忽略Java 8 引入OptionalT核心目的只有一个在类型层面强制声明这个值可能不存在让编译器和代码结构帮你记住这件事而不是靠程序员的记忆力。Optional 是什么你可以把它理解为一个包装盒子OptionalUser 一个盒子里面要么有一个 User 对象要么是空的一旦某个方法返回了OptionalUser调用者看到这个返回值类型就立刻明白这里面的 User 可能不存在我必须处理不存在的情况。这和 JDK7 的区别在于JDK7JDK8 OptionalUser user dao.findById(id);返回值可能是 null但类型上看不出来OptionalUser user dao.findById(id);类型本身就告诉你可能为空忘记检查 null → 编译通过 → 运行时才 NPE拿到 Optional 后编译器逼你必须拆开盒子处理null 的含义模糊返回 Optional 明确表示查不到数据和异常区分开三、Optional 的基本操作从创建到使用3.1 创建 Optional三种方式方式一Optional.of(value)——确定有值时使用String name Alice; OptionalString opt Optional.of(name); // 正常 // 如果你传了 null 进去 OptionalString bad Optional.of(null); // 立即抛出 NullPointerException设计意图of()用于那些你百分之百确定不会为 null的场景。如果你误传了 null它立刻爆炸帮你把 bug 暴露在最早的地方而不是等到后面才 NPE。方式二Optional.ofNullable(value)——值可能为 null 时使用User user userDao.findById(100L); // 可能返回 null OptionalUser opt Optional.ofNullable(user);如果user是 nullopt就是一个空盒子如果有值就是一个装了这个值的盒子。这是从 JDK7 迁移到 Optional 时最常用的方法。方式三Optional.empty()—— 主动返回一个空盒子OptionalString opt Optional.empty();这相当于你自己构造一个无值的状态。通常在方法内部逻辑判断后决定返回空结果时使用。3.2 取出值为什么不要直接调用get()Optional 提供了一个get()方法取出里面的值OptionalString opt Optional.of(Hello); String value opt.get(); // Hello但是如果盒子是空的调用get()会抛出NoSuchElementException。这和 JDK7 的user.getName()在user为 null 时抛 NPE本质上是一个问题没有处理空的情况就直接拿值还是会爆炸。所以Java 8 设计 Optional 时重点不是让你用get()而是让你用下面这些方法安全地处理空盒子。3.3 安全取值orElse / orElseGet / orElseThroworElse(defaultValue)如果盒子为空返回默认值OptionalUser opt userDao.findById(100L); // 如果找到了返回 user.getName()如果没找到返回 未知用户 String name opt.map(User::getName).orElse(未知用户);设计意图给出一个兜底的值保证程序不会崩溃。orElseGet(Supplier)如果盒子为空延迟计算默认值String name opt.map(User::getName) .orElseGet(() - fetchNameFromCache()); // 只有为空时才执行 fetchNameFromCache为什么要设计orElseGet而不是只用orElse因为orElse的参数是立即求值的。比如// 无论 opt 是否为空createDefaultUser() 都会被执行 User user opt.orElse(createDefaultUser());如果createDefaultUser()内部要查数据库或做复杂计算这就白白浪费了性能。orElseGet的参数是一个 Lambda只有盒子真的为空时才会执行。orElseThrow()如果盒子为空抛出自定义异常User user opt.orElseThrow(() - new RuntimeException(用户不存在ID userId));设计意图有些业务场景下查不到本身就是错误不能默默给个默认值必须中断流程。这比 JDK7 时代手动if (user null) throw new ...更简洁。3.4 链式转换map 和 flatMap解决多层 null 检查的核心这是 Optional 最强大的地方专门用来解决前面提到的死亡金字塔问题。map()如果盒子有值就对其做转换返回新的 OptionalOptionalUser userOpt userDao.findById(100L); // 如果 userOpt 有值取出 user.getName()包装成 OptionalString OptionalString nameOpt userOpt.map(user - user.getName());注意map的返回值还是Optional所以你可以链式调用// 获取用户的地址的城市 String city userDao.findById(100L) // OptionalUser .map(user - user.getAddress()) // OptionalAddress .map(address - address.getCity()) // OptionalString .orElse(未知城市); // 如果任何一步为空直接返回默认值对比一下 JDK7 的写法// JDK76 行层层缩进 User user userDao.findById(100L); if (user ! null) { Address address user.getAddress(); if (address ! null) { String city address.getCity(); if (city ! null) { return city; } } } return 未知城市;Optional 的链式调用把横向的层层 if 判断转化成了纵向的流水线处理。每一步的map都隐含了一个逻辑如果上一步是空盒子这一步自动跳过不会抛 NPE。flatMap()当转换函数本身返回 Optional 时使用假设userDao.findAddress(userId)返回的是OptionalAddress而不是Address// ❌ 如果用 map会得到 OptionalOptionalAddress嵌套了 OptionalOptionalAddress nested userOpt.map(user - userDao.findAddress(user.getId())); // ✅ 用 flatMap会把两层 Optional 压平成一层 OptionalAddress flat userOpt.flatMap(user - userDao.findAddress(user.getId()));设计意图flatMap解决的是函数返回类型已经是 Optional时的嵌套问题保证链式调用的流畅性。3.5 过滤filter()OptionalString cityOpt userDao.findById(100L) .map(User::getAddress) .map(Address::getCity) .filter(city - !city.isEmpty()) // 如果城市名是空字符串变成空 Optional .orElse(未知城市);设计意图在链式流程中加入条件判断不满足条件就视为无值统一走orElse的兜底逻辑。四、一个完整的对比示例假设需求根据订单 ID 获取买家手机号如果找不到订单、找不到买家、或者买家没填手机号都返回未绑定。JDK7 写法public String getBuyerPhone(Long orderId) { Order order orderDao.findById(orderId); if (order ! null) { User buyer userDao.findById(order.getBuyerId()); if (buyer ! null) { String phone buyer.getPhone(); if (phone ! null !phone.isEmpty()) { return phone; } } } return 未绑定; }JDK8 Optional 写法public String getBuyerPhone(Long orderId) { return orderDao.findById(orderId) // OptionalOrder .map(order - userDao.findById(order.getBuyerId())) // 这里返回 OptionalUser .flatMap(userOpt - userOpt) // 压平嵌套的 Optional .map(User::getPhone) // OptionalString .filter(phone - !phone.isEmpty()) // 过滤空字符串 .orElse(未绑定); // 兜底 }或者如果order.getBuyerId()查用户也返回 Optionalpublic String getBuyerPhone(Long orderId) { return orderDao.findById(orderId) .flatMap(order - userDao.findById(order.getBuyerId())) .map(User::getPhone) .filter(phone - !phone.isEmpty()) .orElse(未绑定); }核心优势没有缩进金字塔— 代码是扁平的没有 null 关键字— 全程看不到! null的判断每一步的类型都是明确的— 你清楚地知道当前是OptionalOrder还是OptionalUser空值处理集中在最后— 所有找不到的情况统一在orElse一处处理五、设计 Optional 时的一些规矩Java 8 设计 Optional 时是有明确的使用规范的。违反这些规范就失去了它的意义。规矩 1不要把 Optional 当作方法参数// ❌ 错误示范 public void sendEmail(OptionalString email) { // ... } // ✅ 正确做法重载方法 public void sendEmail(String email) { // 正常发送 } public void sendEmail(String email, String defaultEmail) { // 带默认值的版本 }为什么因为Optional 是返回值类型的语义表示我被调用方可能给不出值。如果作为参数相当于强迫调用方去构造 Optional增加了调用方的负担本末倒置。规矩 2不要用isPresent()get()组合// ❌ 这样写和 JDK7 的 if-null 没区别失去了 Optional 的意义 OptionalUser opt userDao.findById(100L); if (opt.isPresent()) { User user opt.get(); System.out.println(user.getName()); } else { System.out.println(未知用户); } // ✅ 直接用 orElse 或 ifPresent opt.ifPresent(user - System.out.println(user.getName())); String name opt.map(User::getName).orElse(未知用户);规矩 3不要把 Optional 作为类的字段或集合元素// ❌ 不推荐 public class User { private OptionalString email; // 字段不要用 Optional private ListOptionalOrder orders; // 集合里也不要放 Optional } // ✅ 正确做法 public class User { private String email; // 字段保持 nullable public OptionalString getEmail() { return Optional.ofNullable(email); // 在 getter 中包装 } }为什么因为 Optional 不是为序列化设计的也不适合长期存储。它应该只存在于方法调用的局部流程中作为从查询到使用这个短暂过程中的类型约束。六、总结Optional 解决了什么问题JDK7 的问题Optional 的解决方案null 是隐形的类型上看不出来OptionalT类型本身就声明了可能为空容易忘记检查 null导致运行时 NPE强迫调用者通过orElse/orElseThrow等处理空的情况多层嵌套 if 判断代码臃肿链式map/flatMap调用扁平化处理流程null 含义模糊返回 Optional 明确表示无数据和异常、错误区分开Optional 的本质不是消灭 null而是把可能为空从隐式的约定变成显式的类型约束让编译器和代码结构帮你守住防线而不是靠人的记忆力。Map() 、flatMap()对Optinal的处理map和flatMap不只是集合专属Optional自己也有这两个方法名字一样但操作的对象完全不同。一、熟悉的mapStream/Collection 里的版本在 JDK7 里你操作集合是这样的ListString names Arrays.asList(Alice, Bob, Charlie); // JDK7循环遍历逐个转换 ListInteger lengths new ArrayList(); for (String name : names) { lengths.add(name.length()); // 对集合里的每个元素做转换 }JDK8 的 Stream 给你提供了map做的是同样的事ListInteger lengths names.stream() .map(name - name.length()) // 对 Stream 里的每一个元素做转换 .collect(Collectors.toList());这里的map操作的是一组数据输入 3 个字符串输出 3 个整数。二、Optional 的map操作的是一个值不是一组值Optional 不是集合它里面最多只装一个对象或者什么都没有。但 Optional 也提供了map它的逻辑和 Stream 的map语义是相通的如果容器里有值就对这个值做转换然后把结果重新包起来返回。只不过 Stream 里可能转换了 100 个元素Optional 里最多转换 1 个元素。看Optional.map的源码逻辑我简化后public U OptionalU map(Function? super T, ? extends U mapper) { if (!isPresent()) { // 如果盒子是空的 return empty(); // 直接返回空盒子 } else { return Optional.ofNullable(mapper.apply(value)); // 把值拿出来转换再包成新盒子 } }对比一下你就明白了Stream.mapOptional.map操作对象Stream 里的多个元素Optional 里的一个值或没有转换次数0 次到 N 次0 次或 1 次返回值新的 Stream新的 Optional空的情况空 Stream 直接返回空 Stream空 Optional 直接返回空 Optional三、代码对比让你看到本质区别// Stream.map处理多个 ListString names Arrays.asList(Alice, Bob); ListInteger result names.stream() .map(name - name.length()) // Alice→5, Bob→3转换了两次 .collect(Collectors.toList()); // 结果[5, 3] // Optional.map处理一个或零个 OptionalString nameOpt Optional.of(Alice); OptionalInteger result nameOpt .map(name - name.length()); // Alice→5只转换了一次 // 结果Optional[5] // 如果 Optional 是空的 OptionalString emptyOpt Optional.empty(); OptionalInteger result2 emptyOpt .map(name - name.length()); // 发现是空的跳过转换直接返回 Optional.empty // 结果Optional.empty所以回到你的问题userDao.findById(100L)不是得到的一个 user 对象吗怎么能用 map 处理呢它返回的不是User而是OptionalUser。这个Optional是一个容器盒子map是这个盒子提供的方法用来转换盒子里的那个User。OptionalUser userOpt userDao.findById(100L); // 这是一个盒子 OptionalString nameOpt userOpt.map(user - user.getName()); // ↑ 盒子 ↑ 盒子的方法 ↑ 盒子里的 User 对象四、为什么要用同一个名字mapJava 8 引入函数式编程时设计了一个统一的概念叫Functor函子。理解它的核心思想任何一个能装东西的容器都应该提供map方法让你对里面的内容做转换而不用关心容器本身的结构。在 Java 8 里至少有这几个类实现了这个思想类容器里装的是什么map 的作用StreamT0 到 N 个 T对每个 T 做转换OptionalT0 个或 1 个 T对那个 T 做转换如果有的话CompletableFutureT异步计算未来的一个 T对计算结果 T 做转换它们都叫map是因为语义完全一致容器负责保管值你负责告诉我怎么转换值容器负责把转换后的结果重新包起来。这样你学会了一个类的map其他类的map自然就懂了。五、flatMap同理解决套娃问题你熟悉的情况可能是ListListInteger nested Arrays.asList( Arrays.asList(1, 2), Arrays.asList(3, 4) ); // flatMap把嵌套的 List 压平成一个 List ListInteger flat nested.stream() .flatMap(list - list.stream()) // ListList → List .collect(Collectors.toList()); // 结果[1, 2, 3, 4]Optional 的flatMap也是同样的语义压平嵌套的 Optional。// 假设 userDao.findById 返回 OptionalUser // 假设 addressDao.findByUserId 返回 OptionalAddress OptionalUser userOpt userDao.findById(100L); // ❌ 如果用 map会得到 OptionalOptionalAddress套了两层盒子 OptionalOptionalAddress nested userOpt.map( user - addressDao.findByUserId(user.getId()) ); // ✅ 用 flatMap会把两层压平成一层 OptionalAddress OptionalAddress flat userOpt.flatMap( user - addressDao.findByUserId(user.getId()) );对比Stream.flatMapOptional.flatMap解决什么问题ListListT压平成ListTOptionalOptionalT压平成OptionalT参数要求返回 Stream 的函数返回 Optional 的函数核心动作把多个小 Stream 合并成一个大 Stream把外层 Optional 和内层 Optional 合并成一个六、一句话总结map/flatMap不是集合的专利它是容器的通用操作。Stream 是装多个值的容器map转换每一个值。Optional 是装零个或一个值的容器map转换那个值如果有的话。