zoukankan      html  css  js  c++  java
  • 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的错误使用。

    参考

  • 相关阅读:
    2.4 将类内联化
    2.3 提炼类
    2.2 搬移字段
    2.1 搬移函数
    1.8 替换你的算法
    1.7 以函数对象取代函数
    1.7 移除对参数的赋值动作
    1.6 分解临时变量
    1.5 引入解释性变量
    1.4 以查询取代临时变量
  • 原文地址:https://www.cnblogs.com/weegee/p/12955063.html
Copyright © 2011-2022 走看看