集合类工具Guava与惰性求值
2016-10-15 / modified at 2022-04-04 / 1.8k words / 7 mins
️This article has been over 2 years since the last update.

在Java中,各种大厂的集合类工具如下

  • org.apache.commons.collections4
  • RxJava
  • Guava
  • Java8 stream
  • Groovy(我个人更推荐)

上面所有的函数均支持泛型,由于Java8与RxJava已经用的太熟了,collections4内部设计不太喜欢,所以下文以Guava为例进行讲解

由于SpringBoot等主流项目中均使用18作为Guava版本,因此本文也用了比较老的18版

目录

[TOC]

关于惰性求值

在函数式编程中,一般有三种结果输出函数式操作后的结果:

  • 惰性求值:内部保存着最原始数据与处理函数,List被调用时才会进行执行函数,特点是循环效率最高,但是在进行RPC或者serialization时,结果不可预料,需要保证元素被遍历。每次写线上代码时,用惰性求值都感觉是一个定时炸弹。
  • 修改内部值:内部数据在处理函数被遍历时被修改,特点是空间占用最小,但是某些数据结构下比如ArrayList进行filter操作时,时间复杂度爆高。
  • 新拷贝:新建一个对象,并通过原始List与函数依次赋值。这个操作最简单安全,一般也在自己的工具类中这样写,但是执行效率较低,空间比较浪费。
1
2
3
4
5
//进行函数式变化时,需要新建一个对象
newList = new ArrayList<>();
for(String s: llist){
newList.add("Hello " + s );
}

目前FP中,比如Haskell、Java8、Guava等函数式内部实现均采用了惰性求值。

Guava的隐患点

Guava作为Google开发的框架,支持简化代码、对RandomAccess进行优化,不过它采取了惰性求值。也就是说在经过函数式转换后的List,保留了原始List数据与映射函数

1
2
3
//Guava中变换后的List
final List<F> fromList;
final Function<? super F, ? extends T> function;

也就是只有它被读取遍历时,才会进行真正的转换操作。在进行序列化时,可能直接反射搞到原始List,而导致结果仍然是转换前的List,

因此需要注意:

  1. 序列化时可能结果不可预料,需要函数同样实现序列化接口
  2. 在变换后的List中,除了list的for循环操作,其它操作(比如size,add)或多或少会导致危险或者性能问题。因此在完成函数式操作流程后,建议返回一个不可变的List或者常规的ArrayList,以免节外生枝。
  3. 在Guava中,如果你使用SOAP、JSON、Hibernate、ProtocolBuf或者自己定义的传输语法工具进行通信时,你需要测试你的序列化工具是否能够正确转化,即序列化操作本身是否对List进行遍历,比如Gson会使用CollectionTypeAdapterFactory作为List的遍历类,可以看出内部有forLoop的代码,因此在Gson使用惰性求值是安全的。

Guava的函数式操作

map

映射操作,这个是最常见的,用于将一种列表转为另一种列表,在大多数语言中使用map作为函数名称,但是Guava中使用的却是transform

1
2
3
4
5
6
List<Integer> in = Lists.newArrayList(1,3,5,7,9);
List<Integer> out = Lists.transform(in, new Function<Integer, Integer>() {
public Integer apply(Integer input) {
return input * 2 ;
}
});

当然我平时更喜欢用

1
2
3
4
5
6
List<String> s = ["aa", "bbb", "333Cs", "11eerS"]
FluentIterable.from(s).transform(new Function<String, String>() {
String apply(@Nullable String o) {
return o.toUpperCase()
}
})

Functions内有很多现成的工厂,可以翻翻找找用,比如Functions.toString()

zip

zip操作不支持,这个比较蛋疼,也就是没法用两个List拼装为一个List了

unique

unique是去重操作(guava remove duplicate)

1
2
List<String> s = ["11","2222","333","11"]
ImmutableSet.copyOf(s).asList()

如果是多个List混在一起再去重

1
2
3
List<String> s = ["11","2222","333","11"]
List<String> s2 = ["333","2222","333","14441"]
ImmutableSet.builder().addAll(s).addAll(s2).build().asList()

filter

过滤操作同样均是惰性求值,API与Java8类似,使用此函数可以避免在主业务中编写for;break;contine等代码,以简化代码还原的流程。

注意,此函数调用后的List性能极其差,除了for循环操作,其它size、add等操作都是O(N)时间,因此一定要万分小心

过滤出符合条件的List

并没有Lists.filter的操作,原因如下

The biggest concern here is that too many operations become expensive, linear-time propositions. If you want to filter a list and get a list back, and not just a Collection or an Iterable, you can use ImmutableList.copyOf(Iterables.filter(list, predicate)), which “states up front” what it’s doing and how expensive it is.

所以日常代码如下

1
2
3
4
5
Collections2.filter(in, new Predicate<Integer>() {
public boolean apply(Integer input) {
return input>3;
}
}));

或者

1
2
3
4
5
Iterables.filter(in, new Predicate<Integer>() {
public boolean apply(Integer input) {
return input>3;
}
}));

它们返回的对象都是一个危险的惰性集合。

Predicates内有很多现成的工厂,可以翻翻找找用,比如Predicates.notNull()

当然还有更推荐使用的FluentIterable.filter,我就不重复写了

一票否决式过滤

要求集合所有元素满足特定条件,有一个条件不满足就一票否决

1
2
3
4
5
boolean isAllAdult = Iterables.all(list, new Predicate<Integer>() {
public boolean apply(Integer input) {
return input >= 18;
}
});

一票通过式过滤

有一个条件满足,就返回true

1
2
3
4
5
boolean isExist = return Iterables.any(list, new Predicate<Integer>() {
public boolean apply(Integer input) {
return input> 18;
}
})

找出第一个符合条件的元素

从List中找到第一个符合条件的元素T,并返回Optional<T>对象

1
2
3
4
5
6
7
Optional<Integer> adult = Iterables.tryFind(in, new Predicate<Integer>() {
public boolean apply(Integer input) {
return input> 18;
}
});
//找到就返回Integer,没有就返回0
return adult.or(0);

还有另一种带默认值的方法

1
2
3
4
5
Integer adult = Iterables.find(in, new Predicate<Integer>() {
public boolean apply(Integer input) {
return input> 18;
}
},0);

如何安全地生成新拷贝对象

刚刚已经说过了,惰性求值过度优化可能导致奇葩结果。为了安全地传递给下家,可能需要将惰性求值转换为新值。

第一种方法(最推荐),如果想偷懒的话,直接创建一个一定执行遍历操作的对象,将返回一个不可变的数组

1
2
3
4
5
6
FluentIterable.from(in)
.transform(new Function<Integer, String>() {
public String apply(Integer input) {
return Integer.toHexString(input);
}
}).toList();

或者下面这样的,但是更丑,结果比较保险,是常见的数组List

1
2
ImmutableList.copyOf(...)
Lists.newArrayList(...)

对比Java8的惰性求值,还是Java8更简洁

1
2
3
4
5
List<String> o = in.stream().map(new Function<Integer, String>() {
public String apply(Integer integer) {
return Integer.toHexString(integer);
}
}).collect(Collectors.<String>toList());

目前我已经很少使用Guava,主要是换成了Stream与Groovy,可以参考这里

Refference

  1. http://stackoverflow.com/questions/8458663/guava-why-is-there-no-lists-filter-function
  2. http://stackoverflow.com/questions/10834577/guava-iterables-filter-vs-collections2-filter-any-big-difference
  3. https://github.com/google/guava/issues/505
  4. http://www.yinwang.org/blog-cn/2013/04/01/lazy-evaluation