Streams支持大量不一样的操做。咱们已经了解了最重要的操做,如filter
,map
。发现全部其余可用的操做(参见Stream Javadoc)。咱们深刻研究更复杂的操做collect
,flatMap
,reduce
。html
本节中的大多数代码示例使用如下人员列表进行演示:java
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name;
}
}
List<Person> persons =
Arrays.asList(
new Person("Max", 18),
new Person("Peter", 23),
new Person("Pamela", 23),
new Person("David", 12));
复制代码
Collect是一个很是有用的终端操做,以流的元素转变成一种不一样的结果,例如一个List,Set或Map。Collect接受Collector包含四种不一样操做的操做:供应商,累加器,组合器和修整器。这听起来很是复杂,可是Java 8经过Collectors类支持各类内置收集器。所以,对于最多见的操做,您没必要本身实现收集器。api
让咱们从一个很是常见的用例开始:oracle
List<Person> filtered =
persons
.stream()
.filter(p -> p.name.startsWith("P"))
.collect(Collectors.toList());
System.out.println(filtered);
复制代码
代码输出:ide
[Peter, Pamela]
复制代码
正如您所看到的,流的元素构造列表很是简单。须要一个集合而不是列表 - 只需使用Collectors.toList()
。函数
下一个示例按年龄对全部人进行分组:this
Map<Integer, List<Person>> personsByAge = persons
.stream()
.collect(Collectors.groupingBy(p -> p.age));
personsByAge
.forEach((age, p) -> System.out.format("age %s: %s\n", age, p));
复制代码
代码产出spa
age 18: [Max]
age 23: [Peter, Pamela]
age 12: [David]
复制代码
您还能够在流的元素上建立聚合,例如,肯定全部人的平均年龄:调试
Double averageAge = persons
.stream()
.collect(Collectors.averagingInt(p -> p.age));
System.out.println(averageAge);
复制代码
代码产出code
19.0
复制代码
若是您对更全面的统计信息感兴趣,汇总收集器将返回一个特殊的内置摘要统计信息对象。所以,咱们能够简单地肯定人的最小,最大和算术平均年龄以及总和和计数。
IntSummaryStatistics ageSummary =
persons
.stream()
.collect(Collectors.summarizingInt(p -> p.age));
System.out.println(ageSummary);
复制代码
代码产出
IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}
复制代码
下一个示例将全部人链接成一个字符串:
String phrase = persons
.stream()
.filter(p -> p.age >= 18)
.map(p -> p.name)
.collect(Collectors.joining(" and ", "In Germany ", " are of legal age."));
System.out.println(phrase);
复制代码
代码产出
In Germany Max and Peter and Pamela are of legal age.
复制代码
Collect接受分隔符以及可选的前缀和后缀。
为了将流元素转换为映射,咱们必须指定如何映射键和值。请记住,映射的键必须是惟一的,不然抛出一个IllegalStateException
。您能够选择将合并函数做为附加参数传递以绕过异常:
Map<Integer, String> map = persons
.stream()
.collect(Collectors.toMap(
p -> p.age,
p -> p.name,
(name1, name2) -> name1 + ";" + name2));
System.out.println(map);
复制代码
代码产出
{18=Max, 23=Peter;Pamela, 12=David}
复制代码
如今咱们知道了一些强大的Collect,让咱们尝试构建咱们本身的特殊Collect。咱们但愿将流的全部人转换为单个字符串,该字符串由|管道字符分隔的大写字母组成。为了实现这一目标,咱们建立了一个新的Collector.of()
。
Collector<Person, StringJoiner, String> personNameCollector =
Collector.of(
() -> new StringJoiner(" | "), // supplier
(j, p) -> j.add(p.name.toUpperCase()), // accumulator
(j1, j2) -> j1.merge(j2), // combiner
StringJoiner::toString); // finisher
String names = persons
.stream()
.collect(personNameCollector);
System.out.println(names);// MAX | PETER | PAMELA | DAVID
复制代码
因为Java中的字符串是不可变的,咱们须要一个帮助类StringJoiner,让Collect构造咱们的字符串。供应商最初使用适当的分隔符构造这样的StringJoiner。累加器用于将每一个人的大写名称添加到StringJoiner。组合器知道如何将两个StringJoiners合并为一个。在最后一步中,整理器从StringJoiner构造所需的String。
咱们已经学会了如何利用map
操做将流的对象转换为另外一种类型的对象。Map有点受限,由于每一个对象只能映射到另外一个对象。可是若是咱们想要将一个对象转换为多个其余对象或者根本不转换它们呢?这是flatMap救援的地方。
FlatMap将流的每一个元素转换为其余对象的流。所以,每一个对象将被转换为由流支持的零个,一个或多个其余对象。而后将这些流的内容放入返回flatMap操做流中。
在咱们看到flatMap实际操做以前,咱们须要一个适当的类型层
class Foo {
String name;
List<Bar> bars = new ArrayList<>();
Foo(String name) {
this.name = name;
}
}
class Bar {
String name;
Bar(String name) {
this.name = name;
}
}
复制代码
接下来,咱们利用有关流的知识来实例化几个对象:
List<Foo> foos = new ArrayList<>();
// create foos
IntStream
.range(1, 4)
.forEach(i -> foos.add(new Foo("Foo" + i)));
// create bars
foos.forEach(f ->
IntStream
.range(1, 4)
.forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " + f.name))));
复制代码
如今咱们列出了三个foos,每一个foos由三个数据组成。
FlatMap接受一个必须返回对象流的函数。因此为了解决每一个foo的bar对象,咱们只传递相应的函数:
foos.stream()
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name));
复制代码
代码产出
Bar1 <- Foo1
Bar2 <- Foo1
Bar3 <- Foo1
Bar1 <- Foo2
Bar2 <- Foo2
Bar3 <- Foo2
Bar1 <- Foo3
Bar2 <- Foo3
Bar3 <- Foo3
复制代码
如您所见,咱们已成功将三个foo对象的流转换为九个bar对象的流。
最后,上面的代码示例能够简化为流操做的单个管道:
IntStream.range(1, 4)
.mapToObj(i -> new Foo("Foo" + i))
.peek(f -> IntStream.range(1, 4)
.mapToObj(i -> new Bar("Bar" + i + " <- " f.name))
.forEach(f.bars::add))
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name));
复制代码
FlatMap也可用于Java 8中引入的Optional类。Optionals flatMap操做返回另外一种类型的可选对象。所以,它能够用来防止使人讨厌的null检查。
这样一个高度分层的结构:
class Outer {
Nested nested;
}
class Nested {
Inner inner;
}
class Inner {
String foo;
}
复制代码
为了解析foo外部实例的内部字符串,您必须添加多个空值检查以防止可能的NullPointerExceptions:
Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
System.out.println(outer.nested.inner.foo);
}
复制代码
利用选项flatMap操做能够得到相同的行为:
Optional.of(new Outer())
.flatMap(o -> Optional.ofNullable(o.nested))
.flatMap(n -> Optional.ofNullable(n.inner))
.flatMap(i -> Optional.ofNullable(i.foo))
.ifPresent(System.out::println);
复制代码
每一个调用flatMap返回一个Optional包装所需对象(若是存在)或null不存在。
Reduce操做将流的全部元素组合成单个结果。Java 8支持三种不一样的reduce方法。第一个将元素流简化为流的一个元素。让咱们看看咱们如何使用这种方法来肯定最老的人:
persons
.stream()
.reduce((p1, p2) -> p1.age > p2.age ? p1 : p2)
.ifPresent(System.out::println); // Pamela
复制代码
reduce方法接受一个BinaryOperator累加器函数。这其实是一个双函数,两个操做数共享同一类型,在这种状况下是Person。双函数相似于函数,但接受两个参数。示例函数比较两我的的年龄,以返回年龄最大的人。
第二种reduce方法接受标识值和BinaryOperator累加器。此方法可用于构造一个新的Person,其中包含来自流中全部其余人的聚合名称和年龄:
Person result =
persons
.stream()
.reduce(new Person("", 0), (p1, p2) -> {
p1.age += p2.age;
p1.name += p2.name;
return p1;
});
System.out.format("name=%s; age=%s", result.name, result.age);
// name=MaxPeterPamelaDavid; age=76
复制代码
第三种reduce方法接受三个参数:标识值,BiFunction累加器和类型的组合器函数BinaryOperator。因为身份值类型不限于Person类型,咱们能够利用reduce来肯定全部人的年龄总和:
Integer ageSum = persons
.stream()
.reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2);
System.out.println(ageSum); // 76
复制代码
正如你所看到的结果是76,可是究竟发生了什么?让咱们经过一些调试输出扩展上面的代码:
Integer ageSum = persons
.stream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s\n", sum, p);
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2);
return sum1 + sum2;
});
复制代码
代码产出
accumulator: sum=0; person=Max
accumulator: sum=18; person=Peter
accumulator: sum=41; person=Pamela
accumulator: sum=64; person=David
复制代码
正如你所看到的,累加器函数完成了全部的工做。它首先以初始恒等值0和第一个person Max被调用。在接下来的三个步骤中,总和随着最后一个步骤的年龄不断增长,人的总年龄达到76岁。
为何组合器永远不会被调用?并行执行相同的流将解除秘密:
Integer ageSum = persons
.parallelStream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s\n", sum, p);
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2);
return sum1 + sum2;
});
复制代码
代码产出
accumulator: sum=0; person=Pamela
accumulator: sum=0; person=David
accumulator: sum=0; person=Max
accumulator: sum=0; person=Peter
combiner: sum1=18; sum2=23
combiner: sum1=23; sum2=12
combiner: sum1=41; sum2=35
复制代码
并行执行此流会致使彻底不一样的执行行为。如今实际上调用了组合器。因为累加器是并行调用的,所以须要组合器来对各个累加值求和。