资讯专栏INFORMATION COLUMN

【修炼内功】[Java8] 使用Optional的正确姿势及序列化问题

Ajian / 1277人阅读

摘要:本文已收录修炼内功跃迁之路的为解决空的问题带来了很多新思路,查看源码,实现非常简单,逻辑也并不复杂。

本文已收录【修炼内功】跃迁之路

Java8的Optional为解决"空"的问题带来了很多新思路,查看Optional源码,实现非常简单,逻辑也并不复杂。Stuart Marks在其一次演讲中花了约1个小时的时间来讲述如何正确的使用Optional (Optional - The Mother of All Bikesheds by Stuart Marks),也有人调侃道1 hour for Optional, you gotta be kidding me.使用Optional不难,但用好Optional并不容易

Stuart Marks在演讲中提到了Optional的基本作用

Optional is intended to provide a limited mechanism for library method return types where there is a clear need to represent "no result", and where using null for that is overwhelmingly likely to cause errors.

在以往的编程模型中,对于“没有内容”,大多数情况需要使用null来表示,而null值总是被人忽略处理(判断),从而在使用过程中极易引起NPE异常

Optional的出现并不是为了替代null,而是用来表示一个不可变的容器,它可以包含一个非null的T引用,也可以什么都不包含(不包含不等于null),非空的包含被称作persent,而空则被称作absent

本质上讲Optional类似于异常检查,它迫使API用户去关注/处理Optional中是否包含内容,从而避免因为忽略null值检查而导致的一些潜在隐患

假设有一个函数用来根据ID查询学生信息public Student search(Long id),现在有一个需求,需要根据ID查询学生姓名

public String searchName(Long id) {
    Student student = search(id);
    return student.getName();
}

注意,search函数是可能返回null的,在这种情况下searchName很有可能会抛出NPE异常

public String searchName(Long id) {
    Student student = search(id);
    return Objects.nonNull(student) ? student.getName() : "UNKNOWN";
}

除非特别明确函数的返回值不可能为null,否则一定要做null值检查,虽然这样写并没有增加太大的编码负担,但人总归是懒惰的,忽略检查的情况也总是会出现

如果我们改造search函数返回Optional,public Optional search(Long id),再来重写searchName函数

public String searchName(Long id) {
    Optional student = search(id);
    return student.getName();
}

这样的代码是编译不过的,它会强制让你去检查search返回的值是否有内容

public String searchName(Long id) {
    Optional student = search(id);
    return student.map(Student::getName).orElse("UNKNOWN");
}
Optional的使用可以参考其API文档,以下内容假设您已了解如何使用Optional

但是否就应该消灭null,全部使用Optional来替代,回答当然是NO,null自有它的用武之地,Optional也并不是全能的

kotlin等语言,使用?.符号来解决java中if...else…... ? ... : ...的啰嗦写法,如上问题可以使用student?.name : null,其语义为"当studen不为null时取其name属性值,否则取null值",kotlin的语法只是简化了编程方式,让编程变得更"爽",但并没有解决"人们容易忽略null值检查"的情况

Stuart Marks从5个不同的角度详细讲述了如何使用Optional,这里不一一叙述,有兴趣的可以直接跳到视频去看,下面将从Stuart Marks提到的7个Optional使用规范,来讲述如何正确使用/不要滥用Optional,最后重点解释一下【为什么Optional不能序列化

0x00 使用规约 Rule 1: Never, ever, user null for an Optional variable or return value.

Optional也是一个引用类型(reference type),其本身也可以赋值为null,如果我们在使用Optional的时候还要多做一层null检查,就违背了Optional的设计初衷,所以在任何时候都不要将Optional类型的变量或返回值赋值为null,我们希望的是在遇到Optional的时候不需要关心其是否为null,只需要判断其是否有值即可

public String searchName(Long id) {
    Optional student = search(id);
    if (Objects.isNull(student)) {
      // Optional可能为null,这严重违背了Optional的设计初衷
      return null;
    }
    return student.map(Student::getName).orElse("UNKNOWN");
}
Rule 2: Never user Optional.get() unless you can prove that the Optional is present.

如果Optional是absent(不包含任何值)的时候使用Optional.get(),会抛出NoSuchElementException异常,该接口的设计者也承认其设计的不合理,之后的某个jdk版本中可能会将其标记为@Deprecated,但还没有计划将其移除

public String searchName(Long id) {
    Optional student = search(id);
    // 直接使用get(),可能会抛NoSuchElementException异常
    return student.get().getName();
}

如果确实需要在Optional无内容的时候抛出异常,也请不要使用Optional.get()方法,而要使用Optional.getOrThrow()方法,主动指定需要抛出的异常,虽然该方法并未在jdk8中设计,但已经有计划在接下来的jdk版本中加入

Rule 3: Prefer alternatives to Optional.isPresent() and Optional.get().

如果一定要使用Optional.get(),请一定要配合isPresent(),先判断Optional中是否有值

public String searchName(Long id) {
    Optional student = search(id);
    // 如果一定要使用Optional.get(),请一定要配合isPresent()
    return student.isPresent() ? student.get().getName() : "UNKNOWN";
}
Rule 4: It"s generally a bad idea to create an Optional for the specific purpose of chaining methods from it to get a value.

链式语法可以让代码的处理流程看起来更加清晰,但是为了链式而去使用Optional的话,在某些情况下并不会显得有多优雅

比如,本来可以使用三目运算

String process(String s) {
  return Objects.nonNull(s) ? s : "DEFAULT";
}

如果非要硬生生地使用链式的话

String process(String s) {
  return Optional.ofNullable(s).orElse("DEFAULT");
}

比如,本来可以使用if判断值的有效性

BigDecimal first = getFirstValue();
BigDecimal second = getSecondeValue();

if (Objects.nonNull(first) && Objects.nonNull(second)) {
  return first.add(second.get());
} else {
    return Objects.isNull(first) ? second : first;
}

如果非要使用链式

Optional first = getFirstValue();
Optional second = getSecondeValue();
return Stream.of(first, second)
            .filter(Optional::isPresent)
            .map(Optional::get)
            .reduce(BigDecimal::add);

或者

Optional first = getFirstValue();
Optional second = getSecondeValue();
return first.map(b -> second.map(b::add).orElse(b))
            .map(Optional::of)
            .orElse(second);

从可读性及可维护性上来讲并没有提升,反而会带来一丝阅读困难,并且上文说过,Optional本身为引用类型,创建的Optional会进入堆内存,如果大量的不合理的使用Optional,也会在一定程度上影响JVM的堆内存及内存回收

Rule 5: If an Optional chain has a nested Optional chain, or has an intermediate result of Optional>, it"s probably too complex.

在使用Optional的时候,一定要保证Optional的简洁性,即Optional运算过程中所包含的类型既是最终需要的类型值,不要出现Optional嵌套的情况

Optional first = getFirstValue();
Optional second = getSecondeValue();

if (!first.isPresent && ! sencond.isPresent()) {
  return Optional.empty();
} else {
  return Optional.of(first.orElse(ZERO).add(second.orElse(ZERO)));
}

这样的写法,会对代码的阅读带来很大的困扰

Rule 6: Avoid using Optional in fields, method parameters, and collections.

尽量避免将Optional用于类属性、方法参数及集合元素中,因为以上三种情况,完全可以使用null值来代替Optional,没有必要必须使用Optional,另外Optional本身为引用类型,大量使用Optional会出现类似(这样描述不完全准确)封箱、拆箱的操作,在一定程度上会影响JVM的堆内存及内存回收

Rule 7: Avoid using identity-sensitive operations on Optionals.

首先需要解释,什么是identity-sensitive,可以参考 object identity and equality in java

identity-sensitive operations包含以下三种操作

reference equality,也就是 ==

identity hash code

synchronization

Optional的JavaDoc中有这样一段描述

This is a value-based class; use of identity-sensitive operations (including reference equality(==), identity hash code, or synchronization) on instances of Optional may hava unpredictable results and should be avoided.

总结下来,就是要避免使用Optional的 == equals hashCode 方法

在继续之前,需要再解释一下什么是 value type

value type - Project Valhalla

an "object" that has no notion of identity

"code like a class, works like an int"

we eventually want to convert Optional into a value type

vale type,首先像一个类(至少从编码角度来讲),但是却没有类中identity的概念,运行的时候却又和基本类型很相似

简单来说就是编码的时候像类,运行的时候像基本类型

显然,Optional目前还不是value type,而是reference type,我们查看Optional类的equalshashCode方法,并没有发现有什么特别之处,但是有计划在接下来的某个jdk版本中将Optional定义为value type

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }

    if (!(obj instanceof Optional)) {
        return false;
    }

    Optional other = (Optional) obj;
    return Objects.equals(value, other.value);
}

@Override
public int hashCode() {
    return Objects.hashCode(value);
}

在jdk8中使用Optional的identity-sensitive operations其实并没有太大问题,但很难保证,在今后的后一个jdk版本中将Optional定义为value type时不会出问题,所以为了兼容jdk升级程序逻辑的正确性,请避免使用Optional的identity-sensitive operations

这也引出了Optional为什么不能序列化

0x01 序列化问题

首先,需要了解jdk中序列化的一些背景

JDK rule: forward and backword serialization compatibility across releases

If Optional were serializable today, it would be serialized as an Object

it"all always be serialized as an Object, even if eventually becomes a value type

Serialization inherently depends on object identity

Consequencds of Optional being serializable

it might prevent it from being converted into a value type in the future

deserializing and Optional might result in a "boxed" value type

首先,JDK的序列化比较特殊,需要同时向前向后兼容,如在JDK7中序列化的对象需要能够在JDK8中反序列化,同样在JDK8中序列化的对象需要能够在JDK7中能够反序列化

其次,序列化需要依赖于对象的identity

有了以上两个序列化的前提条件,我们再来看Optional,上面已将说过,虽然目前Optional是reference type的,但其被标记为value based class,有计划在今后的某一个JDK版本中将其实现为value type

如果Optional可以序列化,那现在就有两个矛盾点

如果Optional可以序列化,那接下来的计划中,就没办法将Optional实现为value type,而必须是reference type

或者将value type加入identity-sensitive operations,这对于目前所有已发行的JDK版本都是相冲突的

所以,虽然现在Optional是reference type,但有计划将其实现为value type,考虑到JDK序列化的向前向后兼容性,从一开始就将Optional定为不可序列化,应该是最合适的方案了

如果真的有在类属性上使用Optional的需求怎么办?这里有两个替代方案/讨论可以参考

Optional Pragmatic Approach

Nothing is better than the Optional type

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74204.html

相关文章

  • 修炼内功】[Java8] Lambda究竟是不是匿名类语法糖

    摘要:本文已收录修炼内功跃迁之路初次接触的时候感觉表达式很神奇表达式带来的编程新思路,但又总感觉它就是匿名类或者内部类的语法糖而已,只是语法上更为简洁罢了,如同以下的代码匿名类内部类编译后会产生三个文件虽然从使用效果来看,与匿名类或者内部类有相 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbui4o?w=800&h=600)...

    ?xiaoxiao, 评论0 收藏0
  • 修炼内功】[JVM] 虚拟机视角方法调用

    摘要:本文已收录修炼内功跃迁之路我们写的方法在被编译为文件后是如何被虚拟机执行的对于重写或者重载的方法,是在编译阶段就确定具体方法的么如果不是,虚拟机在运行时又是如何确定具体方法的方法调用不等于方法执行,一切方法调用在文件中都只是常量池中的符号引 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbuesq?w=2114&h=12...

    shevy 评论0 收藏0
  • 修炼内功】[JVM] 浅谈虚拟机内存模型

    摘要:也正是因此,一旦出现内存泄漏或溢出问题,如果不了解的内存管理原理,那么将会对问题的排查带来极大的困难。 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbsP9I?w=1024&h=580); 不论做技术还是做业务,对于Java开发人员来讲,理解JVM各种原理的重要性不必再多言 对于C/C++而言,可以轻易地操作任意地址的...

    sanyang 评论0 收藏0
  • 修炼内功】[JVM] 类文件结构

    摘要:本文已收录修炼内功跃迁之路学习语言的时候,需要在不同的目标操作系统上或者使用交叉编译环境,使用正确的指令集编译成对应操作系统可运行的执行文件,才可以在相应的系统上运行,如果使用操作系统差异性的库或者接口,还需要针对不同的系统做不同的处理宏的 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbtpPd?w=2065&h=11...

    Eminjannn 评论0 收藏0
  • Java8实用技能

    大概一年多之前,我对java8的理解还仅限一些只言片语的文章之上,后来出于对函数式编程的兴趣,买了本参考书看了一遍,然后放在了书架上,后来,当我接手大客户应用的开发工作之后,java8的一些工具,对我的效率有了不小的提升,因此想记录一下java8的一些常用场景,我希望这会成为一个小字典,能让我免于频繁翻书,但是总能找到自己想找的知识。 用于举例的model: @Data public class ...

    microcosm1994 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<