Java 8 Optional入门实战

1. 简介

本文简要介绍一下Java 8 引入的 Optional 类。引入Optional 类的主要目的是为使用可选值代替 null 提供类型级解决方案。如果,你想知道为什么需要更深入的了解和使用 Optional 类,可以参考甲骨文官方文章

Optionaljava.util.package 的一部分,为了能够使用,需要导入Optional

    import java.util.Optional;

2. 创建 Optional 对象

有多种方式可以创建 Optional 对象,可以使用下面的方法创建一个空的 Optianal对象:

    @Test
    public void test_createsEmptyOptionalObject() throws Exception {
        Optional<String> empty = Optional.empty();
        assertFalse(empty.isPresent());
    }

可以使用 isPresent API 来检查 Optional 对象是否有封装的值,当且仅当 * Optional* 封装了非 null 值时,API才返回 true

还可以使用 Optional 提供了静态方法创建 Optional 对象:

    @Test
    public void test_createOptionalObjectWithStaticMethod() throws Exception {
        String val = "not null";
        Optional<String> hasVal = Optional.of(val);
        assertTrue(hasVal.isPresent());
    }

如果 Optional 对象有封装的值(非 null ),可以对封装的值进行处理:

    @Test
    public void test_processOptionalValue() throws Exception {
        String val = "not null";
        Optional<String> hasVal = Optional.of(val);
        System.out.println(hasVal.toString());
        assertEquals("Optional[not null]", hasVal.toString());
    }

当使用 Optional 提供的静态方法 of 创建 Optional 对象时,方法的参数不能null,否则,方法会抛出 NullPointerException

    @Test(expected = NullPointerException.class)
    public void test_throwNullPointerException() throws Exception {
        String val = null;
        Optional<String> hasVal = Optional.of(val);
    }

如果构建 Optional 对象时可以传入 null 参数,可以使用 ofNullable 方法代替of

    @Test
    public void test_passNullParamNoException() throws Exception {
        String val = null;
        Optional<String> hasVal = Optional.ofNullable(val);
        assertFalse(hasVal.isPresent());
    }

使用 ofNullable 方法创建 Optional 对象时,如果传入一个 null 参数,方法不会抛出异常,而是返回一个空的 Optional 对象,和使用 Optional.empty API 创建的一样。

3. 检查值是否存在

当得到一个从其他方法返回或自己创建的 Optional 对象后,可以使用isPresent API 检查 Optional 对象是否有封装值:

    @Test
    public void test_checkValuePresentOrNot() throws Exception {
        Optional<String> opt = Optional.of("has value");
        assertTrue(opt.isPresent());

        opt = Optional.ofNullable(null);
        assertFalse(opt.isPresent());
    }

当且仅当Optional 对象封装一个非空值时,isPresent API才返回true

在Java 11 中可以使用 isEmpty API 完成相反的工作:

    @Test
    public void test_checkValuePresentOrNotJava11() throws Exception {
        Optional<String> opt = Optional.of("has value");
        assertFalse(opt.isEmpty());

        opt = Optional.ofNullable(null);
        assertTrue(opt.isEmpty());
    }

当且仅当 Optional 对象封装的值为 null 时,isEmpty 返回true,其他情况返回false

4. 使用 ifPresent() 进行条件处理

ifPresent API 允许我们在 Optional 对象封装的值非空时执行一些代码,在没有Optional 之前,最常用的方法是使用 if 语句进行判断,结果为真时执行代码逻辑:

    if(name != null){
        System.out.println(name.length);
    }

这段代码在执行其他代码之前先检查 name 变量是否为 null。冗长并不是这种方法的唯一问题一,这种方法固有很多潜在的bug。

在习惯了这种方法之后,很容易忘记在代码的某些部分执行空检查。如果 null 值进入该代码,可能会在运行时导致 NullPointerException 异常。 当程序因输入问题而失败时,通常是编码不够健壮导致,也是代码实践不好的结果。

作为强制执行良好编程实践的一种方式,Optional 可以明确地处理 null。 在典型的函数式编程风格中,我们可以对实际存在的对象执行操作,使用Java 8重构上面的代码如下:

    @Test
    public void doSomeThingWhenExist()  throws Exception {
        Optional<String> opt = Optional.of("baeldung");
        opt.ifPresent(name -> System.out.println(name.length()));
    }

5. 使用 orElse 获取封装的值

orElse API 用于从 Optional 实例中获取封装的值,orElse 的唯一参数作为Optional 无封装值时的默认值,这点类似 System.getProperty API。如果,Optional 有封装值 orElse API返回 Optional 封装的值,否则返回参数的值。

    @Test
    public void test_getValueUseorElse() throws Exception {
        Optional<String> hasVal = Optional.of("Hello");
        String val = hasVal.orElse("no value");
        assertEquals("Hello", val);

        Optional<String> noVal = Optional.empty();
        String defaultVal = noVal.orElse("default");
        assertEquals("default", defaultVal);
    }

6. 使用 orElseGet 获封装的值

orElseGet API 功能和 orElse 类似,两者的不同之处在于 orElseGet 的参数为一个 Supplier 实例,当 Optional 对象无封装值时,orElseGet 调用 Supplier 实例的 get 方法,并将返回值作为 orElseGet 的返回值。

    @Test
    public void test_getValueUseorElseget() throws Exception {
        Optional<String> hasVal = Optional.of("Hello");
        String val = hasVal.orElseGet(() -> "no value");
        assertEquals("Hello", val);

        Optional<String> noVal = Optional.empty();
        String defaultVal = noVal.orElseGet(() -> "default");
        assertEquals("default", defaultVal);
    }

7. orElseorElseGet 的区别

Optional 对象无封装值时,orElseorElseGet 并无本质上的区别,两个API 都返回各自的默认值。但是,当 Optional 对象有封装值时两者有很大的区别,而且两者在性能上的差异也十分明显。一句话总结两者的差异就是:orElse 会触发获取默认值的动作,尽管并不需要。为了更加形象的说明,这里提供一个方法用于获取默认值,方法中使用 sleep 模拟这是一个耗时的操作:

    private String getDefaultValue() {
        System.out.println("enter method get default value");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "default value";
    }

创建一个非空的 Optional 对象,分别调用 orElseorElseGet 方法,观察两者行为上的差异:

    @Test
    public void test_differenceorElseAndorElseGet() throws Exception {
        Optional<String> hasVal = Optional.of("value");
        System.out.println("enter orElse method");
        String var0 = hasVal.orElse(getDefaultValue());

        System.out.println("enter orElseGet method");
        String var1 = hasVal.orElseGet(this::getDefaultValue);
    }

上面代码的输出结果如下:

enter orElse method
enter method get default value
enter orElseGet method

从输出结果可以非常清晰的看出两个API之间的差异,为了更好的性能,在编码中优先使用 orElseGet API 获取 Optional 的值。。

8. 使用 orElseThrow 抛出异常

orElseThroworElseorElseGet API类似,orElseThrow 提供了一种在Optional 为空时的处理方法-抛异常而不是返回默认值。

    @Test(expected = IllegalArgumentException.class)
    public void test_throwsExecption() {
        String nullName = null;
        String name = Optional.ofNullable(nullName).orElseThrow(
                IllegalArgumentException::new);
    }

9. 使用 get() 获取值

get 是获取 * Optional* 值的最后方法(不是一个好方法):

    @Test
    public void test_getValueUseGet() {
        Optional<String> opt = Optional.of("value");
        String name = opt.get();
        assertEquals("value", name);
    }

和上面三种获取值的方法不同,* get * 方法只能返回 Optional 封装的值,如果Optional 为空,方法会抛出 NoSuchElementException 异常。

    @Test(expected = NoSuchElementException.class)
    public void test_throwsNoSuchElementException() {
        String nullName = null;
        String name = Optional.ofNullable(nullName).get();
    }

抛出异常是 get API 的最大缺陷,Optional 应该帮助我们尽可能屏蔽这些不可见异常,因此 get API 和 * Optional* 目标相背而驰,该方法将来可能被废弃。应该尽可能的使用其他方法获取值。

10. 使用 filter() 进行过滤

filter API 被用于对 Optional 封装的值进行一个内联测试,filter API 使用一个谓词作为参数并返回一个Optional 对象。如果,被封装的值通过测试则返回Optional 本身,否则返回一个空的 Optional 对象。

    @Test
    public void test_filter() throws Exception {
        Optional<Integer> passTest = Optional.of(101);
        assertTrue(passTest.filter(integer -> integer.intValue() > 100).isPresent());
        Optional<Integer> notPassTest = Optional.of(99);
        assertFalse(notPassTest.filter(integer -> integer.intValue() > 100).isPresent());
    }

filter API 的工作套路:根据某个预定义的规则拒绝 Optional 对象封装的值,可以用于拒绝格式错误的邮箱地址或强度不够的密码。

接下来看一个更有趣的例子(有些场景下不使用 Optional 为了安全的操作,我们通常需要进行多次 null 检查)。假设,我们打算购买一部手机并且只关心手机的价格。我们从手机购买网站得到手机价格的推送消息,手机价格被封装在一个对象中,数据结构定义如下:

public class Phone {
    private Double price;

    public Phone(Double price) {
        this.price = price;
    }

    //standard getters and setters
}

当把网址的推送数据传递给检查手机价格是否满足我们的预算要求的函数时(假设能接受的手机价格为3000-5000),如果不使用 * Optional* 一种可能的代码实现如下:

    public boolean checkPriceWithoutOptional(Phone phone) {
        boolean isInRange = false;

        if (phone != null && phone.getPrice() != null
                && (phone.getPrice() >= 3000
                && phone.getPrice() <= 5000)) {

            isInRange = true;
        }
        return isInRange;
    }

为了实现上面的功能我们写了很多代码,尤其在 if 的条件表达式中,函数真正的核心代码仅仅是检查价格范围,其他多余的检查对于实现功能来说都是不必要的。代码冗余可能并不是最严重的问题,忘记 null 检查可能更加糟糕,而这不会引发任何编译错误(代码静态检查工具可以发现并上报告警)。

使用 Optionalfilter API 可以以一种优雅的方式实现同样的功能:

    public boolean checkPriceWithOptional(Phone phone) {
        return Optional.ofNullable(phone)
                .map(Phone::getPrice)
                .filter(p -> p >= 3000)
                .filter(p -> p <= 5000)
                .isPresent();
    }

使用 Optional 让代码在以下两点优于使用 if 语句检查:

  • 给函数出入一个 null 对象,不会触发任何错误。
  • 代码更加聚焦业务实现(价格检查),其他的事情由 Optional 负责。

11. 使用 map() 进行值变换

在之前的章节,我们已经看到如何使用过滤器接受或拒绝 Optional 封装的值。相同的语法可以用于 map API 对 Optional 封装的值进行变换。

    @Test
    public void test_mapList2ListSize() {
        List<String> companyNames = Arrays.asList(
                "Java", "C++", "", "C", "", "Python");
        Optional<List<String>> listOptional = Optional.of(companyNames);

        int size = listOptional
                .map(List::size)
                .orElse(0);
        assertEquals(6, size);
    }

在上面的例子中,我们使用 Optional 封装了一个字符串列表,并使用 map API 对 字符串列表进行变换,上面例子中执行的变化是获取字符串列表的长度。

map API 返回对 Optional 封装对象的计算结果,最后需要调用合适的API来获取Optional 对象的值(变换后的值)。

注意:filter API 值检查 Optional 对象封装的值并返回一个boolean类型的结果,相反 map API 对 Optional 对象封装的值进行计算并返回计算结果。

    @Test
    public void test_mapString2StringSize() {
        String name = "Hello World";
        Optional<String> nameOptional = Optional.of(name);

        int len = nameOptional
                .map(String::length)
                .orElse(0);
        assertEquals(11, len);
    }

我们可以链式调用 mapfilter API 来做一些更有意义的事情。假设,我们有一段代码需要检查用户输入的密码是否正确,我们可以使用 map 对密码进行变换,使用 filter 判断密码是否正确:

    @Test
    public void test_checkPassword() {
        String password = " password ";
        Optional<String> passOpt = Optional.of(password);
        boolean correctPassword = passOpt.filter(
            pass -> pass.equals("password")).isPresent();
        assertFalse(correctPassword);

        correctPassword = passOpt
            .map(String::trim)
            .filter(pass -> pass.equals("password"))
            .isPresent();
        assertTrue(correctPassword);
    }
}

12. 使用 flatMap() 对值进行变换

map API 一样,我们也可以使用 flatMap API 作为一个替代方法对值进行变换。两者的主要区别是:map 值对未封装的值进行转换,flatMap 在处理值之前先进行“去封装”操作,然后再执行变换操作。
为了更清晰的解释两者的区别,我们假设有一个Person对象,对象有三个基本属性:名字、年龄和密码。

public class Person {
    private String name;
    private int age;
    private String password;

    public Person() {
    }

    public Person(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }

    public Optional<String> getName() {
        return Optional.ofNullable(name);
    }

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

    public Optional<String> getPassword() {
        return Optional.ofNullable(password);
    }

    // normal constructors and setters
}

我们创建一个Person对象,并使用 Optional 封装创建的Person对象:

        Person person = new Person("john", 26, "pwd");
        Optional<Person> personOptional = Optional.of(person);

分别使用 mapflatMap API 获取名字的代码如下,从中可以看到使用 flatMap API 的代码量较使用 map 更短小,也更加容易理解。

    @Test
    public void test_flatMap() {
        Person person = new Person("ct", 26,"pwd");
        Optional<Person> personOptional = Optional.of(person);

        Optional<Optional<String>> nameOptionalWrapper
            = personOptional.map(Person::getName);
        Optional<String> nameOptional
            = nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
        String name1 = nameOptional.orElse("");
        assertEquals("ct", name1);

        String name = personOptional
            .flatMap(Person::getName)
            .orElse("");
        assertEquals("ct", name);
    }

13. 总结

本文简要介绍了Java 8 Optional 类的大部分重要特性,与此同时,我们也简单阐述了为什么我们选择使用Optional 代替显示的 null 检查和参数检查。最后,讲解了 orElseorElseGet 之间微妙但重要的区别,关于该主题可以从拓展阅读获取更多内容。

文中的样例代码可以从 GitHub.获取。

参考

[1] Guide To Java 8 Optional
[2] Java 8 Optional
[3] Java 11 Optional

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

推荐阅读更多精彩内容

  • Optional 本章内容 如何为缺失的值建模 Optional 类 应用Optional的几种模式 使用Opti...
    追憶逝水年華阅读 1,783评论 0 0
  • 本文获得Stackify授权翻译发表,转载需要注明来自公众号EAWorld。 作者:EUGEN PARASCHIV...
    72a1f772fe47阅读 11,673评论 3 7
  • 这篇文章写得不错,所以转载了下,修改了小部分,原文地址见末尾 身为一名Java程序员,大家可能都有这样的经历:调用...
    疯狂的冰块阅读 274评论 0 2
  • 文/黄煊墨 我小时候睡懒觉、干活偷懒,我妈必“诅咒”我,长大娶不到媳妇。当时太小,不知道媳妇是什么东西,但老太太总...
    煊墨杂谈阅读 635评论 0 0
  • 人生的路有平坦也有坎坷 平坦坎坷都得走 人生的河有深也有浅 深浅都得趟 人生的风景有美丽也有萧条 美丽萧条都得欣赏...
    文采乐阅读 321评论 8 14