java8 新特性 Stream 流
本文主要参考的是书籍《java核心技术卷Ⅱ》
认识 Stream 流
Stream 流的概念
Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。这种风格将要处理的元素集合看作一种流, 流在管道中传输,并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作 (terminal operation)得到前面处理的结果。
为什么引入 Stream 流操作
通过使用流操作可以更加的方便我们在业务操作中的使用。在这里我们不需要自己去写一些逻辑代码,我们可以说明想要完成什么任务,而不是说明如何去实现它们 ,这样就极大的方便了我们的实际开发。总结成一句话就是:我们只在乎结果不在乎实现的过程。
Stream 流与传统集合的差别
- 流并不存储数据,这些数据可能是存储在底层的集合中的也可能是根据函数生成的(这个后面会提到)。
- 流的操作不会修改数据源,例如:filter 方法只是生成一个新的流并不会对原有的数据产生影响。
- 流的操作尽可能使惰性的。这样意味着到需要结果时才会开始执行。
Stream 流的创建
-
如果使集合类可以使用 Collection 接口的 stream() 方法将集合转化成为一个流。例如:
List<String> list = new ArrayList<>(); Stream<String> stream = list.stream();
-
如果是数组可以使用静态方法 Stream.of() 和工具类 Arrays.stream() 方法
int[] a = {1, 2, 3, 4, 5}; IntStream stream = Arrays.stream(a); Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
-
使用 Stream 类提供的 generate(function) 方法, 中间的参数传递一个函数,Stream 会 无限 的调用这个方法进行生成,这样就会生成一个无限流,可以使用 limit() 方法进行限制。
// 这里会生成一个 无限流 Stream<String> generate = Stream.generate(() -> "ss"); // 会无限输出 "ss" generate.forEach(System.out::println);
Stream 流的使用
遍历和查找
Stream 支持遍历和查找元素的操作,结果返回的使 Optional 的值。
方法 | 作用 |
---|---|
Optional max() | 查找最大值 |
Optional min() | 查找最小值 |
Optional findFirst() | 查找第一个符合条件的元素 |
Optional findAny() | 查找符合条件的任意一个元素 |
boolean anyMatch() | 只要符合条件就行 |
boolean allMatch() | 所有元素符合条件才行 |
boolean noneMatch() | 没有元素符合就行 |
List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> max = list1.stream().max(Comparator.comparingInt(x -> x));
System.out.println("最大值:" + max.get());
Integer first = list1.stream().filter(x -> x > 2).findFirst().get();
System.out.println("第一个值:" + first);
System.out.println("遍历");
list1.stream().forEach(System.out::print);
在 Stream 流中可以采用 iterator 和 forEach 方法进行遍历。
收集结果
方法 | 作用 |
---|---|
void forEach(action) | 在每一个元素上使用 action 方法 |
Object[] toArray() | 产生一个对象数组 |
toList(),toSet() | 转化成集合 |
String joining(value) | 生成一个 value 进行分割的字符串 |
summarizingInt/Double/Long | 获取最大值,最小值,平均值,总和,数量 |
如果我们需要将 stream 流中的结果收集到数据结构(数组)中,那么我们可以使用 toArray() 方法但是会返回一个 Object类型的数组,这并不是我们需要的,我们可以手动指定返回类型。
List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
Object[] objects = list1.stream().toArray(); // 这里返回了一个 Object 类型的数组
// 指定类型
Integer[] integers = list1.stream().toArray(Integer[]::new);
争对将 stream 流中的数据进行转化我们也许更喜欢转换成为集合相关的 java 容器。这里我们就可以使用一个便捷方法 collect,它接受一个 Collector 接口的实例。Collectors 类提供了大量用于生成收集器的方法(收集器:一种将众多元素产生为单一结果的对象)。可以使用方法 Collectors.toList() 和 Collectors.toSet() 方法生成 List 集合 和 Set 集合。如果想要生成具体的实现类可以采用 Collectors.toCollection() 方法,例如:Collectors.toCollection(HashSet::new) 生成 HashSet
List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> collect2 = list1.stream().collect(Collectors.toList());
Set<Integer> collect3 = list1.stream().collect(Collectors.toSet());
TreeSet<Integer> collect4 = list1.stream().collect(Collectors.toCollection(TreeSet::new));
如果需要在元素之间添加分隔符,可以使用 Collectors.joining() 方法。
String result = stream.collect(Collectors.joining(", "))
如果流中包含除字符串以外的其他对象,那么我们需要先将其转换为字符串
String result = stream.map(Object::toString).collect(Collectors.joining(", "))
如果想要将流的结果约简为总和、数量、平均值、最大值或最小值,可以使用summarizing(Int|Long|Double)方法中的某一个。这些方法会接受一个将流对象映射为数值的函数,产生类型为(Int|Long|Double)SummaryStatistics的结果,同时计算总和、数量、平均值、最大值和最小值。
IntSummaryStatistics collect = Stream.of(1, 2, 3, 4, 5).collect(Collectors.summarizingInt(s->s));
System.out.println("最大值:" + collect.getMax());
System.out.println("平均值:" + collect.getAverage());
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
收集到映射表中
假设我们有一个 Stream,并且想要将其元素收集到一个映射表中,这样后续就可以通过它们的ID来查找人员了。Collectors.toMap 方法有两个函数引元,它们用来产生映射表的键和值。
employeeList.add(new Employee(1, "a", new int[]{1, 2, 3}));
employeeList.add(new Employee(2, "b", new int[]{4, 2, 3}));
employeeList.add(new Employee(3, "c", new int[]{5, 2, 3}));
employeeList.add(new Employee(4, "d", new int[]{8, 2, 3}));
employeeList.add(new Employee(5, "e", new int[]{9, 2, 3}));
Map<Integer, Employee> collect = employeeList.stream().collect(Collectors.toMap(Employee::getId, Employee::getName);
collect.forEach((k,v)-> System.out.println(k + " " + v));
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
通常情况下,map 中的 value 应该使实际的值,因此第二个函数可以使用 Function.identity()。
Map<Integer, Employee> collect = employeeList.stream().collect(Collectors.toMap(Employee::getId, Function.identity()));
collect.forEach((k,v)-> System.out.println(k + " " + v));
这里的 Function.identity(): 返回元素本身 源码中是 t -> T 这里的 t 就是类本身,这里的类为 Employee
如果有多个元素具有相同的键,就会存在冲突,收集器将会抛出一个IllegalStateException异常。可以通过提供第3个函数引元来覆盖这种行为,该函数会针对给定的已有值和新值来解决冲突并确定键对应的值。这个函数应该返回已有值、新值或它们的组合。
// 添加一个新的 employee 到 list 集合中,这样 id=5 就重复了
employeeList.add(new Employee(5, "test", new int[]{9, 2, 3}));
Map<Integer, Employee> collect = employeeList.stream().collect(Collectors.toMap(Employee::getId, Function.identity(), (oldValue, newValue)->newValue));
这里的 5 已经进行了新值的覆盖,如果没有第三个函数的判断此时就会报错 因为 key 值重复了。
群组和分区
在实际的开发过程中遇到将相同特性的值群聚成组是很常见的,则就可以使用 groupingBy() 方法。
Subject subject1 = new Subject(1, "语文", 91.0);
Subject subject2 = new Subject(2, "数学", 94.0);
Subject subject3 = new Subject(3, "英语", 92.0);
Subject subject4 = new Subject(4, "语文", 90.0);
List<Subject> subjectList = new ArrayList<>();
subjectList.add(subject1);
subjectList.add(subject2);
subjectList.add(subject3);
subjectList.add(subject4);
// 对学科的科目进行分组
Map<String, List<Subject>> collect4 =
subjectList.stream().collect(Collectors.groupingBy(Subject::getName));
collect4.forEach((k,v)-> System.out.println(k + " " + v));
当分类函数是断言函数(即返回boolean值的函数)时,流的元素可以分为两个列表:该函数返回 true 的元素和其他的元素。在这种情况下,使用 partitioningBy() 比使用 groupingBy() 更高效。
// Collectors.partitioningBy(s->...) 根据 s 中的表达式进行分组 表达式的返回值必须是 boolean 值 s 为当前的类
Map<Boolean, List<Subject>> collect1 = subjectList.stream().collect(Collectors.partitioningBy(s -> "语文".equals(s.getName())));
collect1.forEach((k,v)-> System.out.println(k + " " + v));
约简操作
reduce方法是一种用于从流中算某个值的通用机制,其最简单的形式将接受一个二元函数,并从前两个元素开始持续应用它。如果该函数是求和函数,那么就很容易解释这种机制。
Optional<Integer> reduce1 = Stream.of(1, 2, 3, 4, 5).reduce((x, y) -> {
System.out.println(x + " " + y);
return x + y;
});
从结果我们可以得知:
在第一次执行的时候 x 为数组的第一个元素 y 为数组的第二个元素,在后面执行的时候 x 为前面结果的和, y 为下一个元素 => 此时的返回结果为 Optional 类
第二种形式,可以为 reduce() 函数添加一个起点值。
Integer reduce = Stream.of(1, 2, 3, 4, 5).reduce(0, (x, y) -> {
System.out.println(x + " " + y);
return x + y;
});
从结果我们可以得知:
如果在前面添加起始点 0,在第一次执行的时候 x 为初始值0, y 为数组的第一个元素。在后面执行的时候,,x 为前面结果的和, y 为下一个元素 => 此时的结果为 基础类型 因为就算流中没有元素也有初始值 0
基本类型流
到目前为止,我们都是将整数收集到Streams中,尽管很明显,但是将每个整数都包装到包装器对象中却是很低效的。对其他基本类型来说,情况也是一样,这些基本类型是double、float、long、short、char、byte和boolean。流库中具有专门的类型IntStream、LongStream和DoubleStream,用来直接存储基本类型值,而无须使用包装器。如果想要存储short、char、byte和boolean,可以使用IntStream;而对于float,可以使用DoubleStream。
创建基本类型流
为了创建IntStream,需要调用IntStream.of() 和 Arrays.stream() 方法:
IntStream intStream = IntStream.of(1, 2, 3, 4, 5);
有可能在实际的业务中,我们的流中的数据使对象,这时我们就可以使用 mapToInt、mapToLong 或 mapToDouble 将其转换为基本类型流。
Stream<String> stream = Stream.of("qqq", "china", "American");
IntStream intStream = stream.mapToInt(String::length);
为了将基本类型流转换为对象流,需要使用boxed方法:
IntStream intStream = IntStream.of(1, 2, 3, 4, 5);
Stream<Integer> boxed = intStream.boxed();
通常,基本类型流上的方法与对象流上的方法类似。下面是主要的差异:
- toArray方法会返回基本类型数组。
- 产生可选结果的方法会返回一个OptionalInt、OptionalLong或OptionalDouble。这些类与Optional类类似,但是具有getAsInt、getAsLong和getAsDouble方法,而不是get方法。
- 具有分别返回总和、平均值、最大值和最小值的sum、average、max和min方法。对象流没有定义这些方法。
- summaryStatistics方法会产生一个类型为IntSummaryStatistics、LongSummaryStatistics或DoubleSummaryStatistics的对象,它们可以同时报告流的总和、数量、平均值、最大值
和最小值。
le。这些类与Optional类类似,但是具有getAsInt、getAsLong和getAsDouble方法,而不是get方法。 - 具有分别返回总和、平均值、最大值和最小值的sum、average、max和min方法。对象流没有定义这些方法。
- summaryStatistics方法会产生一个类型为IntSummaryStatistics、LongSummaryStatistics或DoubleSummaryStatistics的对象,它们可以同时报告流的总和、数量、平均值、最大值
和最小值。