Java 8 开始新增的 Optional 类 - Optional 对象中的返回

使用 get() 来返回一个值

在对 Optional 对象完成一些检查和校验后,我们可以使用 get() 方法来返回对象中的值。

    // returning Value With get()
    @Test
    public void givenOptional_whenGetsValue_thenCorrect() {
        Optional<String> opt = Optional.of("HoneyMoose");
        String name = opt.get();
        assertEquals("HoneyMoose", name);
    }

orElse() 或者 orElseGet() 方法不一样的地方是 get() 只会在 Optional 包装的对象不为 null 的时候返回值,否则这个方法将会抛出一个没有这个元素(no such element exception)的异常 。

    @Test(expected = NoSuchElementException.class)
    public void givenOptionalWithNull_whenGetThrowsException_thenCorrect() {
        Optional<String> opt = Optional.ofNullable(null);
        String name = opt.get();
    }

上面的方法显示了如何使用 get() 方法来获得 Optional 中元素的典型操作。

我们使用 Optional 的主要原因就是为了避免在程序中出现 Null 对象异常的这种情况,但是 get() 方法的这种操作还是会给你带来空对象异常的。

因此需要注意下这种代码编写方式,也有可能在 JDK 的后续版本中,这个 get() 方法有可能被取消掉,但是目前还不会。

正是因为这种情况,get() 这个方法也有可能出现空对象异常,在编码的时候还是需要注意下的。

使用 filter() 来进行条件返回

我们可以使用 filter() 方法在输出之前进行测试,然后过滤出满足我们条件的返回对象。

这个方法将会使用 Java 提供的谓语(predicate )作为参数来返回 Optional 对象。

如果通过了 Java 提供的谓语(predicate )测试的话,Optional 对象将会被原样返回。

如果,测试的 谓语(predicate )为 False 的话,那么一个空的 Optional 对象将会被返回。

    @Test
    public void whenOptionalFilterWorks_thenCorrect() {
        Integer year = 2016;
        Optional<Integer> yearOptional = Optional.of(year);
        boolean is2016 = yearOptional.filter(y -> y == 2016).isPresent();
        assertTrue(is2016);
        boolean is2017 = yearOptional.filter(y -> y == 2017).isPresent();
        assertFalse(is2017);
    }

filter() 方法通常用来处理你不需要的返回,或者处理满足你条件的返回。

例如在对用户电子邮件进行检查,或者用户密码进行检查的时候,我们可以设置这样一个 filter() 过滤器,当不满足我们设置条件的时候,我们让程序返回一个空的对象,以此来设置条件。

让我们看另外一个使用场景,我们希望购买一个调制解调器(modem),但是我们只关注的是价格,我们对信号灯并不敏感

我们希望对调制解调器在满足价格区间的时候获得一个通知:

public class Modem {
    private Double price;

    public Modem(Double price) {
        this.price = price;
    }
    // standard getters and setters
}

让我们让这个对象通过一些代码来检查这个对象是不是满足我们设定的价格区间.

下面的代码是我们不使用 Optional 的时候的代码。

从上面的代码来看,我们需要进行 Null 检查,然后获得价格,然后判断价格,更要命的更极端的情况价格也有可能为 null。

public boolean priceIsInRange1(Modem modem) {
    boolean isInRange = false;

    if (modem != null && modem.getPrice() != null 
      && (modem.getPrice() >= 10 
        && modem.getPrice() <= 15)) {

        isInRange = true;
    }
    return isInRange;
}

为了满足 Null 检查的所有条件,我们需要不停的 if 进行判断。我们需要通过这些 if 条件检查来确定是否满足我们的条件,并且这个代码看起来有点郁闷,但是实际上也确实就是这样写的。

@Test
public void whenFiltersWithoutOptional_thenCorrect() {
    assertTrue(priceIsInRange1(new Modem(10.0)));
    assertFalse(priceIsInRange1(new Modem(9.9)));
    assertFalse(priceIsInRange1(new Modem(null)));
    assertFalse(priceIsInRange1(new Modem(15.5)));
    assertFalse(priceIsInRange1(null));
}

同时,你也有可能忘记某一个检查,比如说价格为 NULL 的时候怎么办。

这个检查在编译的时候是不会提示你的,只有程序真正上线运行了,出现了异常了,你才知道,我又忘记检查空了。

现在我们看看 Optional 中的 filter() 是怎么做的。

    public boolean priceIsInRange2(Modem modem2) {
        return Optional.ofNullable(modem2).map(Modem::getPrice).filter(p -> p >= 10).filter(p -> p <= 15).isPresent();
    }

你的程序精简到只需要一行就可以了。

map 这个方法只是简单的从对象中获得值,后面的过滤器才是对获得值进过滤的。

需要注意的是,使用 filter() 不会对输入的参数进行修改。

在我们的用例中,我们非常容易的就从我们的 Model 对象中获得了价格的属性。至于 map() 的使用我们在后面的内容中进行介绍。

针对上面的代码,首先如果对象出现 null 的时候是不会对你程序有任何影响的,还是能一直跑下去的。

其次就是逻辑非常简单,整个逻辑就是对价格进行判断,至于其他的 null 判断都是由 Optional 完成的。

@Test
public void whenFiltersWithOptional_thenCorrect() {
    assertTrue(priceIsInRange2(new Modem(10.0)));
    assertFalse(priceIsInRange2(new Modem(9.9)));
    assertFalse(priceIsInRange2(new Modem(null)));
    assertFalse(priceIsInRange2(new Modem(15.5)));
    assertFalse(priceIsInRange2(null));
}

最开始的 If 代码也是可以完成价格的判断的,但是这个方法有着自身的缺陷,因此我们使用了 Optional 提供的 filter() 来进行了改进,并且使用了 filter() 来替代了 IF 代码判断来过滤掉我们不需要的值。

使用 map() 来转换值

在上面的内容中,我们介绍了如何使用 filter() 来过滤掉我们不需要的值,换句话说就是有条件的拒绝和通过。

使用 map() 的句法和使用 filter() 的句法是差不多的。

    @Test
    public void givenOptional_whenMapWorks_thenCorrect() {
        List<String> companyNames = Arrays.asList("paypal", "oracle", "", "microsoft", "", "apple");
        Optional<List<String>> listOptional = Optional.of(companyNames);

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

在这个例子中,我们使用了一个包含有 Optional 对象的 List 来测试 map() 的使用。

这个例子中,我们使用了 map() 返回了 List 的长度。

map() 方法将会返回对 Optional 内部包装的计算,我们需要调用正确的函数才能够返回正确的值。

需要注意的是 filter() 只是检查对象中的值是不是满足给定的条件,map() 需要做的操作就更近一步了, map() 需要获得 Optional 对象中的值,然后进行计算,在完成计算后将计算的结果进行返回。

    @Test
    public void givenOptional_whenMapWorks_thenCorrect2() {
        String name = "HoneyMoose";
        Optional<String> nameOptional = Optional.of(name);

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

我们还可以将 mapfilter 结合在一起使用,这种结合能够为你的代码提供更加强劲的计算能力。

在 Java 8 介绍的 Stream 中,我们也通常是这样一起结合使用的,

考察下面的使用场景,我们需要对用户的密码进行检查是否满足条件,在这个检查之前,我们首先需要对用户输入的密码进行清理,比如说去除掉前后的空白等,我们可以使用 map() 方法首先对密码进行清理,然后使用 filter() 方法对清理后的密码进行判断是否满足条件。

    @Test
    public void givenOptional_whenMapWorksWithFilter_thenCorrect() {
        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);
    }

上面的代码就比较明确的展示了这个过程,首先进行清理,然后进行过滤。

使用 flatMap() 来转换值

map() 相同,我们同时还有一个 flatMap() 方法作为可选的方法来进行使用。

2 者不同的地方就是 map() 只能对值进行转换,flatMap() 可以对包装的对象进行计算。

简单来说就是 flatMap() 将包装后的对象,进行解开包装,然后进行计算。

如果上面我们说到的例子,我们对简单的 StringInteger 对象包装成了 Optional 实例,但是实际上我们使用 Optional 包装的对象比这个要复杂得多得多。

下面我们就对这个区别进行一些说明,假设我们有一个 Person 对象,在这个 Person 对象中我们有 name,age 和 password 3 个属性。

我们定义的类如下:

public class Person {
    private String name;
    private int age;
    private String 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
}

我们可以将上面的类实例化成对象后,使用 Optional 来进行包装,这种包装的过程就和我们包装 String 是一样的。

例如,我们可以使用下面的方法来完成对这个对象的 Optional 的包装:

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

注意,我们包装了 Person 这个对象,这个对象是 Optional 的一个示例。

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

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

        String name = personOptional.flatMap(Person::getName).orElseThrow(IllegalArgumentException::new);
        assertEquals("john", name);
    }

现在,我们尝试从我们使用 Optional 包装好的 Person 对象中获得用户名字的属性。

注意上面使用的代码,使用 map() 代码也是可以完成的,但是你需要分开 2 次才能获得你需要的值。

Person::getName 这个方法和 String::trim 这个方法的调用是类似的,只是一个是调用 JDK 提供的方法罢了。

考虑这样一个问题,假设我们的对象中有对象,对象中再有对象,还有对象中有 List ,Map 这样比较复杂的数据类型我们应该怎么呢。

我们是不是要不停的解包,解包再解包,这太难了。

这个时候使用 flatMap() 就可以一句话搞定了。我们对对象中属性可能使用 Optional 完成了解包。这样代码的可读性就更高了。