了解、接受和利用Java中的Optional (类)

本文获得Stackify授权翻译发表,转载需要注明来自公众号EAWorld。


作者:EUGEN PARASCHIV

译者:海松

原题: Understanding, Accepting and Leveraging Optional in Java


编者按:Java 9终于在9月21号发布,于是知乎上关于“现在Java初学用等Java9出来再学吗”之类的问题可能有更新。在 Java 8 引入Optional特性的基础上,Java 9 又为 Optional 类增加了三种方法:or()、ifPresentOrElse() 和 stream(),本文的最后,也针对这些新特性做了一些说明和实例,希望有助于大家理解。


1.概述


Java 8 最有趣的特性之一,就是引入了全新的 Optional 类。该类主要用来处理几乎每位程序员都碰到过的麻烦问题—— 空指针异常(NullPointerException)。


从本质上来说,该类属于包含可选值的封装类(wrapper class),因此它既可以包含对象也可以仅仅为空。


伴随着 Java函数式编程方式的异军突起,Optional 应运而生,除了可助该编程方式一臂之力外,Optional 的作用显然还远不止于此。


我们先来看一个简单的案例。在 Java 8 之前,凡涉及到访问对象方法或者对象属性的操作,无论数量多寡,都可能导致 空指针异常:


String isocode = user.getAddress().getCountry().getIsocode().toUpperCase();


假如我们想保证上面的小示例不出现异常,我们可能需要在访问它之前对每一个值进行显式检查:

if (user != null) {
    Address address = user.getAddress();
    if (address != null) {
        Country country = address.getCountry();
        if (country != null) {
            String isocode = country.getIsocode();
            if (isocode != null) {
                isocode = isocode.toUpperCase();
            }
        }
    }
}


这么一来,会让代码显得累赘而难以维护。


为简化这一过程,我们将使用 Optional 类取代上述代码,从创建和验证一个实例开始,再到使用其提供的不同方法,最后将其和返回相同类型的其他方法进行组合,而最后这项组合功能正是 Optional 的真正强大之处。


2.创建 Optional  实例


为了实现重复迭代(reiterate),该类型对象既可以包含一个值,也可以为空。我们先用具有相同名称的方法来创建一个空 Optional:

@Test(expected = NoSuchElementException.class)
public void whenCreateEmptyOptional_thenNull() {    Optional<User> emptyOpt = Optional.empty();    emptyOpt.get();
}


毫无疑问,如果您要访问 emptyOpt  变量的值,会导致 NoSuchElementException异常。


您可以用 of() 和 ofNullable(),来创建包含一个值的Optional 对象。两种方法的区别在于:如果你将 null 值作为参数传入 of() 方法,那么该方法会抛出一个 空指针异常。

@Test(expected = NullPointerException.class)public void whenCreateOfEmptyOptional_thenNullPointerException() {
    Optional<User> opt = Optional.of(user);
}

如你所见,空指针 异常的问题并没有得到彻底解决。因此,只有当对象不为 null 时, of()的方法才可行。


如果对象既可能为 null ,也可能为非 null ,就必须选择 ofNullable()。

Optional<User> opt = Optional.ofNullable(user);


访问 Optional 对象的值


想要获取Optional实例内部的对象,方法之一是使用get()方法

@Test
public void whenCreateOfNullableOptional_thenOk() {    String name = "John";    Optional<String> opt = Optional.ofNullable(name);        assertEquals("John", opt.get());
}


但和之前类似,这种方法在值为 null 时也会抛出异常。为避免出现异常,您可选择首先检验其中是否存在值。

@Test
public void whenCheckIfPresent_thenOk() {    User user = new User("john@gmail.com", "1234");    Optional<User> opt = Optional.ofNullable(user);    assertTrue(opt.isPresent());    assertEquals(user.getEmail(), opt.get().getEmail());
}


利用 ifPresent()也可以用来检查是否存在值。而且该方法还带有一个 Consumer 参数,在对象不为空时执行 λ 表达式:

opt.ifPresent( u -> assertEquals(user.getEmail(), u.getEmail()));


在此示例中,只有在用户对象非空时,才会执行assertion。


接下来,我们看看能够替换空值的各种方法。


返回默认值


Optional 类提供了一些 API,用于返回对象值或在对象为空时返回默认值。

其中的第一种方法是 orElse(),它的工作方式相当直接:如果存在值,则返回该值,如果不存在值,则返回它收到的参数:

@Test
public void whenEmptyValue_thenReturnDefault() {    User user = null;    User user2 = new User("anna@gmail.com", "1234");    User result = Optional.ofNullable(user).orElse(user2);    assertEquals(user2.getEmail(), result.getEmail());
}


此处,user 对象为空,所以 user2 作为默认替代值返回。


如果对象的初始值不为空,则默认值会被忽略:

@Test
public void whenValueNotNull_thenIgnoreDefault() {    User user = new User("john@gmail.com","1234");    User user2 = new User("anna@gmail.com", "1234");    User result = Optional.ofNullable(user).orElse(user2);    assertEquals("john@gmail.com", result.getEmail());
}


第二种同类 API 是 orElseGet() ——其工作方式略有不同。在本例中,如果存在值,则方法回返该值,如果不存在,则其执行 Supplier 函数接口(作为其收到的一个参数),并返回执行结果:

User result = Optional.ofNullable(user).orElseGet( () -> user2);


orElse() 和 orElseGet() 之间的区别


乍一看,两种方法似乎效果相同。但实际还是有差别。我们可以通过创建几个例子,来看看二者在功能表现上的相似处和不同点。


首先,我们来看对象为空时,二者的表现:

@Test
public void givenEmptyValue_whenCompare_thenOk() {    User user = null    logger.debug("Using orElse");    User result = Optional.ofNullable(user).orElse(createNewUser());    logger.debug("Using orElseGet");    User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
}

private User createNewUser() {    logger.debug("Creating New User");    return new User("extra@gmail.com", "1234");
}


在上面的代码中,两种方法都调用了createNewUser()  方法,后者会记录消息日志并返回 User 对象。


代码输出如下:

Using orElse
Creating New User
Using orElseGet
Creating New User


可见,当对象为空时,二者在表现上并无差别,都是代之以返回默认值。


接下来,我们举一个 Optional 不为空时的相似例子:

@Test
public void givenPresentValue_whenCompare_thenOk() {    User user = new User("john@gmail.com", "1234");    logger.info("Using orElse");    User result = Optional.ofNullable(user).orElse(createNewUser());    logger.info("Using orElseGet");    User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
}


这次的输出如下:

Using orElse
Creating New User
Using orElseGet

此处,两个 Optional 对象都包含有一个非空值,而两种方法都会将其作为返回值。但是,orElse() 方法仍然会创建默认的 User 对象。相反,orElseGet()  方法将不再创建 User 对象。


当操作中包含大量密集调用时,比如 web 服务调用或者数据库查询,这种差别就会对代码执行产生重大影响。


返回异常


除了 orElse() 和 orElseGet() 方法,Optional还定义了 ElseThrow() API,其作用是在对象为空时,直接抛出一个异常,而不是返回一个替代值。

@Test(expected = IllegalArgumentException.class)
public void whenThrowException_thenOk() {    User result = Optional.ofNullable(user)      .orElseThrow( () -> new IllegalArgumentException());
}


此处,如果 user 值为空,则会抛出 非法参数异常。


这让我们可以从更多灵活的语义中挑选所要抛出的异常,而不是千篇一律的 空指针异常。


既然我们已对 Optional 本身的使用有了一定了解,那就让我们再来看看用于转换和过滤 Optional 值的其他方法。


3.对值进行转换


Optional 值可通过多种方法进行转换;我们就从 map() 和 flatMap() 说起。


首先,让我们看个使用 map() API 的例子:

@Test
public void whenMap_thenOk() {    User user = new User("anna@gmail.com", "1234");    String email = Optional.ofNullable(user)      .map(u -> u.getEmail()).orElse("default@gmail.com");        assertEquals(email, user.getEmail());
}


Map() 将 Function 参数作为值,然后返回 Optional 中经过封装的结果。这将使我们可以在后续附加一些操作,比如此处的 orElse() 。


相比之下,flatMap() 也是将 Function 参数作为 Optional 值,但它后面是直接返回结果。


为了查看实际效果,我们添加一个方法,可向 User  类返回 Optional:

public class User {    
    private String position;

    public Optional<String> getPosition() {
        return Optional.ofNullable(position);
    }
    
    //...
}


因为 getter 方法返回一个 Optional 字符串值,在请求Optional User 对象时,您可将其作为 flatMap() 的参数。返回值为非封装字符串值:

@Test
public void whenFlatMap_thenOk() {    User user = new User("anna@gmail.com", "1234");    user.setPosition("Developer");    String position = Optional.ofNullable(user)      .flatMap(u -> u.getPosition()).orElse("default");        assertEquals(position, user.getPosition().get());
}


4.对值进行过滤


除了对值进行转换的功能,Optional 类还提供了根据条件对值进行“过滤”的功能。


filter() 方法将 predicate 作为参数,当测试评估为真时,返回实际值。否则,当测试为假时,返回值则为空 Optional。


我们来看一个例子——基于非常基本的电子邮件验证,接受或者拒绝 User:

@Test
public void whenFilter_thenOk() {    User user = new User("anna@gmail.com", "1234");    Optional<User> result = Optional.ofNullable(user)      .filter(u -> u.getEmail() != null && u.getEmail().contains("@"));        assertTrue(result.isPresent());
}


作为通过过滤测试的结果,Result 对象将包含一个非 null 值。


5.对 Optional 类的方法进行链接


Optional 还具有更多强大的应用,鉴于绝大多数 Optional 方法会返回相同类型的对象,您可以将它们的不同组合链接起来。


我们把示例代码重新写一下。


首先,我们重构这些类,这样 getter 方法将返回 Optional 引用(references):

public class User {
    private Address address;

    public Optional<Address> getAddress() {
        return Optional.ofNullable(address);
    }

    // ...
}
public class Address {
    private Country country;
    
    public Optional<Country> getCountry() {
        return Optional.ofNullable(country);
    }

    // ...
}


上述结构可用嵌套集合来直观地表示:



现在删除对 null 进行检查的代码,并以 Optional 方法来取代:

@Test
public void whenChaining_thenOk() {    User user = new User("anna@gmail.com", "1234");    String result = Optional.ofNullable(user)      .flatMap(u -> u.getAddress())      .flatMap(a -> a.getCountry())      .map(c -> c.getIsocode())      .orElse("default");    assertEquals(result, "default");
}


上面的代码可通过方法引用(method references)做进一步精简:

String result = Optional.ofNullable(user)
  .flatMap(User::getAddress)
  .flatMap(Address::getCountry)
  .map(Country::getIsocode)
  .orElse("default");


从现在的结果看,代码比先前冗长的条件驱动(conditional-driven)版本要简洁许多。


6.Java 9 新增特性


在 Java 8 引入Optional特性的基础上,Java 9 又为 Optional 类增加了三种方法:or()、ifPresentOrElse() 和 stream()。


在某种意义上,or() 方法同 orElse() 和 orElseGet() 类似,都是在对象为空时提供替换功能。在本例中,返回值为另一个由 Supplier 参数生成的 Optional 对象。


如果对象包含一个值,则λ表达式不会执行:

@Test
public void whenEmptyOptional_thenGetValueFromOr() {    User result = Optional.ofNullable(user)      .or( () -> Optional.of(new User("default","1234"))).get();                    assertEquals(result.getEmail(), "default");
}


在上述示例中,如果  user  变量为空,则将返回包含一个带有电子邮件“default”的 User  对象的 Optional 。


 ifPresentOrElse() 方法带有两个参数:Consumer  和 Runnable。如果对象包含一个值,则会执行 Consumer  动作;否则,会执行 Runnable  动作。


如果您希望使用某个现有值执行一个动作,或者仅仅想跟踪某个值是否已作定义,则该方法非常有用:

Optional.ofNullable(user).ifPresentOrElse( u -> logger.info("User is:" + u.getEmail()),
  () -> logger.info("User not found"));


最后,您可从新 stream() 方法的扩展 Stream API 得到益处,具体做法是将实例转换为一个 Stream 对象。如果 Optional 不存在值,则 Stream 为空,如果 Optional 包含一个非 null 值,则 Stream 会包含单个值。


我们举个将 Optional 作为 Stream 处理的例子:

@Test
public void whenGetStream_thenOk() {    User user = new User("john@gmail.com", "1234");    List<String> emails = Optional.ofNullable(user)      .stream()      .filter(u -> u.getEmail() != null && u.getEmail().contains("@"))      .map( u -> u.getEmail())      .collect(Collectors.toList());      assertTrue(emails.size() == 1);    assertEquals(emails.get(0), user.getEmail());
}


在此处使用 Stream ,使得应用 filter()、map() 和 collect() 等 Stream 接口方法 来获取 List 成为可能。


7.应该如何使用 Optional


在使用 Optional 时,我们需要考虑几个问题,来决定什么时候用以及如何用。


第一个要点,Optional 并不能序列化(Serializable )。因此,它不可以在类中当作一个字段(field)来使用。


如果您需要序列化一个包含 Optional 值的对象,Jackson library(https://stackify.com/java-xml-jackson/)可支持将 Optionals当作普通对象来对待。这意味着,Jackson 会将空对象作为 null,它还会将有值对象当作一个包含该值的字段。这个功能可在 jackson-modules-java8 (https://github.com/FasterXML/jackson-modules-java8) 项目中找到。


另一种不太适合使用该类型的情况,是将该类型作为方法或者构造函数的参数。这将导致不必要的代码复杂化。

User user = new User("john@gmail.com", "1234", Optional.empty());


相反,使用方法重载(method overloading)来处理非强制性参数要方便得多。


Optional的主要用途是作为一种返回类型。在获得该类型的一个实例后,如果存在值,您可以提取该值,如果不存在值,则您可以获得一个替换值。


Optional类对我们最有帮助的一个用例,是其同 stream 或者其他方法的组合使用,这些方法会返回一个可构建流畅 API 的Optional 值。


我们举个使用 Stream findFirst() 方法并返回 Optional 对象的例子:

@Test
public void whenEmptyStream_thenReturnDefaultOptional() {    List<User> users = new ArrayList<>();    User user = users.stream().findFirst().orElse(new User("default", "1234"));        assertEquals(user.getEmail(), "default");
}


8.总结


对于 Java 语言来说,Optional 是一项非常有用的新增特性。尽管无法彻底消除 空指针异常,但 Optional 可以最大限度减少代码执行过程中出现的此类异常。


同时,该类经过精心设计,对于 Java 8 加入的新函数式支持(functional support)而言,它自然而然地成为非同一般的新增特性。


总之,该类简单而不失强大,相比之前的同类功能,用其编写代码既简单易读又不易出错。


原文链接https://stackify.com/optional-java/


关于作者

Eugen是一名软件工程师,对Spring、REST API、安全和教育拥有极大热情。同时,他还是Baeldung(推特账号@baeldung)的创始人。


关于EAWorld

微服务,DevOps,元数据,企业架构原创技术分享EAii(Enterprise Architecture Innovation Institute)企业架构创新研究院旗下官方微信公众号。


微信号:eaworld,长按二维码关注


8月-9月,PWorld系列技术趴还将继续上演。目前,10月28日将在北京举行PWorld MeetUP“移动平台新技术发展新趋势及企业实践”已启动报名,戳“阅读原文”可直达报名页面,并了解更多详情~


阅读原文:http://mp.weixin.qq.com/s?timestamp=1507543888&src=3&ver=1&signature=UZ8tr00*SsuDn00EmfHtU1-xiFhuukYS4p5y6T-Are8St7NyWyF*9tS*xa9ANqKHKVPDlsCZ534ofloKyemZBJbb7W1nG9csgIh31SPMAOKSeAZ*ZMQFblNgL6A9cXFtMHwhPAZRopoP2-Ekysi2JtNfqchrWo1Z*cwi04eg2PM=&devicetype=Windows-QQBrowser&version=61030004&pass_ticket=qMx7ntinAtmqhVn+C23mCuwc9ZRyUp20kIusGgbFLi0=&uin=MTc1MDA1NjU1&ascene=1
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Optional 本章内容 如何为缺失的值建模 Optional 类 应用Optional的几种模式 使用Opti...
    追憶逝水年華阅读 1,735评论 0 0
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • 太累 累 没有任何休息 还有有一个人陪伴我 感激她
    凯燊阅读 181评论 0 2
  • 最近,一首歌“成都”,让成都享誉大江南北,没错,我就是来自成都的一只猴。 2年前,我在川师毕业了,由于研究生没考上...
    二西子阅读 3,761评论 3 2