Guava Collection Zero To Hero Part Two

第二部分主要展示Guava Collection的使用,从实际用例出发。

Guava Collection Use Case

在了解了Guava的Function和Predicate后,可以开始使用了。这篇文章里面不是文档的堆积,而是一些具体的使用场景,这些场景仅仅是我遇到的,觉得在一定程度上简化了操作,美化了代码的,希望对读者有所启发,开发自己的Best Practice。

测试用数据

测试使用一个自定义的数据结构,如下:

public class Node {
    private Integer id;
    private String title;
    private List<Integer> items;
}

1. 使用transform做抽取

很多时候我们手中会拿到一组复杂的模型,不论是从数据库还是从别人的什么接口。但是真正在我们的逻辑中需要用到的仅仅是Model种的一个或多个字段。在这种情况下,你可以尝试Lists或者Iterables工具集中的transform方法来抽取我们需要的部分。位于Lists包内的函数签名为:

public static <F, T> List<T> transform(List<F> fromList, Function< ? super F, ? extends T> function);
传入的参数分别为待处理的数据List,以及一个实现具体抽取逻辑的Function。加入我们希望抽取出Node中的id,获得所有Node的id数组。那么我们可以定义如下的Function:传入一个Node,返回该Node的Id。
Function<Node, Integer> idExtractor = new Function<Node, Integer>() {
        public Integer apply(Node input) {
            return input.getId();
        }
};
然后使用transform即可:
List<Node> nodes = Lists.newArrayList(
    Node.nodeGenerator(1), Node.nodeGenerator(2), Node.nodeGenerator(3));
List<Integer> nodeIds = Lists.transform(nodes, idExtractor);

使用的语法是非常简单的,但是这背后有一些注意的事项,甚至是隐藏的陷阱。

1.1 返回的仅仅是个Lazy的View

最容易犯的错误就是认为上面得到的结果 nodeIds是一个和我们在堆上分配的List一样的东西,但事实是,它不是的。更加准确的说,他甚至还不是他自己。为什么这么说呢?Lists是一个包含若干工具方法的类,可以理解为是博文Part One里面提到的组装场地,在这里数据和逻辑相互作用,具体的,transform方法会返回一个Guava自定义的List, 这种List并不会重新开辟内存来保存transform的返回值,它的内部仅仅保存了一个指向传入数据的引用,其get方法返回Function作用后的结果。当你用迭代器迭代这个List(包括foreach)的时候,会产生一个自定义的iterator:TransformedListIterator。这个iterator的next()返回被Function作用后的结果,迭代器禁用了set和add方法。他扩展了AbstractList,但并没有自己实现add和set方法,所以这两种操作也不被支持。总结一下:

另外需要注意的是,使用transform后你并没马上获得这个List,当你实际使用的时候才会生成,亦即lazy。如果你需要实际修改返回的List,那么需要显式的将结果拷贝出来。

List<Integer> nodeIds_supportModification = Lists.newArrayList(Lists.transform(nodes, idExtractor));

1.2警惕你的异常被吞掉

在实现Function中的apply的时候,你或许会调用一个抛出checked异常的函数。但是你的IDE会给你一个错误,因为你Override的apply方法并没有抛出任何checked异常,为了规避这个问题,你可以把一个checked的异常转化为一个Runtime异常来抛出,从而让程序正常的编译,如下:

Function<Node, Integer> exceptionFunction = new Function<Node, Integer>() {
    public Integer apply(Node input) {
        try {
            input.strangeGet(); //throws a checked exception here
            return input.getId();
        } catch (MyException e) {
            throw Throwables.propagate(e);  //transfer to unchecked exception
        }
    }
};
//checked exception is swallowed 
List ids = Lists.transform(nodes, exceptionFunction);

编译没有问题,看样子一切都还不错,但是这里隐藏了一个陷阱,我们把一个需要检查的异常变成了一个运行时异常,为了使用Guava的特性,我们为程序引入了不确定性,为了弥补上面的问题,我们需要在调用的transform的时候显式的捕捉运行时异常。

try {
    ids = Lists.transform(nodes, exceptionFunction);
} catch (Exception e) {
    throw new MyException(e);
}
但是这样使用Guava,反而让代码变得难以解读,所以当apply中需要使用抛异常的函数时,是否要或者如何使用Guava是需要权衡的。

1.3 序列化会序列化全部

当你认为你可以放心的序列化transform的返回是,可能结果并非如你所愿。首先你需要确保你的原始数据和传入的Function本身是支持序列化的。另外,当你尝试序列化transform的返回结果是,你实际序列化的其实是整个原始队列加上那个Function。倘若你的原始数据是个不小的对象,而你误以为你仅仅序列了你抽取的数据,那么可能会引入性能的损失。

2. 使用MultiMaps聚类

Guava官方文档对于MultiMap的解释可能会让你豁然开朗。“几乎所有的程序员都实现过类似于Map>这样的数据结构。” 啊哈,没错吧,我们都曾经纠结的,无数次的使用过这种数据机构,而且使用大量并不易读的嵌套来遍历这种数据结构,其结果是让自己都不喜欢自己的代码。MultiMap的在一定程度上会缓解这种情况。

我们使用这样一个场景来介绍MultiMap的用法。我们从学校的学生数据库中拉取了一个List的学生信息,每个学生的信息中有一个country的字段描述TA所来自的国家,而我们希望按照按国家来统计和处理这些学生。对于这个用例,我们可以使用index方法,其函数签名如下:

public static <K, V> ImmutableListMultimap<K, V> index(
      Iterable<V> values, Function< ? super V, K> keyFunction)

接受一个可迭代对象,已经一个用于index的函数,经过函数作用的Value,其结果将会作为生成的MultiMap的Key值。所以我们学生的用例,需要一个如下的Function,并将其传入MultiMaps下的index方法即可

Function<Student, Integer> countryIdExtractor = new Function<Student, Integer>() {
    public Integer apply(Student input) {
        return input.getCountryId();
    }
};
Function<Student, Integer> countryIdExtractor = new Function<Student, Integer>() {
    public Integer apply(Student input) {
        return input.getCountryId();
};

ListMultimap<Integer, Student> countryIdToStudents = Multimaps.index(students, countryIdExtractor);

我们获得的是一个由学生国家代码作为Key,属于该国家的所有学生集合作为Value的数据结构。对于返回值,有一些是需要我们注意的:

3. Iterables工具集

Iterables工具集提供了一些暗黑小科技,可以在一定程度上简化我们的开发工作。假设我们操作的数据来自多个数据源,但是在最后处理的过程中,希望用一次遍历,那么我们可以使用Iterables.contact,返回的结果是链接了若干个Iterable的集合的lazy view,并没有开辟新的空间,然后我们就可以使用一次迭代完成对若干个集合的遍历了。

ImmutableList<Integer> immutableList1 = ImmutableList.of(1, 2, 3, 4);
ImmutableList<Integer> immutableList2 = ImmutableList.of(1, 2, 3, 4);
Iterable<Integer> immutableListConcat = Iterables.concat(immutableList1, immutableList2);
Iterator<Integer> immutableIterator = immutableListConcat.iterator();
while (immutableIterator.hasNext()){
    if (immutableIterator.next() == 1 ) {
        immutableIterator.remove(); //unsupported operation
    }
}
注意到concat的结果集合具有和原集合相同的mutability,上面代码的异常是因为原始集合immutable造成的。由于contact返回的是一个lazy的view,意味着即使你在concat之后对原集合进行的修改,这种修改在你真正遍历concat结果的时候都会反映出来。除此之外,Iterables集合还有几个顺手的小工具:

Guava还提供了一个在Java8才能使用的Streaming方式,当然,由于Guava的函数式变成支持并不是是非彻底,所以Guava提供的Streaming并没有Java8强大,但是,始终,在大量项目嗨停留在java6/7的阶段,Guava的Streaming方式还是可以给代码带来令人愉快的变化。GuavaG是通过FluentIterable这个类来实现类Streaming编程的。举个简单的例子来展示FluentIterable:

Predicate<Student> chineseMale = new Predicate<Student>() {
    public boolean apply(Student input) {
        return Gender.MALE.equals(input.getGender()) && CountryEnum.CHINA.equals(input.getCountry());
    }
};
Function<Student, String> getName = new Function<Student, String>() {
    public String apply(Student input) {
        return input.getName();
    }
};
//获得所有来自中国的男同学的名字,取前三个并按照字母序排列
List<String> nameList = FluentIterable.from(students).filter(chineseMale).transform(getName).toSortedList(Ordering.<String>natural());

是不是有种很流畅的感觉。而且返回的值是lazy的,只有你真正使用它的时候才会计算。

未完待续...

Sincerely,
@stevenyfy

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.