• 你应该更新的Java知识之常用程序库(一)
    你应该更新的Java知识之常用程序库(二)
    你应该更新的Java知识之构建工具
    你应该更新的Java知识之Observer
    你应该更新的Java知识之集合初始化
    你应该更新的Java知识之集合操作
    你应该更新的Java知识之惰性求值
    你应该更新的Java知识之Optional
    你应该更新的Java知识之Optional高级用法

    在传统的Java里,为了表示一个集合,我们常常会写出这样的代码:

    public class People {
        private List people;

        public void setPeople(List people) {
            this.people = people;
        }

        public List getPeople() {
           return this.people;
       }

         ...
    }

     严格说来,这样的代码存在缺陷。虽然貌似List被封装到People里,但实际上,这个List的引用却暴露在外面,这个类的用户可以轻松地拿到List的引用,随意修改。所以,下面是一种更严谨的写法:

    public class People {
        private List people;

        public void setPeople(List people) {
             this.people.addAll(people);
        }

        @SuppressWarnings("unchecked")
        public List getPeople() {
            return (List)this.people.clone();
        } 

        ...
    }

     这里的做法基本思路是,做一个副本,保证内部引用(这里的people)不会传播到外面。但在实际编写代码的时候,大多数人都不会这么做,能否意识到这样的问题只是一个方面,这样的代码写起来,也要比原来的那种写法麻烦得多。按照许多公司的做法,这种要求只能放到代码规范里,但无论如何,在程序世界里,人为规定永远是最容易忽略的约定。

     不过,在真实世界中,即便是我们写的只是上面的最简单那种形式,却很少出现问题。原因何在呢?因为大多数情况下,我们编写这样程序的时候,会有一种隐形的约定,这个“List”是不变的。我们设置(set)完这个List之后,基本上不会留着它的引用在做任何操作;而得到(get)它之后,也基本上不会去修改它。

     在这种情况下,我们使用的实际上是一个不变的List。既然是一个不变的List,那不如就更直接地把它表现出来。

     Guava为我们提供了不变集合的概念,对应着各种具体类型,有ImmutableList、ImmutableSet,还有ImmutableMap。从名字上,我们不难看出它们的用法。

     Guava不变集合的做法并不是另起炉灶,而是遵循了Java已有集合框架的约定。比如,通过查阅文档,我们不难发现,ImmutableList就是一个List,只不过使用这个“List”,只不过,当我们尝试调用诸如add、set之类试图修改集合的方法时,它会抛出异常。正是有了这样的基础,不变集合可以和很多已有的类库、框架配合在一起。

     有了这个基础,我们把不变集合的概念应用于之前的代码,它就是下面这个样子:

    public class People {
        private ImmutableList people;

        public void setPeople(ImmutableList people) {
            this.people = people;
        }

        public ImmutableList getPeople() {
            return this.people;
        } 

        ...
    }

    这样一来,代码依然很简单,但是,意义却与从前完全不一样了。我们不必再为可能存在的隐忧顾虑了:一旦元素放到集合里,就不可能修改它了,因为它是不可变的。

    对于使用这段代码的人来说,getter自不必说,如往常一样使用,setter也不费力。只是当做字面量使用的集合,我们已经在《你应该更新的Java知识之集合初始化》中讨论过了。如果要适配于现有程序库,把一个已有的List转成ImmutableList也不复杂,一个简单的copyOf方法就可以实现:

        List existingPeople = ...
        ImmutableList immutablePeople = copyOf(existingPeople);

    为了让讨论更完整,这里不得不提到另外一个不变接口的选择:Iterable。它是从Java 5开始进入JDK的一个接口:

    public interface Iterable  {
        Iterator iterator();
    }

    作为一个Java程序员,我们对Iterator简直再熟悉不过了,有了Iterator,我们就可以遍历一个集合。所以,在一些情况下,我们也可以使用Iterable表示一个不变集合。大多数Java已有的集合类都实现了这个接口。

    既然它是JDK里的东西,为什么不把它优先推荐呢?原因有几个。最重要的一个原因在于我们熟知的Iterator,它有一个方法叫做remove,可能大多数Java程序员已经习惯性地忽略了这个方法,但它就在哪里,它是一个可以“改变”集合的方法,所以,从语义上说,它不是一个很好的选择。另外,从已有的代码习惯来说,很多程序员还是很喜欢用List、Set作为接口,所以,ImmutableList从心理上来说,更接近已有的习惯。剩下的一个点似乎不那么重要,有些代码真的需要使用到特定类型的接口。不过,就大多数代码而言,我们只是要得到的一个集合,做一些操作,而这些操作我们在《你应该更新的Java知识之集合操作》中做了一些讨论。

    在函数式编程中,不变是提升程序稳定性一个很重要的概念。既然我们大多数情况下实际用到的是不变集合,那就不妨直接把它表现出来。

  • 你应该更新的Java知识之常用程序库(一)
    你应该更新的Java知识之常用程序库(二)
    你应该更新的Java知识之构建工具
    你应该更新的Java知识之Observer
    你应该更新的Java知识之集合初始化
    你应该更新的Java知识之集合操作
    你应该更新的Java知识之惰性求值
    你应该更新的Java知识之Optional

    介绍了Optinal的基本用法,我们来看一个有趣的例子,找到一个人的出生国家。按照传统的思路,代码大约是这个样子:

    Place place = person.getPlaceOfBirth();
    if (place != null) {
     City city = place.getCity();
     if (city != null) {
       Province province = city.getProvince();
       if (province != null) {
         return province.getCountry();
       }
     }
    }

    return null;

    如果你对整洁代码稍有追求,这样的if套if都会让你觉得不爽。让我们尝试用Optional改造它,不过,事先声明一下,以下代码并不在Guava代码库里,而是自行的扩展,也是弥补Guava Optional的缺失,你可以把下面的代码添加到你自己的程序库中,作为基础代码:

    首先,我们要给Optionals添加一个方法:

    public class Optionals {
       public static <T, U> Optional bind(Optional value,
                                           Function<T, Optional> function) {
         if (value.isPresent()) {
          return function.apply(value.get());
        }

        return absent();
      }
    }
    (参见具体代码

    这个方法的意图是,对一个Optional值(value)执行一个操作(function),如果value不是空,则对value执行操作,否则,返回空。

    如果单纯从这个函数,你还不是很清楚它到底能做些什么,那我们就直接来看代码:

    bind(
     bind(
       bind(
         bind(personOptional, getPlaceOfBirth()),                          
         getCityFromPlace()),
       getProvinceFromCity()),
     getCountryFromProvince());

    我们连着用了几个bind连Person对象里一层一层地往外找我们所需的值,如你所料,这里的每个函数实际上都是一个函数对象,我们就以其中的一个演示一下基本的做法:

    Function<Province, Optional> getCountryFromProvince() {
     return new Function<Province, Optional>() {
       @Override
       public Optional apply(Province input) {
         return fromNullable(input.getCountry());
       }
     };
    }

    把所有这些放在一起你就应该理解了,在这中间执行的任何一个环节如果出现空值,那么整个的返回值就是一个空值,否则,它就会一层一层的执行下去。

    这样一来,如果我们把bind函数视为程序库里的函数,那我们的客户端代码里面,一个if都没有出现,我们成功地消除那一大堆的if嵌套。

    不过,这种括号套括号的用法颇有Lisp风味,作为一个Java程序员,我们对于这样的写法还着实需要适应一下。让我们再进一步探索一下,看看怎么能把它做得更Java一些。

    public class FluentOptional {
       private Optional optional;

       private FluentOptional(Optional optional) {
         this.optional = optional;
      }

      public static FluentOptional from(Optional optional) {
         return new FluentOptional(optional);
     }

      public FluentOptional bind(Function<T, Optional> function) {
         if (isPresent()) {
             return from(function.apply(get()));
         }

          return from(Optional.absent());
     }

      ...

    (参见具体代码

    通过这段代码,我们可以用FluentOptional提供一个对Optional类的封装,这里面我们新增了两个方法from和bind,其它方法都是可以由Optional提供,实现很容易,这里省略了。我们看看通过这个新实现,我们的方法变成了什么模样:

    from(personOptional)
      .bind(getPlace())
      .bind(getCityFromPlace())
      .bind(getProvinceFromCity())
      .bind(getCountryFromProvince());

    怎么样,如此一来,代码就就像Java代码了吧!

    实际上,这种做法也是来自一种函数式编程的理念:Maybe Monad,这是Haskell程序设计语言为探索纯函数式编程所做的努力之一,这里就不做过多的介绍了。

  • 你应该更新的Java知识之常用程序库(一)
    你应该更新的Java知识之常用程序库(二)
    你应该更新的Java知识之构建工具
    你应该更新的Java知识之Observer
    你应该更新的Java知识之集合初始化
    你应该更新的Java知识之集合操作
    你应该更新的Java知识之惰性求值

    java.lang.NullPointerException,只要敢自称Java程序员,那对这个异常就再熟悉不过了。为了防止抛出这个异常,我们经常会写出这样的代码:

    Person person = people.find("John Smith");
    if (person != null) {
     person.doSomething();
    }

    遗憾的是,在绝大多数Java代码里,我们常常忘记了判断空引用,所以,NullPointerException便也随之而来了。

    “Null Sucks.”,这就是Doug Lea对空的评价。作为一个Java程序员,如果你还不知道Doug Lea是谁,那赶紧补课,没有他的贡献,我们还只能用着Java最原始的装备处理多线程。

    "I call it my billion-dollar mistake.",有资格说这话是空引用的发明者,Sir C. A. R. Hoare。你可以不知道Doug Lea,但你一定要知道这位老人家,否则,你便没资格使用快速排序。

    在Java世界里,解决空引用问题常见的一种办法是,使用Null Object模式。这样的话,在“没有什么”的情况下,就返回Null Object,客户端代码就不用判断是否为空了。但是,这种做法也有一些问题。首先,我们肯定要为Null Object编写代码,而且,如果我们想大规模应用这个模式,我们要为几乎每个类编写Null Object。

    幸好,我们还有另外一种选择:Optional。Optional是对可以为空的对象进行的封装,它实现起来并不复杂。在某些语言里,比如Scala,Optional实现成了语言的一部分。而对于Java程序员而言,Guava为我们提供了Optional的支持。闲言少叙,先来如何使用Optional,完成前面的那段代码。

    Optional person = people.find("John Smith");
    if (person.isPresent()) {
     person.get().doSomething();
    }

    这里如果isPresent()返回false,说明这是个空对象,否则,我们就可以把其中的内容取出来做自己想做的操作了。

    如果你期待的是代码量的减少,恐怕这里要让你失望了。单从代码量上来说,Optional甚至比原来的代码还多。但好处在于,你绝对不会忘记判空,因为这里我们得到的不是Person类的对象,而是Optional。

    看完了客户端代码,我们再来看看怎样创建一个Optional对象,基本的规则很简单:

    如果我们知道自己要封装的对象是一个空对象,可以用
     Optional.absent();

    如果封装的对象是一个非空对象,则可以用
     Optional.of(obj);

    如果不知道对象是否为空,就这样创建创建
     Optional.fromNullable(obj);

    有时候,当一个对象为null的时候,我们并不是简单的忽略,而是给出一个缺省值,比如找不到这个人,任务就交给经理来做。使用Optional可以很容易地做到这一点,以上面的代码为例:

      Optional person = people.find("John Smith");
      person.or(manager).doSomething()

    说白了,Optinal是给了我们一个更有意义的“空”。

  • 你应该更新的Java知识之常用程序库(一)
    你应该更新的Java知识之常用程序库(二)
    你应该更新的Java知识之构建工具
    你应该更新的Java知识之Observer
    你应该更新的Java知识之集合初始化
    你应该更新的Java知识之集合操作

    在开发中,我们经常会遇到一些需要延迟计算的情形,比如某些运算非常消耗资源,如果提前算出来却没有用到,会得不偿失。在计算机科学中,有个专门的术语形容它:惰性求值。惰性求值是一种求值策略,也就是把求值延迟到真正需要的时候。

    在Java里,我们有一个专门的设计模式几乎就是为了处理这种情形而生的:Proxy。不过,现在我们有了新的选择:Supplier。

    我们先来看看Supplier的定义:

    public interface Supplier {
     T get();
    }

    非常简单的一个定义,简而言之,得到一个对象。

    但它有什么用呢?我们可以把耗资源运算放到get方法里,在程序里,我们传递的是Supplier对象,直到调用get方法时,运算才会执行。这就是所谓的惰性求值。

    对于Java这种缺乏惰性求值的语言,惰性一般就是通过一个间接层来实现的。David Wheeler曾经说过:“计算机科学中的所有问题都可以通过引入一个间接层解决。”

    理解了基本的用法,实现一个Supplier并不困难:

    Supplier ultimateAnswerSupplier = new Supplier() {
     @Override
     public Integer get() {
       // Long time computation
       return 42;          
     }
    };

    当我们需要这个终极答案时,只要

    int ultimateAnswer = ultimateAnswerSupplier.get();

    确实很简单,但是,我知道你已经心生另一个疑问。通常实现Proxy模式,我们只会计算一次,像终极答案这样的东西,反复运算我们可承受不起,也没有必要。

    我甚至知道你已经迫不及待地打算动手实现自己的解决方案,把结果保留下来,再下次调用时,直接返回结果。但,且慢。

    不,我并不是说多线程并发让保存结果这件小事变得复杂,因为我相信你的能力。但你是否想过,如果你打算为它这么做,也就意味着,你几乎要为所有的Supplier对象这么做。反复做一件事,显然不应该是一个程序员的作为。

    幸好Guava已经给我们准备好了:

    memorizedUltimateAnswerSupplier = Suppliers.memoize(ultimateAnswerSupplier);

    memoize函数帮我打点了前面所说的那些事情:第一次get()的时候,它会调用真正Supplier,得到结果并保存下来,下次再访问就返回这个保存下来的值。

    有时候,这个值只在一段时间内是有效的,你知道我要说什么了,Guava还给我们提供了一个另一个函数,让我们可以设定过期时间:

    expirableUltimateAnswerSupplier = memoizeWithExpiration(target, 100, NANOSECONDS);

    好了,还在自己编写Proxy处理惰性求值吗?Supplier便是你需要更新的Java知识。顺便说一下,Java 8里也有Supplier哦!

  • 你应该更新的Java知识之常用程序库(一)
    你应该更新的Java知识之常用程序库(二)
    你应该更新的Java知识之构建工具
    你应该更新的Java知识之Observer
    你应该更新的Java知识之集合初始化

    我们打算做要做这样一件事,给出所有有资格过五四青年节的人的名字。如果你不知道的话,有资格过五四青年节的人要年龄在14周岁到28周岁之间。

    按照传统的思路,这段代码应该是这样写:

    List names = new ArrayList();
    for (Person person : people) {
      int age = person.getAge();
      if (age >= 14 && age <= 28) {
        names.add(person.getName());
      }
    }

    是不是很熟悉?这样的代码在我们的代码库中几乎随处可见。单就这段代码而言,本身没什么问题。这里只是举了一个简单的例子,但现实情况往往比这复杂得多,于是,我们经常看到一个for循环里嵌套了大量的代码。

    好,新风格代码时间。这次我们还是会用到Guava,先上代码后解释:

    Iterable names = transform(filter(people, isAgeQualified()), toName());

    这里先介绍两个重要的函数transform和filter,这两个函数都是对一个集合中的各个元素进行操作。

    transform如名所示,它把集合中的一个值转换为另一个值的函数,至于具体怎么转换,则由后面的函数定义。我们来看看toName的定义:

    Function<Person, String> toNames() {
      return new Function<String, Integer>() {
        @Override
        public boolean apply(Person person) {
          return person.getName();
        }
      };
    }

    这个函数的返回值是一个Function,是的,它是一个“函数”。在Java里,函数不是一等公民,所谓一等公民指的是:

    • 它可以按需创建
    • 它可以在数据结构中存储
    • 它可以作为实参传给一个函数
    • 它可以当做一个函数的返回值

    Java世界的一等公民是对象,但我们可以用对象包装函数,就像前面这样,这就是函数对象。如果你了解函数式编程,你会发现,transform接收一个函数作为它的参数,那它实际上就是一个高阶函数:

    • 接收一个或多个函数做实参
    • 以一个函数做返回值

    在函数式编程里,最为强大的一点就是通过函数的组合,我们可以做到很多未曾想过的做法,程序的可扩展性也会出乎意料地好。

    让我们继续把未介绍完的说完,接下来是filter,它表示用一个条件对集合的元素进行过滤,条件在哪?我们来看isAgeQualified的定义:

    Predicate isAgeQualified() {
      return new Predicate() {
        @Override
        public boolean apply(Person person) {
          int age = person.getAge();
          return age >= 14 && age <= 28;
        }
      };
    }

    Predicate实际上也是一种形式的函数,只不过,它的返回值是boolean,有了上面的基础,它也就不那么难以理解了。

    好,为了体现这段程序的扩展性,我们继续扩展一下我们的需求:我们要找出所有符合条件的男同胞。如果采用的是原来的循环,那么我们通常的做法是在if里面在添加一个条件,事实上,大量复杂的for循环就是这么一点一点扩充出来的。好,我们看看按照新的做法,我们会怎么做:

    Iterable names = transform(filter(people, and(isAgeQualified(), isMale()), toName());

    这里新增了isMale()函数,它的实现你也可以想到:

    Predicate isMale() {
      return new Predicate() {
        @Override
        public boolean apply(Person person) {
          return person.isMale();
        }
      };
    }

    这里还增加了一个and函数,它可以把多个Predicate组合起来。Guava本身给我们还提供许多这样的逻辑运算符,比如or、not等等。

    一旦形成了一个函数,它最大的价值在于重用,比如,我们现在要超出一些男同胞,干些体力活,那这个函数就又可以发挥价值了。

    如果你接触 Lisp 族的程序设计语言,这样的括号套括号的做法你会觉得眼熟。当我们有很多的操作要组合在一起的时候,这种写法就颇具 Lisp 风味了。之所以我们需要写成这样,很大程度是拜 Java 僵化的语法所赐,我们无法给既有类型添加方法。在一 些可以扩展类的语言里,这段代码会更流畅一些。

    Guava还给我们提供了另外一种做法,既然我们不能扩展已有的类,就不妨提供一个新的类,它的名字叫做FluentIterable:

    FluentIterable names = FluentIterable.from(people).filter(isAgeQualified()).transform(toName());

    如果你说过反if反for运动,看了这篇,你就应该知道怎么样消除集合操作中的一些if和for了。