ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

一个大佬说,Java8的Optional是个鸡肋,我怒了!

一个大佬说,Java8的Optional是个鸡肋,我怒了! 一、先别急着喷我们聊聊这场争论到底在吵什么如果你在技术群里待得够久一定见过这样的名场面有人贴出一段层层嵌套的判空代码配上一句“写成这样Java 不得背锅”紧接着就有人跳出来说“所以 Java 8 的 Optional 就是个鸡肋食之无味、弃之可惜用了反而更难看。”然后一群人点赞附和仿佛 Optional 生来就是原罪。我必须承认第一次听到“Optional 是鸡肋”这个说法时我也差点信了。毕竟刚接触它的时候我确实写出过比原来更丑陋的代码到处if (optional.isPresent())、到处optional.get()原本两三行判空硬是被我拉成了五六行包装。那一刻我甚至怀疑Java 引入 Optional 是不是只是为了给面试官多一个考点。但后来我慢慢意识到一个关键问题当我们说一个工具“没用”的时候很多时候并不是工具没用而是我们把它当成了另一种工具的替代品。把 Optional 当成“花式判空语法糖”然后把判空逻辑原样搬进去当然会觉得它鸡肋。这就像你买了一台洗碗机却偏偏用手把碗一个个搓干净再放进去当碗柜用最后得出结论洗碗机还不如手洗快。这篇文章不是要给你洗脑“Optional 天下第一”也不是要和那位大佬抬杠到脸红脖子粗。我会把 Optional 的设计初衷、底层实现、正确用法、常见误用、性能代价、实战边界全部摊开讲清楚。讲完之后你可以继续不喜欢它但至少你会明白它到底解决什么问题、在什么场景下好用、在什么场景下确实不该用。这才是一个工程师面对技术争议时该有的态度而不是跟着一句“鸡肋”就人云亦云。下面是这篇文章的整体结构你可以按需跳读Optional 的诞生背景它到底想解决什么Optional 核心 API 逐一拆解那些让 Optional 背上“鸡肋”骂名的错误用法Optional 的正确打开方式链式、组合与缩略语义深入源码看懂 Optional 到底做了什么Optional 的性能真相它真的拖慢你的系统吗Optional 的边界什么时候确实不该用它与其他空值处理方案的横向对比实战把一段丑陋判空代码重构干净总结Optional 是不是鸡肋最终取决于你系好安全带我们正式开始。二、Java 为什么需要 Optional十亿美金的错误与空指针的诅咒要理解 Optional必须先理解一个更古老、更深刻的敌人null。计算机科学史上有一个著名的典故图灵奖得主 Tony Hoare 在 2009 年的一次演讲中把 1965 年引入空引用的决定称为自己的“十亿美元错误”。他当时原话的大意是引入 null 引用是一个很容易做出的决定因为它实现起来太方便了但这么多年过去它导致了无数的错误、漏洞和系统崩溃累计造成的损失可能高达十亿美元。为什么 null 这么危险核心问题在于类型系统不知道一个引用到底“可能是空”还是“一定非空”。看下面这段代码public String getUserCity(User user) { return user.getAddress().getCity(); }编译器看到user.getAddress()返回一个Address类型于是放心地认为它就是一个Address。但它不知道真实运行时这个返回值可能是null。于是你只能在运行时收获一个NullPointerException运气好的话日志里还能看到一堆堆栈运气不好它直接在生产环境把用户请求打挂。更讽刺的是这个错误往往发生在离真正根因很远的地方明明是最上游某个字段没塞值异常却爆发在最下游的调用链末端。于是工程师们发明了各种防御手段最经典的莫过于“if 判空地狱”public String getUserCity(User user) { if (user null) { return 未知城市; } Address address user.getAddress(); if (address null) { return 未知城市; } String city address.getCity(); if (city null) { return 未知城市; } return city; }这段代码没有错甚至可以说是“稳健”的。但问题在于它把“业务主线”和“防御噪音”搅在了一起。读者必须穿过三层 if 才能看清这段方法真正想干什么。而且这种判空是纯靠程序员自觉的你今天记得判断 user 是否为空明天接手的人可能就忘了判断 address 是否为空。其他语言是怎么解决这个问题的Scala 有Option[T]Haskell 有MaybeKotlin 有可空类型和?.、?:运算符。它们的共同思路是把“可能为空”这件事显式地编码到类型里逼着你处理它或者让你用更优雅的方式处理它。Java 8 的 Optional 正是沿着这条思路诞生的。它的目标不是消灭 null——null 在 Java 里已经根深蒂固也不是给你一个绝对安全的魔法盒而是提供一种类型层面的信号当一个方法的返回类型是OptionalT而不是T时它在明确告诉你——“我这里可能没有值请你有意识地处理这种可能。”所以 Optional 的第一个关键价值其实是契约显式化。它把原来写在 Javadoc 注释里、邮件里、口头约定里的那句“这个方法可能返回 null”变成了编译器可见的类型签名。这是理解 Optional 的起点也是后面所有讨论的地基。离开这个地基去争论 Optional 鸡不鸡肋就像离开红绿灯去争论斑马线有没有用一样讨论从一开始就跑偏了。三、先把 API 摸透Optional 到底给了我们哪些武器很多人对 Optional 的负面印象源于只用了它两三个方法却误以为那就是全部。实际上 Optional 的 API 虽然不多但设计得相当克制且有层次。我们先完整过一遍后面讲用法时会反复用到。3.1 构造方法如何得到一个人 OptionalOptional 的核心构造方式有三种// 1. 明确知道有值 OptionalString opt1 Optional.of(hello); // 2. 值可能为空安全构造 OptionalString opt2 Optional.ofNullable(maybeNull); // 3. 明确表示空 OptionalString opt3 Optional.empty();这里有第一个高频踩坑点Optional.of(null)会直接抛出NullPointerException。所以of适合你“确信不可能为 null”的场景而ofNullable才是日常处理外部输入、数据库查询结果、接口返回值时的常用入口。如果你一个方法内部要返回 Optional最常见的写法是public OptionalString findName(Long id) { String name nameDao.findNameById(id); return Optional.ofNullable(name); }这样做的好处是调用方一眼就知道结果可能为空而不用再去翻方法实现或者 Javadoc。3.2 取值的四种姿势Optional 让你“拿到值”的方式有好几种选择不同代码的安全性和颜值完全不同。OptionalString opt Optional.of(value); // 危险但直接不存在时抛 NoSuchElementException String v1 opt.get(); // 存在则执行不存在什么都不做 opt.ifPresent(v - System.out.println(v)); // 存在返回原值不存在返回兜底值 String v2 opt.orElse(default); // 存在返回原值不存在才去执行 Supplier 生成兜底值 String v3 opt.orElseGet(() - computeDefault()); // 存在返回原值不存在抛出你指定的异常 String v4 opt.orElseThrow(() - new IllegalStateException(缺少值)); // Java 9 起可用不存在则尝试从另一个 Optional 兜底 String v5 opt.or(() - Optional.of(fallback)).orElse(final);注意get()的行为如果 Optional 为空它会抛出NoSuchElementException。从语义上说这其实只是把NullPointerException换成了另一个异常如果你拿到 Optional 后直接get()那确实谈不上多大进步。这一条我们后面会重点展开因为“鸡肋论”的很大一部分火力都来自对get()的滥用。3.3 判断是否存在的两种写法boolean empty opt.isEmpty(); // Java 11 起可用 boolean present opt.isPresent(); // 判断是否有值这里要特别提醒isPresent()本身不是坏方法坏的是你把它用成了 if 判空的替身。稍后第三、四章会给出大量对比。3.4 映射与过滤Optional 真正精彩的部分如果说前面的方法是 Optional 的“四肢”那么下面这几个方法就是它的“灵魂”// map值存在时做转换返回新的 Optional OptionalString upper opt.map(String::toUpperCase); // flatMap值存在时做转换但转换函数本身返回 Optional避免嵌套 OptionalInteger lengthOpt opt.flatMap(s - Optional.of(s.length())); // filter值存在且满足条件才保留否则变空 OptionalString filtered opt.filter(s - s.startsWith(A)); // Java 10 起值存在或不存在都能执行的动作 OptionalString afterPeek opt .map(String::trim) .or(() - Optional.of(default)) ;map和flatMap是 Optional 能写出优雅链式调用的关键。它们让你可以在完全不写 if 的情况下对“可能存在也可能不存在”的值做一连串转换。而filter则负责把“某个条件不满足”也翻译成“空”从而继续被后面的orElse或ifPresent统一处理。把这一整套组合起来就可以写出非常流畅的链路。下面是一个常见的例子public String resolvePhoneLabel(User user) { return Optional.ofNullable(user) .map(User::getProfile) .map(Profile::getPhone) .filter(Objects::nonNull) .map(Phone::getLabel) .orElse(未设置标签); }这段代码从头到尾没有一个 if却完整地处理了 user、profile、phone 三个环节可能为空的情况。如果你愿意还可以继续往下链。这就是 Optional 与命令式判空最本质的区别它把“防御空值”从控制流语句变成了数据流表达。四、为什么总有人说它鸡肋那些年我们踩过的 Optional 误用现在我们来直面那位大佬的吐槽。他为什么觉得 Optional 鸡肋大概率是因为见到了下面这些代码。我先把这些“反模式”一个个揪出来再告诉你正确的替代写法。4.1 反模式一isPresent get换汤不换药这是 Optional 被骂得最惨的用法没有之一public String badExample(OptionalUser userOpt) { if (userOpt.isPresent()) { User user userOpt.get(); return process(user); } return default; }乍一看好像挺安全但仔细想想这不就是把原来的if (user ! null)换成了if (userOpt.isPresent())再把user换成userOpt.get()吗判空逻辑一点没少反而多包了一层盒子。更糟的是如果哪天有人忘了先isPresent()就直接get()还会得到一个NoSuchElementException。这段代码的正确写法是public String goodExample(OptionalUser userOpt) { return userOpt .map(this::process) .orElse(default); }同样是“有值就处理没值给默认”后者把意图表达得清清楚楚。所以如果你发现自己写 Optional 时总是不自觉地出现isPresent()后面跟一个get()先别急着骂 Optional 鸡肋先问问自己是不是在用命令式思维套函数式 API。4.2 反模式二把 Optional 当方法参数传来传去这是另一个非常常见的误用public void saveUser(OptionalUser userOpt) { // 调用方还得先包一层 }一旦你开了这个头整个调用链都会被迫 Optional 化调用方要Optional.ofNullable(user)方法内部又要拆盒子。更尴尬的是调用方完全可以传一个null进来于是你辛辛苦苦做的类型安全瞬间被击穿——因为 Java 编译器并不能阻止别人把 null 塞进一个OptionalUser参数里。所以社区公认的一条准则是Optional 主要用在返回值上不要用在方法参数上也不要用在字段上。方法参数照常用普通类型字段照常用普通类型加必要的判空约定。Optional 的价值在于告诉“调用方”结果可能为空而参数和字段的“可能为空”更适合用别的机制表达比如Nullable注解、构造函数校验或者领域模型设计。4.3 反模式三在实体字段和序列化场景里塞 Optional有些人学了 Optional 之后走火入魔恨不得把所有字段都改成 Optionalpublic class User { private OptionalString nickname; // 不建议 private OptionalAddress address; // 不建议 }这会带来一连串麻烦。首先Optional 本身不是为了作为字段存储而设计的它会增加对象的内存开销其次很多 JSON 序列化框架对 Optional 的支持并不好Jackson 虽然能做一定处理但依赖版本、配置和序列化结果都容易让人踩坑最后实体字段的可空性通常有更清晰的表达方式比如NOT NULL约束、默认值、构造器参数校验等。把 Optional 写进实体往往是把“返回值语义”和“数据模型”混为一谈。4.4 反模式四orElse 里写高成本计算先看一个非常隐蔽的性能坑public String getName(OptionalUser userOpt) { // 危险无论 userOpt 是否有值buildDefaultName() 都会先执行 return userOpt.map(User::getName) .orElse(buildDefaultName()); }很多初学者以为orElse是“没有值的时候才调用兜底”但实际上orElse的参数是一个已经计算好的值。也就是说在代码执行到orElse之前buildDefaultName()就已经被调用了。如果这个兜底函数涉及查数据库、调远程接口或者复杂计算你就白白付了一次性能代价尽管最后根本没用到它。正确做法是使用orElseGet它接收的是一个Supplier只有在真正需要兜底时才会执行public String getName(OptionalUser userOpt) { return userOpt.map(User::getName) .orElseGet(this::buildDefaultName); }记住这条规则兜底值是常量就用 orElse兜底值需要计算就用 orElseGet。这个细节如果搞反了生产环境里可能藏着不少无谓的开销。4.5 反模式五Optional 里再套 Optional有时候你会写出或遇到这样的类型OptionalOptionalString bad Optional.of(Optional.of(x));这几乎没有任何现实意义只会让下游代码陷入两层get()或者flatMap纠缠。出现这种类型通常说明某一步的转换逻辑没有理顺该用map的地方用了flatMap或者某个方法明明返回 Optional却又被外面包了一层 Optional。修复的思路不是“再拆一层”而是重新审视这条链路的类型设计。4.6 反模式六为了用而用无意义的包装还有一种情况是纯属形式主义public OptionalString doSomething() { String result compute(); if (result null) { return Optional.empty(); } return Optional.of(result); }这段代码本身不算错但如果compute()内部的结果本来就是“有则返回、无则返回 null”那你完全可以直接return Optional.ofNullable(compute());。如果再极端一点某些局部变量、循环内部也动不动就包一层 Optional就会让代码显得臃肿。Optional 应当被用在类型边界的语义表达上而不是每一个可能为空的局部值上。五、Optional 正确的打开方式四类高频实战场景看完反模式我们来看看 Optional 真正能发光的场景。我总结了四类覆盖了绝大多数日常开发。5.1 场景一层层导航的对象图取值这是 Optional 最经典、最高频的用途。比如从用户到地址再到城市的路径public String getCity(User user) { return Optional.ofNullable(user) .map(User::getAddress) .map(Address::getCity) .orElse(未知城市); }与前面第二节那段三层 if 相比这段代码的意图是不是一目了然它的好处还不只是好看。因为 Optional 的每一层map都自动处理了“上一步为空就短路为空”的逻辑所以无论user、address哪个环节是 null最终都会安全地落到orElse的兜底值上再也不会出现“少判断了一层导致 NPE”的问题。5.2 场景二查询单条数据可能查不到数据库查询、缓存读取、配置项获取都是典型的“可能没有结果”场景。过去我们通常返回 null然后靠 Javadoc 或者约定来提醒调用方判空。用 Optional 之后契约直接写在了类型上public OptionalOrder findOrder(String orderId) { return Optional.ofNullable(orderDao.selectById(orderId)); }调用方则可以这样自然地处理public OrderStatus queryStatus(String orderId) { return orderService.findOrder(orderId) .map(Order::getStatus) .orElse(OrderStatus.NOT_FOUND); }这种用法在 DAO 层、Service 层尤其受欢迎。它逼迫调用方在编译期就意识到“这个查询可能查不到”从而减少一路 null 传递到控制器层才爆发的问题。5.3 场景三条件过滤后取第一个满足条件的值Optional 和 Stream 可以天然地组合在一起。比如从列表中找到第一个满足条件的元素public OptionalProduct findFirstInStock(ListProduct products) { return products.stream() .filter(p - p.getStock() 0) .findFirst(); }注意Stream.findFirst()和Stream.findAny()返回的正是 Optional。拿到之后你可以继续链式处理而不必先判空再操作public String firstInStockName(ListProduct products) { return products.stream() .filter(p - p.getStock() 0) .findFirst() .map(Product::getName) .orElse(暂无库存商品); }Optional 与 Stream 的组合让“过滤、取值、兜底”这一整套逻辑可以被表达成一条顺畅的流水线。5.4 场景四校验与业务规则的函数式表达Optional 的filter方法非常适合表达“满足条件才继续”的业务规则。比如用户登录后只有当用户是 VIP 时才返回专属权益public OptionalBenefit getVipBenefit(User user) { return Optional.ofNullable(user) .filter(User::isVip) .map(this::buildBenefit); }如果用户为空或者不是 VIP最终都会得到空 Optional调用方再决定兜底策略。这样“身份校验”和“权益构建”被清晰地分隔开业务规则也更容易阅读和维护。六、翻开源码Optional 内部到底做了什么很多人用了很久 Optional却从没看过它的源码。其实 Optional 的源码非常短核心实现思路也极其简单。看懂它你会对它的性能和行为边界有更踏实的把握。6.1 Optional 的本质一个不可变的容器Optional 本质上就是一个 final 的容器类内部只有一个字段public final class OptionalT { private static final Optional? EMPTY new Optional(); private final T value; private Optional() { this.value null; } private Optional(T value) { this.value Objects.requireNonNull(value); } // 其余方法略 }它把所有可能为 null 的复杂度都收敛到了构造阶段空的 Optional 内部 value 就是 null但对外永远不暴露这个 null有值的 Optional 在构造时就用Objects.requireNonNull保证 value 非空。所以任何一个合法构造出来的 Optional要么是“空容器”要么是“装着非空值的容器”不会出现“容器本身是 null”或者“容器装着 null”的混乱状态。6.2 empty、of、ofNullable 的实现三个构造入口逻辑非常直观public staticT OptionalT empty() { SuppressWarnings(unchecked) OptionalT t (OptionalT) EMPTY; return t; } public static T OptionalT of(T value) { return new Optional(value); } public static T OptionalT ofNullable(T value) { return value null ? empty() : of(value); }empty()直接返回全局单例EMPTY所以创建空 Optional 几乎零成本不会反复 new 对象。这也说明在代码里到处写Optional.ofNullable(null)并不可怕它最终只会返回那个共享的单例。6.3 map 与 flatMap 的实现这两个方法是 Optional 链式调用的核心源码同样很朴素publicU OptionalU map(Function? super T, ? extends U mapper) { Objects.requireNonNull(mapper); if (!isPresent()) { return empty(); } else { return Optional.ofNullable(mapper.apply(value)); } } publicU OptionalU flatMap(Function? super T, OptionalU mapper) { Objects.requireNonNull(mapper); if (!isPresent()) { return empty(); } else { return Objects.requireNonNull(mapper.apply(value)); } }注意map的返回用Optional.ofNullable包装也就是说即使你的转换函数返回了 nullmap也会把它安全地变成空 Optional而不会把 null 泄漏出去。相比之下flatMap不允许转换函数返回 null否则会直接抛NullPointerException。这个细节在调试链式调用时很有用看到 NPE 先别忙着怪 Optional先检查flatMap里的转换函数是不是返回了 null。6.4 orElse 与 orElseGet 的差别根源看一眼方法签名前面 4.4 节那个“性能坑”就一目了然了public T orElse(T other) { return value ! null ? value : other; } public T orElseGet(Supplier? extends T other) { return value ! null ? value : other.get(); }orElse接收的是一个已经求值完毕的other所以不管 Optional 有没有值other这个参数都必须在调用前先被算出来orElseGet接收的是一个Supplier只有当value null时才会执行它的get()。源码面前一切“我以为”都无处遁形。七、Optional 真的慢吗性能真相与基准思考“性能差”也是 Optional 经常被贴的标签。有人言之凿凿地说Optional 会拖慢系统、增加 GC 压力。这个说法到底有多少水分我们先定性分析再给出实践建议。Optional 的成本主要来自两个地方。第一创建 Optional 对象本身会有轻微的对象分配开销尤其是Optional.of(value)每次都会 new 一个对象。第二链式调用中的 Lambda 表达式如果发生了装箱、拆箱会额外产生Integer、Long等包装对象。但这是不是意味着 Optional 会让你的接口变慢绝大多数情况下不会。原因有三其一现代 JIT 编译器的逃逸分析能力很强。像 FunctionalInterface 的 Lambda、生命周期很短的 Optional 对象在很多热路径上可以被栈上分配Scalar Replacement根本不会进入堆也不产生 GC 压力。你写出来的链式调用经过 JIT 优化后可能比手写 if 判空多不了几个指令。其二真正决定性能的通常是 IO、网络、数据库和算法复杂度。一次远程调用动辄几毫秒甚至几百毫秒而 Optional 带来的纳秒级开销在这条成本曲线上几乎可以忽略。为一个 DAO 查询包装 Optional比起这次查询本身的开销连零头都算不上。把性能优化的枪口对准 Optional多半是打错了靶子。其三可读性和可维护性也是成本而且是更贵的成本。一段嵌套判空代码可能当年写的时候快几个纳秒但下个维护者看错一行判断补丁打出生产事故损失的是几个小时甚至几天的排障时间。做性能优化要讲 ROI把 Optional 在普通业务链路上的开销当作性能瓶颈往往是优化错了方向。当然Optional 确实有它不适合的性能敏感场景。比如极高频的底层工具方法、纳秒级调优的核心循环、追求零对象分配的框架内部实现。在这些场景里你完全可以不用 Optional老老实实判空或者用Nullable注解。但这不代表 Optional 在更广阔的业务开发场景下就是“性能灾难”。工具要放到合适的场景里而不是用一个极端场景否定它的全部。八、恰恰相反Optional 的边界以及什么时候真的不该用为了避免这篇文章变成无脑吹我们也要坦率地承认 Optional 的局限。搞清楚边界反而能让你用得更自信。8.1 不适合作为实体字段和序列化对象前面已经提过Optional 字段会带来内存开销、序列化兼容问题和模型语义混乱。实体的“字段可能为空”是数据模型的属性应该用数据库约束、默认值、构造器校验或领域语义来表达而不是塞一个包装类型进去。8.2 不适合作为方法参数方法参数本身不需要 Optional 来提醒调用方“可能为空”因为调用方的空值问题应该由它自己处理。而且 Optional 参数很容易被传入 null破坏类型安全。方法重载、参数校验、Nullable注解是更适合的方案。8.3 不适合超大集合或原生类型转换Optional 没有为int、long、double提供专门的 OptionalInt、OptionalLong、OptionalDouble 之外的更多类型而且这些专门的 Optional 也无法和普通 Optional 统一使用。如果你在极高频的数值计算里频繁装箱拆箱Optional 可能会放大这种开销。这种场景下优先考虑 Stream 的原生特化或位运算标记。8.4 不适合表达“多种空原因”Optional 只能表达“有值”和“没值”两种状态。如果你的查询失败需要区分“未找到”“权限不足”“系统异常”等多种原因Optional 就力不从心了。这时更适合返回一个结果对象Result、Either 风格或者抛出自定义异常。Optional 面向的是“可能没有结果”这种二值语义而不是完整的错误处理模型。承认这些边界并不削弱 Optional 的价值反而让它更清晰它是一个返回值语义工具不是万能空值解决方案。它解决的是“调用方不知道返回值可能为空”这个最普遍的问题而不是所有和 null 有关的问题。九、Optional 之外的方案对比它到底是不是最优解我们把目光拉远一点看看在 Java 生态里处理空值的几种主流方案Optional 究竟处在什么位置。9.1 方案 A传统 if 判空优点没有额外依赖所有 Java 程序员都看得懂性能最好。缺点判空逻辑和业务逻辑缠绕容易遗漏判断代码可读性随层级增加急剧下降。它适合简单场景但在多层对象导航和跨层传递中会越来越吃力。9.2 方案 BObjects.requireNonNull 与参数校验这是“快速失败”思想的代表在方法入口或构造器里对关键参数做非空校验null 一旦出现立刻抛异常把问题暴露在最早的位置。它和 Optional 并不冲突反而互补参数用 requireNonNull 校验返回值用 Optional 表达。9.3 方案 CNullable / NonNull 注解借助 IDE 和静态分析工具如 IntelliJ IDEA、SpotBugs、Checker Framework注解可以在编译期或编写期给出空值警告。它的优点是不改变运行时行为、零性能开销缺点是约束力依赖开发者和工具链容易“君子协议”且无法表达“这个值可能为空请优雅处理”的链式语义。9.4 方案 DKotlin 可空类型Kotlin 通过String?、?.、?:、let等语法在语言层面把空安全做成了原生能力。相比 Optional它更简洁、更自然而且是编译器强制。但这是语言层面的差异不是 Java 8 项目里可以随手切换的选项。对还在用 Java 的团队来说Optional 是语言范围之内最接近这种思想的选择。综合来看Optional 不是空值处理的“终极答案”但它是在 Java 8 及更高版本中把“可能为空”显式化并支持函数式链式处理的最直接、最无入侵的方案。它和注解、参数校验、异常设计配合使用能形成一套相当完整的空值治理体系。十、实战重构把一段真实判空代码改造成优雅链路道理讲了一堆最后我们用一段近真实的业务代码收个尾。假设我们要根据用户查询其所属企业的法人联系方式用来发送通知。最初的代码可能是这样的public String getLegalPersonPhone(User user) { if (user null) { return 暂无联系方式; } Company company user.getCompany(); if (company null) { return 暂无联系方式; } LegalPerson legalPerson company.getLegalPerson(); if (legalPerson null) { return 暂无联系方式; } String phone legalPerson.getPhone(); if (phone null || phone.isEmpty()) { return 暂无联系方式; } return phone; }这段代码没有功能 bug但可读性已经被四层 if 拖垮。更重要的是空值兜底逻辑重复了四次将来如果要修改兜底文案得一处处改。用 Optional 重构之后public String getLegalPersonPhone(User user) { return Optional.ofNullable(user) .map(User::getCompany) .map(Company::getLegalPerson) .map(LegalPerson::getPhone) .filter(phone - !phone.isEmpty()) .orElse(暂无联系方式); }对比之下重构后的代码有几个明显的优势其一用户、企业、法人的空值判断被统一短路处理其二手机号为空的业务规则通过filter表达得很清楚其三兜底文案只出现一次后续好维护其四整条链路像流水线一样从左到右读下来就是完整的业务过程。如果业务再复杂一点比如“法人联系方式取不到时再尝试取公司客服电话”Optional 依然能从容应对public String getContactPhone(User user) { return Optional.ofNullable(user) .map(User::getCompany) .flatMap(company - Optional.ofNullable(company.getLegalPerson()) .map(LegalPerson::getPhone) .filter(p - !p.isEmpty()) .or(() - Optional.ofNullable(company.getServicePhone())) ) .orElse(暂无联系方式); }这段代码展示了一个非常重要的事实Optional 能表达的逻辑比 if 判空丰富得多。它不光处理“有没有值”还能通过filter、or、flatMap把业务回退策略也编织进类型安全的链式表达里。这才是它区别于传统判空的真正价值。十一、为什么我们总是急着骂一个工具“鸡肋”聊完技术我想说点题外话因为这可能才是这场争论真正的根源。“鸡肋”这个词在技术圈出现频率极高。有人说 Optional 鸡肋有人说设计模式鸡肋有人说单元测试鸡肋有人说注释鸡肋甚至有人说 DRY 原则鸡肋。每次争论到最后真正的问题往往不是那个工具本身而是我们讨论问题时习惯性地抽离了“场景”。一个工具有没有用从来不是孤立地看它本身而是看它在什么约束下、解决什么问题、替代什么方案。脱离场景谈“有用没用”就像脱离菜谱争论“盐是不是鸡肋”。盐本身没有味道好坏放对地方是点睛之笔放错地方就是灾难。Optional 也是如此。那位大佬说 Optional 是鸡肋我某种程度上理解他如果他见过的 Optional 全都是isPresent get的换皮判空如果他所在的项目因为 Optional 泛滥导致代码又臭又长那他说它是鸡肋是基于他真实体感的判断并不是毫无道理。但我依然要“怒”一下——不是怒他不懂 Optional而是怒一种风气用一个片面的、被误用的样本去给一个工具下一个盖棺定论的标签然后让更多还没深入了解的人望而却步。这种标签式结论传播得比真正的知识快得多。于是很多新人还没弄明白 Optional 的 map 和 flatMap就先把“Optional 鸡肋”这句话背了下来。这才是最可惜的地方。技术判断最重要的一点是把“我见过它被用得很烂”和“它本身很烂”分开。前者说明用法需要纠正后者才说明工具需要淘汰。对 Optional 来说显然属于前者。十二、总结Optional 是不是鸡肋不取决于它而取决于你回到最初的问题Java 8 的 Optional 到底是不是鸡肋我的结论是它不是鸡肋但它也不是免死金牌。它是一个设计克制、语义清晰的工具专门用来把“返回值可能为空”这件事从约定升级为类型契约。用得对它能显著减少空指针隐患、提升链式处理代码的可读性用得错它的确会成为又臭又长的垃圾代码甚至比判空更啰嗦。如果你记不住那么多细节至少带走下面这几条实用准则Optional 主要用于方法返回值不要用在字段和方法参数上。拿到 Optional 后优先用map、flatMap、filter、orElse、orElseGet、ifPresent链式处理尽量避免isPresent() get()的组合。兜底值是常量用orElse兜底值需要计算用orElseGet。查询类方法返回OptionalT让“可能查不到”成为类型的一部分。需要区分多种失败原因时不要硬塞 Optional改用结果对象或异常。做性能优化时先测再优化别在业务链路上和小对象分配较劲。说到底编程语言和它的标准库只是给我们提供了原材料。同样的 JDK有人能写出整洁如诗的系统也有人能写成一团乱麻。Optional 这玩意儿你把它当判空语法糖它就是个鸡肋你把它当类型契约和函数式管道的入口它就是个好工具。下次再有人拍着桌子说“Optional 是鸡肋”别急着点赞也别急着对喷。你可以把这篇文章甩过去然后问问对方你见过用对了的 Optional 长什么样吗
返回列表