Google Guava之Optional

文中所述Guava版本基于29.0-jre,文中涉及到的代码完整示例请移步Github查看。

null的合理性

对于所有的Javaer来说,null类型是我们在编写代码中不可能不遇到的一个神奇的东西,当然每个人对null类型也有自己的看法和见解,在开始本篇文章之前,让我们看一下其他的一些人是如何看待null的吧。

Java JUC(java.util.concurrent)包的主要开发者Doug Lea的看法是"Null sucks."(令人恶心的null)。
null的发明者Sir C. A. R. Hoare的看法是"I call it my billion-dollar mistake." (null是我的十亿美元的错误),InfoQ的一篇文章也也介绍了null的错误。

粗心的使用null可能会引起大量惊人的错误。查看Google的code base会发现大约95%的集合不应该包含null值,当往集合里面放置null的时候,发生快速失败(fail fast)而不是安静的接收null对开发者来说更有用。

此外,null也是意义不明确的。很难明确的知道返回null到底表示什么含义。比如Map.get(key)返回null既可以表示这个key的值是null,也可以表示这个key不在Map中。null可以表示失败,可以表示成功,可以表示任何情况。使用其他的东西代替null会让含义更加明确。

这就是说,当null适合在一些场景下使用的时候,对于内存空间和存取速度来说,null使用的代价是低廉的,而且在object数组中null也是不可避免的。相对于在一些库代码中(libraries code),在应用程序的代码中使用null会导致混乱,困难而且奇怪的bugs和令人不愉快的歧义等等。当Map.get(key)返回null的时候,可以表示这个key不在map中,也可以表示这个key的值就是null。最关键的是,null不能表明null值的含义。

常见的在HashMap中是允许key和value为null,但是在ConcurrentHashMap中,key和value都不允许为null,这样设计的原因就在于Doug Lea对null的厌恶以及null的含义不明确。HashMap不是线程安全的,所以我们经常在单线程内使用,这样value为null的entry可以认为是我们自己操作造成的,而ConcurrentHashMap经常在多线程环境使用,当我们获取到一个value为null的entry时,很难知道是另外一个线程删除了这个key还是放入的entry的value就是null值。

由于这些原因,Guava的很多工具集被设计为对null产生fail fast而不允许null值使用。此外Guava提供一些工具可以帮你在必须使用null的时候更容易的使用null,也可以帮你避免使用null。

null在集合中的使用

不要在Set中使用null作为值或者在Map中使用null作为key。

当想在Map中使用null作为值时,最好把这些有着null值的key单独放在一个Set中。Map中存放null值的key和非null值的key是很简单的,但是最好不要这样做。把有着null值的key分离是更好的,并且需要考虑key的值为null对你的程序意味着什么。

如果在List中使用null,若List是很稀疏的,可能你需要使用Map<Integer, E>(Map中的key作为下标,value作为List存储的值)。这样使用可能更有效,也更准确的满足你的应用程序的需要。

是否存在可以使用的空对象(null obejct),虽然不是总是存在的。如果在枚举中添加一个常量来表示你对null值期望的含义。例如,在java.math.RoundingMode中有一个UNNECESSARY 值表示不需要舍入,如果舍入是必须的则抛出异常。

如果你确实需要null值,而且在使用不兼容null的集合中遇到了问题,请选择其他的实现方法。比如,使用Collections.unmodifiableList(Lists.newArrayList())代替ImmutableList

null的替代者Optional

Optional比null更具有可读性,它强制的要求你去拆开Optional,避免直接使用可能造成返回null的方法。在实际的编写代码中,我们经常会忘记对方法的返回值是否为null进行判断,而更能记住对方法参数传递非null值。当把方法的返回值改成Optional的时候,调用者很难忘记null的情况,因为他们不得不拆开Optional。

Guava中提供了Optionalcom.google.common.base.Optional,Java核心类库中在JDK1.8之后也加入了java.util.Optional

首先看下JDK和Guava提供的Optional类有哪些方法

JDK Guava 描述
Optional.of(T) Optional.of(T) 用非null的值构造Optional对象,使用null值会快速失败
Optional.empty() Optional.absent() 返回某种类型不存在的Optional
Optional.ofNullable(T) Optional.fromNullable(T) 把可能为null的值构造为Optional,非null值视为存在,null视为不存在
boolean isPresent() boolean isPresent() Optional是否是非null的实例
T get() T get() 返回存在的T类型的实例,不存在则抛出异常(JDK抛出异常NoSuchElementException,Guava抛出异常IllegalStateException
T orElse(T) T or(T) 返回T类型的实例,如果不存在则返回某个值
T orElseThrow() - 返回T类型的实例,如果不存在则抛出NoSuchElementException
- T orNull() 返回T类型的实例,如果不存在返回null,是fromNullable的反操作
- Set<T> asSet() Optional的值返回为一个不可变的Set,Set中包含一个元素或者为空
- Optional.fromJavaUtil(java.util.Optional<T>) 把JDK的Optional转换为Guava的Optional
- java.util.Optional<T> Optional.toJavaUtil(Optional<T>) 把Guava的Optional转换为JDK的Optional

如何有效的使用Optional来提高我们代码的健壮性和可读性?

首先看一段我们最常编写的代码

/**
 * 以自然顺序比较两个字符串并返回较大的字符串
 */
public String compare(String firstString, String secondString) {
    if (firstString.compareTo(secondString) >= 0) {
        return firstString;
    }
    return secondString;
}

这种情况下如果我们的传参是null,则会抛出空指针异常。
改进后的代码

/**
 * 以自然顺序比较两个字符串并返回较大的字符串(若是字符串为null则返回null)
 */
public String compare(String firstString, String secondString) {
    if (firstString == null || secondString == null) {
        return null;
    }
    if (firstString.compareTo(secondString) >= 0) {
        return firstString;
    }
    return secondString;
}

改进后的代码可以避免因传入null值导致出现异常,但是仍不可避免的就是对返回值的判断,如果我们按照如下方式调用

@Test
public void compare() {
    UsingOptional usingOptional = new UsingOptional();
    String first = "abcde";
    String result = usingOptional.compare(first, null);

    if (result.equalsIgnoreCase(first)) {
        System.out.println("First Win!!!");
    }
}

仍会在if语句处抛出异常,除非在比较前进行null值的判断,可有时我们不太关注null值判断的话,则会引发这样的bug。如何防止这样的情况发生呢?按照前文的建议,用Optional包装函数的返回值。

/**
 * 以自然顺序比较两个字符串并返回较大的字符串
 */
public Optional<String> compareReturnOption(String firstString, String secondString) {
    if (firstString == null || secondString == null) {
        return Optional.fromNullable(null);
    }
    if (firstString.compareTo(secondString) >= 0) {
        return Optional.of(firstString);
    }
    return Optional.of(firstString);
}

这样处理后,方法的返回值是Optional类,调用者需要拆开Optional以获取实际返回值

@Test
public void compareReturnOption() {
    UsingOptional usingOptional = new UsingOptional();
    String first = "abcde";
    Optional<String> result = usingOptional.compareReturnOption(first, null);
    if (result.isPresent()) {
        if (result.get().equalsIgnoreCase(first)) {
            System.out.println("First Win!!!");
        }
    }
}

有的人会有些困惑,这样的代码编写方式和传统的编写方式如下代码所示

@Test
public void compare() {
    UsingOptional usingOptional = new UsingOptional();
    String first = "abcde";
    String result = usingOptional.compare(first, null);

    if (result != null) {
        if (result.equalsIgnoreCase(first)) {
            System.out.println("First Win!!!");
        }
    }
}

并没有什么区别,都需要判空result != nullresult.isPresent()然后取值。我认为使用Optional包装发法返回值的重点在于能够提示调用者返回的值可能是absent,能够引起调用者的关注,防止未检查的空指针异常抛出。

更进一步,如果我们把方法参数也用Optional包装,会成什么样子呢?

/**
 * 以自然顺序比较两个字符串并返回较大的字符串
 */
public Optional<String> compareParamOption(Optional<String> firstString, Optional<String> secondString) {
    if (!firstString.isPresent() || !secondString.isPresent()) {
        return Optional.fromNullable(null);
    }
    if (firstString.get().compareTo(secondString.get()) >= 0) {
        return firstString;
    }
    return secondString;
}

可以看到方法块中每次使用参数时都要调用参数的get方法,然后看下调用方代码

@Test
public void compareParamOption() {
    UsingOptional usingOptional = new UsingOptional();
    Optional<String> first = Optional.of("abcde");
    // 这种写法会导致运行出错
    // Optional<String> second = Optional.of(null);
    Optional<String> second = Optional.fromNullable(null);
    Optional<String> result = usingOptional.compareParamOption(first, second);
    if (result.isPresent()) {
        if (result.get().equalsIgnoreCase(first.get())) {
            System.out.println("First Win!!!");
        }
    }
}

调用者需要自己创建参数的Optional包装对象,在方法代码和调用者代码中都看到了使用Optional包装参数所增加的繁琐语句,可见方法参数使用Optional包装并不是一个很好的编码实践。

更甚者,如果我们的参数时自定义类型,就需要使用Optional包装多层,判断时需要逐层拆解,工作量增长严重而且并不会比未包装的方式更健壮、更简洁。

Optional包装自定义类型

/**
 * 以自然顺序比较两个人名字并返回较大的人员
 */
public Optional<Person> comparePersonName(Optional<Person> firstPerson, Optional<Person> secondPerson) {
    if (!firstPerson.isPresent() || !secondPerson.isPresent()) {
        return Optional.fromNullable(null);
    }
    if (!firstPerson.get().getName().isPresent() || !secondPerson.get().getName().isPresent()) {
        return Optional.fromNullable(null);
    }
    if (firstPerson.get().getName().get().compareTo(secondPerson.get().getName().get()) >= 0) {
        return firstPerson;
    }
    return secondPerson;
}

所以使用Optional的最佳编程实践应该是使用Optional包装方法的返回值,调用者在接收返回之后需要判断Optional是否absent,以防止空指针异常的产生。

JDK的Optional

JDK提供的Optional相比Guava的Optional有着更多的应用场景,其中最重要的就是和Lambda配合使用给开发者带来的收益。

@Nullable@NonNull

为了在编码过程中提醒编码人员对null的注意,一些框架引入了两个注解——@Nullable@NonNull,这两个注解在程序运行的过程中不会起任何作用,只会在IDE、编译器、FindBugs检查、生成文档的时候有做提示,同时对null提供注解也是JSR 305中的提议规范。这样在编写完代码进行代码质量检查时,可以在一定程度上防止我们把对null的错误使用。

参考

posted @ 2020-05-25 09:43  weegee  阅读(751)  评论(0编辑  收藏  举报