Java8一般通过称为流水线(pipeline)的中间操作(intermediate operation)来传递流元素,并在到达终端操作(terminal operation)后结束。Stream接口定义的collect方法就是一种终端操作,用于将流转换为集合。
将Stream转化为集合
单参数形式转化为集合
collect方法有两种重载形式,单参数形式:
/**
* 对Stream中的元素使用收集器执行可变规约操作。
* 这是个终端操作。
* 下面这个例子将字符串依次添加到一个ArrayList中。
* List<String> asList = stringStream.collect(Collectors.toList());
* 下面这个例子根据城市对Person进行分组:
* Map<String, List<Person>> peopleByCity
* =
* personStream.collect(Collectors.groupingBy(Person::getCity));
* 下面这个例子根据Person的state和city进行两级分组:
* Map<String, Map<String, List<Person>>> peopleByStateAndCity
* = personStream.collect(Collectors.groupingBy(Person::getState,
* Collectors.groupingBy(Person::getCity)));
* @param <R> 结果类型
* @param <A> 累加器
* @param collector 收集器
* @return 规约结果
*/
<R, A> R collect(Collector<? super T, A, R> collector);
Collector<? super T, A, R> collector这个看着有点晕,这就是传说中的收集器,是本章中要说明的对象,这篇文章中会慢慢试图把它讲清楚。 先看最简单的应用的例子:
@Test
public void testToList1() {
List<String> superHeros =
Stream.of("Mr. Furious", "The Blue Raja", "The Shoveler", "The Bowler")
.collect(Collectors.toList());
}
这个例子虽然简单,但是进一步探究,会是我们深入理解收集器的钥匙。 对于单参数形式Stream.collect方法,参数是Collector<? super T, A, R>类型的收集器,所以Collectors.toList()一定会是一个返回结果类型为Collector<? super T, A, R>的静态函数,我们看看Collectors.toList()的实现:
public static <T>
Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}
看上去还是挺复杂,CollectorImpl的构造函数有四个参数,四个参数全是函数类型。基本上5个函数就可以确定收集器的实现方式,这里出现了四个: 第一个参数是一个Supplier,简单说就是集合容器的构造器; 第二个参数称为累加器(accumulator),描述了怎么讲元素添加到集合容器中; 第三个参数被称作合并器(combiner),用于合并两个累加器容器,这个合并器主要在并行操作流时合并集合的; 第四个参数是一个特性选择器函数,特性选择是一个枚举类型:Collector.Characteristics,有三个取值项,分别表示并行优化(CONCURRENT),无需排序(UNORDERED)和直接返回参数(IDENTITY_FINISH)。 对照CollectorImpl实现,可以看出Collectors.toList()的逻辑是相当简单的:new一个ArrayList,把Stream中的元素挨着加进去。不过由于参数都是行为(函数),代码看上去不如传统的java代码直观。 可以想象,toSet方法转化集合,也是类似的实现方式,这里有个简单的例子:
@Test
public void testToSet1() {
Set<String> superHeros =
Stream.of("Mr. Furious", "The Blue Raja", "The Shoveler", "The Bowler")
.collect(Collectors.toSet());
}
还有Map的例子:
private List<Emp> list = new ArrayList<>();
@Before
public void init() {
list.add(new Emp("YuanGong1", 20, 1000));
list.add(new Emp("YuanGong2", 25, 2000));
list.add(new Emp("YuanGong3", 30, 3000));
list.add(new Emp("YuanGong4", 35, 4000));
list.add(new Emp("YuanGong5", 38, 5000));
list.add(new Emp("YuanGong6", 45, 9000));
list.add(new Emp("YuanGong7", 55, 10000));
list.add(new Emp("YuanGong8", 42, 15000));
list.add(new Emp("YuanGong1", 20, 1000));
}
@Test
public void testToMap1() {
Map<String, Integer> empMap =
list.stream()
.collect(Collectors.toMap(Emp::getName, Emp::getAge));
}
Emp::getName是生成键值所用的函数,Emp::getAge是生成Map的value所用的函数。
三参数形式转化为集合
经过上面对单参数转化为集合分析后,其实三参数形式非常容易理解。 先看三参数重载形式的签名:
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
三参数形式虽然参数多,但是实际比单参数形式更简单。很明显第一个参数supplier为结果存放容器,第二个参数accumulator为结果如何添加到容器的操作,第三个参数combiner则为多个容器的聚合策略。 下面这个例子看上去很无聊,但是很好的说明了三参数收集器的用法:
@Test
public void testCollectThreeParams() {
ArrayList<Integer> list = Stream.of(1, 2, 3, 4, 5)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
Stream.of(list).forEach(System.out::println);//[1, 2, 3, 4, 5]
}
转化为数组
Stream接口还定义了用于创建对象数组的方法toArray,它有两种重载形式:
Object[] toArray();
<A> A[] toArray(IntFunction<A[]> generator);
第一种形式返回一个包含流元素的数组,但未指定类型。 第二种形式传入一个函数并生成所需类型的新数组,数组的长度与流相同,很容易与数组构造函数引用一起使用。这种重载形式的参数是IntFunction<A[]>,即指定了返回的数组类型,也限定了数组的长度,数组长度即流元素的元素个数,如果强行指定一个与数组元素不一致的长度,会出现运行时错误。下面是一些例子:
@Test
public void testToArray1() {
//使用Object[] toArray()的例子
Object[] objs = Stream.of("The Waffler", "Reverse", "PMS")
.toArray();
System.out.println(objs.length);
//<A> A[] toArray(IntFunction<A[]> generator)的例子
String[] wannabes = Stream.of("The Waffler", "Reverse", "PMS")
.toArray(String[]::new);
System.out.println(wannabes.length);
//<A> A[] toArray(IntFunction<A[]> generator)的例子
String[] wannabes2 = Stream.of("The Waffler", "Reverse", "PMS")
.toArray(len -> new String[len]);
System.out.println(wannabes2.length);
//数组长度限定为2,会出现运行时错误
String[] wannabes3 = Stream.of("The Waffler", "Reverse", "PMS")
.toArray(len -> new String[2]);
System.out.println(wannabes2.length);
//数组长度限定为4,会出现运行时错误
String[] wannabes4 = Stream.of("The Waffler", "Reverse", "PMS")
.toArray(len -> new String[4]);
System.out.println(wannabes2.length);
}
转化为Map
为了将流转化为Map,Collectors.toMap需要传入两个Function实例,分别用于获取键和值。 下面的例子以一个包装了name和role的Actor POJO为例进行讨论。
@Test
public void testToMap() {
Set<Actor> actors = Stream.of(
new Actor("李连杰", "霍元甲"),
new Actor("孙俪", "盲女"))
.collect(Collectors.toSet());
Map<String, String> actorMap = actors.stream()
.collect(Collectors.toMap(Actor::getName, Actor::getRole));
actorMap.forEach((key, value) -> System.out.printf("%s 扮演 %s%n", key, value));
}
上述例子的打印结果为:
李连杰 扮演 霍元甲
孙俪 扮演 盲女
如果对例子稍作修改,toMap换位Collectors.toConcurrenMap,就会生成ConcurrentMap。
将线性集合添加到Map
如果不是像前面的例子那样以POJO属性作为键和值,如果键为某种对象属性,值为对象本身,需要怎么做呢? 仍旧使用上面的例子,仍旧是toMap方法,下面是两种可选的实现方式:
@Test
public void testToMap2() {
Set<Actor> actors = Stream.of(
new Actor("李连杰", "霍元甲"),
new Actor("孙俪", "盲女"))
.collect(Collectors.toSet());
//使用lambda表达式t -> t:给定一个元素并返回
Map<String, Actor> actorMap1 = actors.stream()
.collect(Collectors.toMap(Actor::getName, t -> t));
//使用静态方法Function.identity()
Map<String, Actor> actorMap2 = actors.stream()
.collect(Collectors.toMap(Actor::getName, Function.identity()));
}
对Map进行排序
Map接口始终包含一个称谓Map.Entry的公共静态内部接口,它表示一个键值对。Map接口定义的entrySet方法返回Map.Entry元素的Set。getKey和getValue是Map.Entry接口两种最常用的方法,二者分别返回与每个条目对应的建和值。 Java8之后,Map.Entry接口引入了一些新的静态方法:
//返回一个比较器,根据键的自然顺序比较Map.Entry
static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey();
//返回一个比较器,根据键的指定比较器顺序比较Map.Entry
static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp);
//返回一个比较器,根据值的自然顺序比较Map.Entry
static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue();
//返回一个比较器,根据值的指定比较器顺序比较Map.Entry
static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp);
每种类Unix系统(MacOS系统也有)都有一个系统文件:/usr/share/dict/words,这个文件收录了第二版韦氏词典的所有单词,每个单词在文件中占据一行。我们的目标是从这个文件中找出所有单词的单词长度并统计每种单词长度有多少个单词。结果太多,我们只考察长度大于20的单词长度。
@Test
public void testSortMap() {
Path path = Paths.get("/usr/share/dict/words");
System.out.println("单词长度 \t 单词个数");
try (Stream<String> lines = Files.lines(path)) {
lines.filter(s -> s.length() > 20)
.collect(Collectors.groupingBy(String::length, Collectors.counting()))
.forEach((len, num) -> System.out.printf("%d:\t\t %d%n", len, num));
} catch (IOException e) {
e.printStackTrace();
}
}
代码简单解释如下:
文件读取
文件在try-with-resources代码块内读取,Stream接口实现了AutoCloseabe,try代码块执行完毕后,Java对Stream调用close方法,然后对File调用close方法,从而释放文件资源;
groupingBy分组
Collectors.groupingBy方法传入Fucntion作为第一个参数,表示分类器,我们上述例子中,每个单词的长度作为分类器。 如果Collectors.groupingBy只传入一个参数,那么Collectors.groupingBy的结果类型为Map,分类器的值作为这个Map的键,匹配分类器的元素列表作为Map的值。我们上面的例子中,如果只传入String::length作为Collectors.groupingBy参数的话,返回的结果类型就是Map<Integer, List>。
下游收集器
本例中,双参数形式的Collectors.groupingBy方法第二个参数是另一个收集器,被称为下游收集器(downstream collector),用于对单词列表进行后期处理。我们例子中的情况下,Collectors.groupingBy返回的类型是Map<Integer,Long>,其中键是单词长度,值是词典中该长度的单词数量。
我们例子的输出结果如下:
单词长度 单词个数
21: 82
22: 41
23: 17
24: 5
从结果中看出,是按照单词长度的升序打印的结果,如果希望按照降序打印,我们需要使用Map.Entry.comparingByKey来进行排序。
@Test
public void testSortMap2() {
Path path = Paths.get("/usr/share/dict/words");
System.out.println("单词长度 \t 单词个数");
try (Stream<String> lines = Files.lines(path, Charset.defaultCharset())) {
Map<Integer, Long> map = lines.filter(s -> s.length() > 20)
.collect(Collectors.groupingBy(String::length, Collectors.counting()));
map.entrySet().stream()
.sorted(Map.Entry.comparingByKey(Comparator.reverseOrder()))
.forEach(e -> System.out.printf("单词长度 %d:\t单词个数 %d%n", e.getKey(), e.getValue()));
} catch (IOException e) {
e.printStackTrace();
}
}
返回Map<Integer, Long> 之后,程序提取entrySet流,调用Stream.sorted方法对流进行排序,排序依据是sorted的参数:Map.Entry.comparingByKey(Comparator.reverseOrder()),本例中使用自然顺序的逆序进行排序。
注:如果排序结果符合Comparable.compareTo的结果,就可以称为“自然顺序”。
Map.Entry.comparingByValue与Map.Entry.comparingByKey的使用类似。
划分(Partitioning)
Collectors.partitioningBy将Stream中的元素拆分为两部分:满足参数中Predicate与不满足Predicate的两类。 Collectors.partitioningBy有两种重载形式:
static <T>
Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate);
static <T, D, A>
Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
Collector<? super T, A, D> downstream);
第一种形式传入单个的Predicate参数,它将元素划分为满足Predicate和不满足Predicate的两类。会得到一个恰好包含两个条目的Map,其中一个值列表满足Predicate,另一个不满足Predicate。 第二种重载形式传入Collector作为第二个参数,这个Collector被称为下游收集器。下游收集器对返回的列表进行后期处理。下游收集器后续讨论。 下面的例子把字符串列表按照偶数长度和奇数长度划分。
@Test
public void testPartitioningBy1() {
List<String> strings = Arrays.asList("this", "is", "a", "long", "list", "of",
"strings", "to", "use", "as", "a", "demo");
Map<Boolean, List<String>> lengthMap = strings.stream()
.collect(Collectors.partitioningBy(s -> s.length() % 2 == 0));
lengthMap.forEach((key,value) -> System.out.printf("%5s: %s%n", key, value));
//输出结果如下:
//false: [a, strings, use, a]
// true: [this, is, long, list, of, to, as, demo]
}
分组(Grouping)
Collectors.partitioningBy将元素按照Predicate匹配结果划分Stream,Collectors.groupingBy则在一定程度上相当于SQL中group by。它有几种重载形式:
//1:仅分类器作为参数
static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier);
//2:分类器和下游收集器作为参数
static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream);
//3:分类器、下游收集器和map工厂作为参数
static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T, A, D> downstream);
由于大量的泛型和函数类型的使用,定义看上去比较复杂,下面会以一个字符串列表为例说明各种可能的用法。不过,我们总是要提供指定元素的某个属性,作为分类器。 为了保持代码适当简短,对Collectors.groupingBy等方法做了静态引用。
根据字符串长度进行分组
@Test
public void testGroupingBy1() {
List<String> strings = Stream.of("a", "bb", "cc", "ddd")
.collect(Collectors.toList());
Map<Integer, List<String>> result = strings.stream()
.collect(groupingBy(String::length));
System.out.println(result); // {1=[a], 2=[bb, cc], 3=[ddd]}
}
分组到具体的Map实现
@Test
public void testGroupingBy2() {
List<String> strings = Stream.of("a", "cc", "bb", "ddd")
.collect(toList());
TreeMap<Integer, List<String>> result = strings.stream()
.collect(groupingBy(String::length, TreeMap::new, toList()));
System.out.println(result); // {1=[a], 2=[cc, bb], 3=[ddd]}
}
提供一个下游收集器
@Test
public void testGroupingBy3() {
List<String> strings = Stream.of("a", "cc", "bb", "ddd")
.collect(toList());
Map<Integer, TreeSet<String>> result = strings.stream()
.collect(groupingBy(String::length, toCollection(TreeSet::new)));
System.out.println(result); // {1=[a], 2=[bb, cc], 3=[ddd]}
}
对分组结果进行计数
@Test
public void testGroupingBy4() {
List<String> strings = Stream.of("a", "cc", "bb", "ddd")
.collect(toList());
Map<Integer, Long> result = strings.stream()
.collect(groupingBy(String::length, counting()));
System.out.println(result); // {1=1, 2=2, 3=1}
}
分组并将个分组结果中的元素连接成字符串
@Test
public void testGroupingBy5() {
List<String> strings = Stream.of("a", "cc", "bb", "ddd")
.collect(toList());
Map<Integer, String> result = strings.stream()
.collect(groupingBy(String::length, joining(",", "[", "]")));
System.out.println(result); // {1=[a], 2=[bb,cc], 3=[ddd]}
}
分组后过滤分组结果(Collectors.filtering从java9才开始引入)
@Test
public void testGroupingBy6() {
List<String> strings = Stream.of("a", "bb", "cc", "ddd")
.collect(toList());
Map<Integer, List<String>> result = strings.stream()
.collect(groupingBy(String::length,
filtering(s -> !s.contains("c"), toList())));
System.out.println(result); // {1=[a], 2=[bb], 3=[ddd]}
}
分组后计算每组中均值
@Test
public void testGroupingBy7() {
List<String> strings = Stream.of("a", "bb", "cc", "ddd")
.collect(toList());
Map<Integer, Double> result = strings.stream()
.collect(groupingBy(String::length, averagingInt(String::hashCode)));
System.out.println(result); // {1=97.0, 2=3152.0, 3=99300.0}
}
分组后求和
@Test
public void testGroupingBy8() {
List<String> strings = Stream.of("a", "bb", "cc", "ddd")
.collect(toList());
Map<Integer, Integer> result = strings.stream()
.collect(groupingBy(String::length, summingInt(String::hashCode)));
System.out.println(result); // {1=97, 2=6304, 3=99300}
}
分组后计算每组的统计结果
@Test
public void testGroupingBy9() {
List<String> strings = Stream.of("a", "bb", "cc", "ddd")
.collect(toList());
Map<Integer, IntSummaryStatistics> result = strings.stream()
.collect(groupingBy(String::length,
summarizingInt(String::hashCode)));
System.out.println(JSON.toJSONString(result));
}
结果如下:
{
1: {
"average": 97,
"count": 1,
"max": 97,
"min": 97,
"sum": 97
},
2: {
"average": 3152,
"count": 2,
"max": 3168,
"min": 3136,
"sum": 6304
},
3: {
"average": 99300,
"count": 1,
"max": 99300,
"min": 99300,
"sum": 99300
}
}
分组后规约
@Test
public void testGroupingBy10() {
List<String> strings = Stream.of("a", "bb", "cc", "ddd")
.collect(toList());
Map<Integer, List<Character>> result = strings.stream()
.map(toStringList())
.collect(groupingBy(List::size,
reducing(List.of(), (l1, l2) -> Stream.concat(l1.stream(), l2.stream())
.collect(Collectors.toList()))));
System.out.println(result); // {1=[a], 2=[b, b, c, c], 3=[d, d, d]}
}
private static Function<String, List<Character>> toStringList() {
return s -> s.chars().mapToObj(c -> (char) c).collect(toList());
}
分组后计算最大值/最小值
@Test
public void testGroupingBy11() {
List<String> strings = Stream.of("a", "bb", "cc", "ddd")
.collect(toList());
Map<Integer, Optional<String>> result = strings.stream()
.collect(groupingBy(String::length,
Collectors.maxBy(Comparator.comparing(String::toUpperCase))));
System.out.println(result); // {1=Optional[a], 2=Optional[cc], 3=Optional[ddd]}
}
组合下游收集器
@Test
public void testGroupingBy() {
List<String> strings = Arrays.asList("this", "is", "a", "long", "list", "of",
"strings", "to", "use", "as", "a", "demo");
Map<Integer, List<String>> lengthMap = strings.stream()
.collect(Collectors.groupingBy(String::length));
lengthMap.forEach((key,value) -> System.out.printf("%5s: %s%n", key, value));
//输出结果如下:
//1: [a, a]
//2: [is, of, to, as]
//3: [use]
//4: [this, long, list, demo]
//7: [strings]
}
关于下游收集器的补充说明
上面的例子中我们发现,Collectors.groupingBy、Collectors.partitioningBy和Collectors.flatMapping等方法有一种重载形式的最后一个参数是downstream,这个downstream用于对收集器产生的结果进行二次加工,这就是所谓的下游收集器。这几种重载形式签名如下:
//groupingBy
static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream)
//partitioningBy
static <T, D, A>
Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
Collector<? super T, A, D> downstream)
//flatMapping
static <T, U, A, R>
Collector<T, ?, R> flatMapping(Function<? super T, ? extends Stream<? extends U>> mapper,
Collector<? super U, A, R> downstream)
下游收集器的用途是对于上游操作(如分组或者划分)产生的对象集合进行后期处理。 下游收集器本质上还是收集器(Collectors),下游收集器的二次加工操作往往还是对流的操作,所以收集器和Stream的一些方法命名和功能看上去比较像就不奇怪了。下表是Stream定义的一些方法和Collectors类定义的对应关系。
Stream | Collectors |
---|---|
count | counting |
map | maping |
min | minBy |
max | maxBy |
IntStream.sum | sumingInt |
LongStream.sum | sumingLong |
DoubleStream.sum | sumingDouble |
IntStream.summarizing | summarizingInt |
LongStream.summarizing | summarizingLong |
DoubleStream.summarizing | summarizingDouble |
这篇文章的整理过程中,很大程度参照了《Modern Java Recipes》和Grzegorz Piwowarek的文章,向原作者表示感谢。