JDK8
学习目标
- 了解
Stream的基本机制 - 了解
Stream的常用方法
基本机制
Stream是一个支持顺序和并行聚合操作的元素序列。
为了执行计算,流的操作被组合成一个流管道。流管道由源(可能是数组、集合、生成器函数、I/O 通道等)、零个或多个中间操作(将流转换为另一个流,例如_filter(Predicate)_) 和终端操作(产生结果或副作用,例如_count()_或_forEach(Consumer)_ )组成。
集合和流虽然有一些表面上的相似之处,但有不同的目标。集合主要关注其元素的有效管理和访问。相比之下,流不提供直接访问或操作其元素的方法,而是关注以声明方式描述其源以及将在该源上聚合执行的计算操作。
流管道,可以被视为对流源的查询。除非源明确设计用于并发修改(例如ConcurrentHashMap ),否则在查询流源时修改流源可能会导致不可预测或错误的行为。
中间操作是懒惰的,仅在发起终端操作时才对源数据进行计算,并且仅在需要时消耗源元素。
懒惰地处理流可以提高效率;在诸如上面的 filter-map-sum 示例的管道中,过滤、映射和求和可以融合到数据的单次传递中,具有最小的中间状态。
并行性
流管道可以按顺序或并行执行。
这种执行模式是流的属性。流是通过初始选择顺序或并行执行来创建的。 (例如, Collection.stream()创建一个顺序流, Collection.parallelStream()创建一个并行流。)这种执行模式的选择可以通过sequential()或parallel()方法进行修改,并且可以通过以下方式查询isParallel()方法。
除非明确指定为并行,否则 Java 中的任何流操作都是按顺序处理的。当数据量过大时,可以通过并行流的处理数据,可以显著提高效率 。
无状态与无干扰
大多数流操作接受描述用户指定行为的参数,为了保持正确的行为,这些行为参数:
- 必须是无干扰的(它们不会修改流源)
- 在大多数情况下必须是无状态的(它们的结果不应依赖于在流管道执行期间可能更改的任何状态)。
中间操作又分为无状态操作和有状态操作。
无状态操作,例如filter和map ,在处理新元素时不保留先前看到的元素的状态——每个元素都可以独立于其他元素的操作进行处理。
有状态的操作,例如distinct和sorted ,在处理新元素时可能会合并来自先前看到的元素的状态。
有状态的操作可能需要在产生结果之前处理整个输入。例如,在查看流的所有元素之前,无法通过对流进行排序产生任何结果。
如果流操作的行为参数是有状态的,则结果可能是不确定的。如下示例:
Set<Integer> seen = Collections.synchronizedSet(new HashSet<>());
stream.parallel().map(e -> { if (seen.add(e)) return 0; else return e; })
在这里,如果映射操作是并行执行的,由于线程调度的差异,相同输入的结果可能因运行而异,而对于无状态 lambda 表达式,结果将始终相同。
流的关闭
流有一个close()方法并实现了AutoCloseable ,但几乎所有的流实例在使用后实际上并不需要关闭。通常,只有源为 IO 通道的流(例如由Files.lines(Path, Charset)返回的流)才需要关闭。 大多数流由集合、数组或生成函数支持,不需要特殊的资源管理。 (如果流确实需要关闭,可以在try -with-resources 语句中将其声明为资源。)
规约操作
归约操作(也称为折叠)采用一系列输入元素并通过重复应用组合操作将它们组合成单个汇总结果,例如找到一组数字的总和或最大值,或将元素累加到列表中。流类具有多种形式的通用归约操作,reduce()和collect() ,以及多种专门的归约形式,例如sum() 、 max()或count()。
可变归约操作在处理流中的元素时将输入元素累积到可变结果容器中,例如Collection或StringBuilder。
可变归约操作调用collect() ,因为它将所需的结果收集到一个结果容器中,例如Collection。
短路
短路的意思是,没有遍历完所有的元素,就会返回结果。
An intermediate operation is short-circuiting if, when presented with infinite input, it may produce a finite stream as a result. 如果在呈现无限输入时,中间操作可能会产生有限流,则它是短路的。
A terminal operation is short-circuiting if, when presented with infinite input, it may terminate in finite time. 如果一个终端操作在有无限输入时,可能会在有限时间内终止,那么它就是短路的。
中间操作
中间操作会返回一个新的流。
过滤 filter
转换元素类型 map
map、mapToInt、mapToLong、mapToDouble
一对多转换 flatMap
flatMap、flatMapToInt、flatMapToLong、flatMapToDoubleflatMap()操作具有对流的元素应用一对多转换的效果,然后将生成的元素展平为新的流。
该系列方法,可以按照规则,将每一个元素转换未多个元素组合成的流,然后将这些流组合成一个新的流返回。
/**
* 测试 flatMap
*/
public static void testFlatMap() {
// 读取磁盘下目录中所有的子目录
File file = new File("D:\\");
Arrays.stream(Objects.requireNonNull(file.listFiles()))
.flatMap(a -> {
if (a.listFiles()!=null) {
return Arrays.stream(Objects.requireNonNull(a.listFiles()));
}
return null;
})
.map(File::getAbsolutePath).forEach(System.out::println);
}
D:\CodeRepository\Android
D:\CodeRepository\Gradle
D:\CodeRepository\Java
D:\CodeRepository\Maven
去重 distinct
这是一个有状态的中间操作。
根据元素的Object.equals(Object)进行去重。对于有顺序的源生成的流,重复元素保留第一个。对于无序的流,则不确定保留的元素。
在并行管道中保持distinct()的稳定性相对昂贵(要求操作充当完整的屏障,并具有大量缓冲开销),并且通常不需要稳定性。如果您的情况的语义允许,使用无序流源(例如generate(Supplier) )或使用unordered()删除排序约束可能会显着提高并行管道中distinct()的执行效率。如果需要与遇到顺序保持一致,并且您在并行管道中使用distinct()时遇到性能或内存利用率不佳的情况,则使用 sequence sequential() ) 切换到顺序执行可能会提高性能。
排序 sorted
这是一个有状态的中间操作。
返回由该流的元素组成的流,按自然顺序排序。如果该流的元素不是Comparable ,则在执行终端操作时可能会抛出java.lang.ClassCastException 。
对于有序流,排序是稳定的。对于无序流,不保证稳定性。
按指定的规则排序:Stream<T> sorted(Comparator<? super T> comparator)
Java8 使用 stream().sorted()对List集合进行排序
查看 peek
这是一个无状态的中间操作。
此方法的存在主要是为了支持调试,您希望在元素流过管道中的某个点时查看它们,类似于forEach,不同在于forEach是个终端操作。
示例:
Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
截取 limit
这是一个短路有状态的中间操作。返回有前面n个元素组成的新的流。
因为limit(n)被限制为按照顺序返回前n个元素。所以在并行流中,该方法的代价是昂贵的,如果可以,取消其顺序或将其转换为顺序流,将显著提升性能。
跳过 skip
这是一个有状态的中间操作。跳过前面n个元素,返回有剩余元素组成的流。
因为skip(n)被限制为按照顺序跳过前n元素。所以在并行流中,该方法的代价是昂贵的,如果可以,取消其顺序或将其转换为顺序流,将显著提升性能。
终端操作
遍历
void forEach(Consumer<? super T> action)
对此流的每个元素执行一个操作。
对于并行流管道,此操作不能保证尊重流的遇到顺序,因为这样做会牺牲并行性的好处。对于任何给定的元素,可以在库选择的任何时间和任何线程中执行操作。如果动作访问共享状态,它负责提供所需的同步。
void forEachOrdered(Consumer<? super T> action)
如果流具有定义的遇到顺序,则按照流的遇到顺序为此流的每个元素执行一个操作。
此操作一次处理一个元素,如果存在则按遇到顺序。该操作可以在库选择的任何线程中执行。
两者的区别在于,在并行流中,**forEach**的遍历时无序的,而**forEachOrdered**的遍历是有序的。
转换为数组
Object[] toArray()<A> A[] toArray(IntFunction<A[]> generator),generator用于创建一个所需类型和提供长度的新数组
规约操作 reduce
T reduce(T identity, BinaryOperator<T> accumulator)
使用提供的标识值和关联累积函数对此流的元素执行归约,并返回归约后的值。支持并行。
如下:
public static void testReduce() {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add(i);
}
int sum = 0;
for (Integer i : list) {
sum += i;
}
System.out.println(" sum is " + sum);
// 与上段代码结果等同
int finalSum = list.stream().reduce(0, Integer::sum);
System.out.println(" finalSum is " + finalSum);
}
Sum、min、max、average 和字符串连接都是归约的特殊情况。
规约函数以一种更简洁的方式来实现聚合功能,并能优雅的提供并发支持。
Optional<T> reduce(BinaryOperator<T> accumulator):返回一个Optional
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
三个参数的方法,相对于两个参数而言,第二个参数支持返回不同的类型,可用于改变返回流的元素类型。而第三个参数用于在并行的情况下,合并多个子线程的计算结果。如下示例:
// 将Integer类型转换为Long类型
List<Integer> list = new ArrayList<>();
long result = list.stream().reduce(0L, (a, b) -> a + b, (a, b) -> a + b);
可变规约操作 collect
对此流的元素执行可变归约操作。可变规约操作的规约值是一种容器。支持并行。
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
supplier:创建新结果容器的函数。对于并行执行,此函数可能会被多次调用,并且每次都必须返回一个新值。accumulator:一种关联的、非干扰的、无状态的函数,用于将附加元素合并到结果中。累加器。combiner: 一个关联的、无干扰的、无状态的函数,用于组合两个值,它必须与累加器函数兼容。用于并行执行中,合并多个子线程的计算结果。
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add(i);
}
StringBuilder stringBuilder = new StringBuilder();
for (Integer i : list) {
stringBuilder.append(i);
}
System.out.println(" stringBuilder is " + stringBuilder.toString());
// 与上面代码效果等同
String collect = list.stream().collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
System.out.println(" collect is " + collect);
JDK 中有许多现有的类,它们的签名非常适合与方法引用一起用作collect()的参数。例如,以下会将字符串累积到ArrayList中:
List<String> asList = stringStream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
以下将采用字符串流并将它们连接成单个字符串:
String concat = stringStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
<R, A> R collect(Collector<? super T, A, R> collector)
这个重构方法是对前面方法分一次封装升级,Collector接口里面封装了上面函数的方法((Supplier, BiConsumer, BiConsumer)),并提供了许多便利实现,具体方法可参考:java.util.stream.Collectors
// Accumulate names into a List
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
// Accumulate names into a TreeSet
Set<String> set = people.stream().map(Person::getName).collect(Collectors.toCollection(TreeSet::new));
// Convert elements to strings and concatenate them, separated by commas
String joined = things.stream().map(Object::toString).collect(Collectors.joining(", "));
// Compute sum of salaries of employee
int total = employees.stream().collect(Collectors.summingInt(Employee::getSalary)));
// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment));
// Compute sum of salaries by department
Map<Department, Integer> totalByDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment, Collectors.summingInt(Employee::getSalary)));
// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing = students.stream().collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
Java 8 Stream 的终极技巧——Collectors 操作
求最小值 min
这是一种特殊的规约操作。
求最大值 max
统计元素数量 count
检查元素是否存在
anyMatch:只要有一个元素匹配,则返回ture;如果流为空,返回false。这是一个短路的终端操作。allMatch:流中所有的元素都匹配或者流为空,返回true。这也是一个短路的终端操作,只要有一个不匹配,就会返回fasle。noneMatch:如果没有元素匹配或流为空,返回true。这也是一个短路的终端操作。
返回流的第一个元素 findFirst
设置一个短路的终端操作。