Java 8 Stream Tutorial
java8提供了lambada表达式,函数接口,极大方便开发者。本文是翻译他人文章,语文一般,请见谅。如果有兴趣,可以通过链接阅读原文。
stream
stream代表了一系列元素,并且提供了各种各样的操作。
List<String> myList =
Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList
.stream()
.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
// C1
// C2
Stream操作要么是中间变量,要么是结束。结束方法返回void或者非Stream result。在上面,filter,map,和 sorted 是中间操作,foreach 是结束操作。
Stream的操作大部分是无干涉或者是无状态的。
比如上面的例子,没有操作修改mylist的元素,也不会依赖外部在执行可能发生改变的值。
各种类型Stream
Stream大部分是通过collections产生。Lists和Sets 支持 stream() 和 parallelStream()。parallelStream表示用多线程来并行处理(处理得当可以增加吞吐量)。本文就只讲stream()。
Arrays.asList("a1", "a2", "a3")
.stream()
.findFirst()
.ifPresent(System.out::println); // a1
通过调用stream来产生。我们也可以通过下面例子的Stream.of来产生。
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println); // a1
java8提供了Primitive Stream:IntStream,LongStream,DoubleStream.
通过intStream.range 可以替代for-loop循环
IntStream.range(1, 4)
.forEach(System.out::println);
// 1
// 2
// 3
primitive stream支持sum() 和 average()操作。
Arrays.stream(new int[] {1, 2, 3})
.map(n -> 2 * n + 1)
.average()
.ifPresent(System.out::println); // 5.0
我们也可以通过mapToInt ,mapToLong,mapToDouble 把objectStream 转化 primitive Stream
Stream.of("a1", "a2", "a3")
.map(s -> s.substring(1))
.mapToInt(Integer::parseInt)
.max()
.ifPresent(System.out::println); // 3
Primitive Stream 也可以通过 mapToObj 转化 object Stream
IntStream.range(1, 4)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3
这里是把一个double stream 先转化成 int stream 再转化成 obj stream
Stream.of(1.0, 2.0, 3.0)
.mapToInt(Double::intValue)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3
执行顺序
非终止性操作的一个重要特征是 laziness(懒).看下面的例子
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
});
该例子不会输出任何内容。这是因为非终止性操作只在有终止操作存在才会执行。
看看 foreach
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
})
.forEach(s -> System.out.println("forEach: " + s));
执行结果如下。
filter: d2
forEach: d2
filter: a2
forEach: a2
filter: b1
forEach: b1
filter: b3
forEach: b3
filter: c
forEach: c
结果的顺序可能会令人吃惊。本来应该水平的顺序执行。结果却是每个元素沿着链垂直移动。第一个字符串“d2”通过filter然后forEach,然后才是第二个字符串“a2”的处理。
这种行为可以减少在每个元素上执行操作的实际数量,当我们看到在下面的例子:
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.anyMatch(s -> {
System.out.println("anyMatch: " + s);
return s.startsWith("A");
});
// map: d2
// anyMatch: D2
// map: a2
// anyMatch: A2
操作anyMatch尽快返回true当适用于给定的输入元素。这适用于第二个元素“A2”。由于垂直的执行,map只执行两次。而不是映射的所有元素。
为什么顺序重要
下一个示例包含两个非终止性的map和filter操作,以及forEach的终止性操作。让我们再一次检查这些操作是如何被执行:
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A");
})
.forEach(s -> System.out.println("forEach: " + s));
// map: d2
// filter: D2
// map: a2
// filter: A2
// forEach: A2
// map: b1
// filter: B1
// map: b3
// filter: B3
// map: c
// filter: C
可能已经猜到map和filter都是五次调用,而forEach只调用一次。
我们可以大大减少实际执行数如果我们改变的顺序操作,移动filter到开始:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
// filter: d2
// filter: a2
// map: a2
// forEach: A2
// filter: b1
// filter: b3
// filter: c
现在map只调用一次,所以操作管道执行更快更多的元素。记得以后把filter放在前头。
让我们扩展上面的例子中,一个额外的操作,sorted:
Stream.of("d2", "a2", "b1", "b3", "c")
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
排序是一种特殊的中间操作。它是所谓的有状态操作,必须在排序过程中记住状态。
执行这个例子的结果在控制台输出如下:
sort: a2; d2
sort: b1; a2
sort: b1; d2
sort: b1; a2
sort: b3; b1
sort: b3; d2
sort: c; b3
sort: c; d2
filter: a2
map: a2
forEach: A2
filter: b1
filter: b3
filter: c
filter: d2
首先,所有collection都参与了排序。换句话说水平执行排序。
再次我们可以通过重新编排链的执行顺序来优化性能:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
// filter: d2
// filter: a2
// filter: b1
// filter: b3
// filter: c
// map: a2
// forEach: A2
在这个例子中sorted从未被调用因为filter可以合并输入集合到一个元素。所以性能增加输入集合。
重用Stream
java8 Streams 不能重复使用。只要你调用任何终端操作流即关闭:
Stream<String> stream =
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));
stream.anyMatch(s -> true); // ok
stream.noneMatch(s -> true); // exception
在anyMatch后调用noneMatch将导致以下Exception:
java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
at com.winterbe.java8.Streams5.test7(Streams5.java:38)
at com.winterbe.java8.Streams5.main(Streams5.java:28)
对于每一个终端操作,我们必须创建一个新的steam,为了克服这种局限性,我们可以创建一个流supplier,来构建所有中间操作:
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));
streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok
每次调用get()就构造了一个新的Stream,该stream保存所需的终端操作。
高级操作
stream支持大量不同的操作。我们已经了解filter或map最重要的操作。查看所有其他可用操作(请参见stream Javadoc)。让我们更深入地研究更复杂的操作collect,flatMap,reduce。
从本节中的大多数代码引用下面的Person例子:
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
Collect是流的元素转变成一种不同的结果,比如List,Set和Map。Collect接受四个不同的操作的Collector:supplier,accumulator,combiner和finisher。这听起来起初超级复杂,但Java 8已经提供大部分操作了,支持各种内置的collectors 通过Collectors类。因此,对于最常见的操作,你不必自己实现一个Collector。
让我们先从常见的用例:
List<Person> filtered =
persons
.stream()
.filter(p -> p.name.startsWith("P"))
.collect(Collectors.toList());
System.out.println(filtered); // [Peter, Pamela]
它是非常简单的构造从流到list。需要set时 - 只使用Collectors.toSet()
下一个例子所有人员按年龄分组
ap<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));
// age 18: [Max]
// age 23: [Peter, Pamela]
// age 12: [David]
collectors都非常灵活。你还可以在流中的元素创建聚合,例如所有人员的平均年龄
Double averageAge = persons
.stream()
.collect(Collectors.averagingInt(p -> p.age));
System.out.println(averageAge); // 19.0
如果您有兴趣更全面的统计,总结性collectors返回一个特殊的内置的汇总统计的对象。因此,我们可以简单地确定最小值,最大值和人员的算术平均年龄以及总和和计数。
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.
连接的collector 接受一个分隔符,以及一个可选的前缀和后缀。
为了将stream转成map,我们必须同时指定键和值应该如何被映射。请记住,映射的键必须是唯一的,否则抛出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}
现在我们知道了一些最强大的内置collectors,让我们尝试建立特殊collector。我们要把persons of stream 转化成包括所有名字的大写,用|隔开。为了实现这一点,我们通过创建一个Collector,用Collector.of()。我们必须通过Collector的四种元素:supplier
,accumulator,combiner和finisher。
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让collector建构我们的字符串。supplier通过分隔符构造StringJoiner。accumulator用于添加每个人的名字大写到StringJoiner。combiner知道如何把两个StringJoiners合并成一个。在最后步骤中,finisher从StringJoiner构造所需字符串。
FlatMap
我们已经学会了如何利用map操作流的对象转换为另一种类型的对象。map是有点限制,因为每个对象只能映射到一个其他对象。但是,如果我们想要一个对象转换成多个其他对象或者根本没有?这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,每组有3个bars。
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
正如你所看到的,我们已经成功地将3个foo的对象流分为9个对象流。
最后,上面的代码示例可以简化为流操作的单一管道:
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也可用于在Java8中引入的optional 类。Optionals flatMap操作返回另一种类型的option object。因此,它可以避免烦人的null检查。
class Outer {
Nested nested;
}
class Nested {
Inner inner;
}
class Inner {
String foo;
}
你必须添加多个null检查,以防止可能发生的NullPointerException异常,outer
实例的内部Inner String:
Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
System.out.println(outer.nested.inner.foo);
}
相同的行为可以通过利用Option 的 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的调用将会返回想要的object如果不为空的话。
Reduce
reduce将流的所有元素组合成一个单一的结果。java 8支持三种不同的reduce方法。第一个是从stream中得到一个元素。让我们看看如何使用这种方法来确定年纪最大的人:
persons
.stream()
.reduce((p1, p2) -> p1.age > p2.age ? p1 : p2)
.ifPresent(System.out::println); // Pamela
reduce接受binaryoperator函数。这实际上是一个双功能,操作同一个类型。bifunction接受两个参数。示例函数将两个人的年龄进行比较,以返回最大年龄的人。
第二个reduce接受特定值和binaryoperator蓄电池。这种方法可以用来构建一个新的人,该人的名称和年龄是其他人的名称和年龄的集合:
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接受三个参数:一个标识值,accumulator和cominer,类型是binaryoperator。由于特定值并不局限于人的类型,我们可以利用这个减少,以确定所有的年龄的总和:
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
正如你可以看到的accumulator。它第一次被调用的初始身份值0和名字Max。在接下来的三个步骤和不断增加的最后一个步骤的人的年龄达到76岁。
然而combiner为什么没被调用?并行执行相同的流将揭开秘密:
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
并行执行是完全不同的执行行为。现在combiner被执行了。由于accumulator被并行执行,combiner需要把单独的累加值再累加。
让我们深入到平行流在下一章。
Parallel Streams
流可以并行执行,以增加大量的输入元素的运行时性能。并行stream流,使用一个共同的forkjoinpool,通过静态ForkJoinPool.commonPool() 方法。底层的线程池的大小使用最多五个线程-取决于可用的物理内核的数量:
ForkJoinPool commonPool = ForkJoinPool.commonPool();
System.out.println(commonPool.getParallelism()); // 3
在我的机器上,该值是3.可以通过JVM参数进行设置
-Djava.util.concurrent.ForkJoinPool.common.parallelism=5
collection支持方法parallelstream()创建一个元素并行stream。或者你可以通过方法parallel()转换为并行Stream。
为了描述并行执行的具体行为,下面的例子将会把执行的线程打印出来:
Arrays.asList("a1", "a2", "b1", "c2", "c1")
.parallelStream()
.filter(s -> {
System.out.format("filter: %s [%s]\n",
s, Thread.currentThread().getName());
return true;
})
.map(s -> {
System.out.format("map: %s [%s]\n",
s, Thread.currentThread().getName());
return s.toUpperCase();
})
.forEach(s -> System.out.format("forEach: %s [%s]\n",
s, Thread.currentThread().getName()));
通过输出信息,能更好的了解执行过程
filter: b1 [main]
filter: a2 [ForkJoinPool.commonPool-worker-1]
map: a2 [ForkJoinPool.commonPool-worker-1]
filter: c2 [ForkJoinPool.commonPool-worker-3]
map: c2 [ForkJoinPool.commonPool-worker-3]
filter: c1 [ForkJoinPool.commonPool-worker-2]
map: c1 [ForkJoinPool.commonPool-worker-2]
forEach: C2 [ForkJoinPool.commonPool-worker-3]
forEach: A2 [ForkJoinPool.commonPool-worker-1]
map: b1 [main]
forEach: B1 [main]
filter: a1 [ForkJoinPool.commonPool-worker-3]
map: a1 [ForkJoinPool.commonPool-worker-3]
forEach: A1 [ForkJoinPool.commonPool-worker-3]
forEach: C1 [ForkJoinPool.commonPool-worker-2]
正如你可以看到的平行流利用所有可用线程从常见的forkjoinpool执行流操作。输出可能在连续运行时不同,因为实际使用的线程的行为是不确定的。
让我们通过一个额外的流操作来扩展示例,Srot:
Arrays.asList("a1", "a2", "b1", "c2", "c1")
.parallelStream()
.filter(s -> {
System.out.format("filter: %s [%s]\n",
s, Thread.currentThread().getName());
return true;
})
.map(s -> {
System.out.format("map: %s [%s]\n",
s, Thread.currentThread().getName());
return s.toUpperCase();
})
.sorted((s1, s2) -> {
System.out.format("sort: %s <> %s [%s]\n",
s1, s2, Thread.currentThread().getName());
return s1.compareTo(s2);
})
.forEach(s -> System.out.format("forEach: %s [%s]\n",
s, Thread.currentThread().getName()));
结果看起来会比较奇怪陌生
filter: c2 [ForkJoinPool.commonPool-worker-3]
filter: c1 [ForkJoinPool.commonPool-worker-2]
map: c1 [ForkJoinPool.commonPool-worker-2]
filter: a2 [ForkJoinPool.commonPool-worker-1]
map: a2 [ForkJoinPool.commonPool-worker-1]
filter: b1 [main]
map: b1 [main]
filter: a1 [ForkJoinPool.commonPool-worker-2]
map: a1 [ForkJoinPool.commonPool-worker-2]
map: c2 [ForkJoinPool.commonPool-worker-3]
sort: A2 <> A1 [main]
sort: B1 <> A2 [main]
sort: C2 <> B1 [main]
sort: C1 <> C2 [main]
sort: C1 <> B1 [main]
sort: C1 <> C2 [main]
forEach: A1 [ForkJoinPool.commonPool-worker-1]
forEach: C2 [ForkJoinPool.commonPool-worker-3]
forEach: B1 [main]
forEach: A2 [ForkJoinPool.commonPool-worker-2]
forEach: C1 [ForkJoinPool.commonPool-worker-1]
似乎Srot是只在主线程上被执行的。事实上,在一个并行流Sort使用新的java 8方法parallelsort()。正如在Javadoc,这种方法是由数组的长度决定着将顺序或并行执行:
If the length of the specified array is less than the minimum granularity, then it is sorted using the appropriate Arrays.sort method
回到最后一节的reduce的例子。我们已经发现combiner只在并行中才被调用。让我们看看哪些线程实际上涉及:
List<Person> persons = Arrays.asList(
new Person("Max", 18),
new Person("Peter", 23),
new Person("Pamela", 23),
new Person("David", 12));
persons
.parallelStream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s [%s]\n",
sum, p, Thread.currentThread().getName());
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s [%s]\n",
sum1, sum2, Thread.currentThread().getName());
return sum1 + sum2;
});
结果说明了combiner和accumulator 在并行流中都被执行。
accumulator: sum=0; person=Pamela; [main]
accumulator: sum=0; person=Max; [ForkJoinPool.commonPool-worker-3]
accumulator: sum=0; person=David; [ForkJoinPool.commonPool-worker-2]
accumulator: sum=0; person=Peter; [ForkJoinPool.commonPool-worker-1]
combiner: sum1=18; sum2=23; [ForkJoinPool.commonPool-worker-1]
combiner: sum1=23; sum2=12; [ForkJoinPool.commonPool-worker-2]
combiner: sum1=41; sum2=35; [ForkJoinPool.commonPool-worker-2]
总之,它可以说,并行流可以带来一个很好的性能提升到流与大量的输入元素。但要记住,一些并行流操作,如reduce和collect需要的额外的计算(combine操作),在顺序执行中却是不需要的。
此外,我们已经了解到,所有的并行操作流共享相同的JVM并且拥有共同forkjoinpool。因此,您可能希望避免实施缓慢的阻塞流操作,因为这可能会减慢应用程序的其他严重依赖并行Stream的部分。
总结
我的java编程指南Sttreams在这里结束。如果你在学习更多关于java 8流感兴趣,我给你推荐的javadoc文档流包。如果你想更多的了解背后的机制,你可能想读Martin Fowlers的 article about Collection Pipelines。
如果你有兴趣在JavaScript中一样,你可能想在stream.js - JavaScript实现的java 8流API一看。你可能还想阅读我的java教程8和我的java教程8犀牛。
希望本教程是有帮助的,你和你喜欢阅读它。本教程中的全部源代码托管在GitHub。随时frok库或通过推特发送给我你的反馈。
Happy coding!