Stream
约 3040 字大约 10 分钟
2025-04-10
stream流分类
- 中间操作:可以有多个,每次返回一个新的流,可进行链式操作
- 结束操作:只能有一个,每次执行完了这个流也就结束了,只能放在最后
- 无状态:指元素的处理不受之前元素的影响
- 有状态:指该操作只有拿到所有元素之后才能继续下去。
Stream流的使用
1.创建
通过 java.util.Collection.stream()
方法用集合创建流
List<String> list = Arrays.asList("a", "b", "c");
// 创建一个顺序流
Stream<String> stream = list.stream();
// 创建一个并行流
Stream<String> parallelStream = list.parallelStream();
使用java.util.Arrays.stream(T[] array)
方法用数组创建流
int[] array={1,3,5,7,9};
IntStream stream = Arrays.stream(array);
使用Stream的静态方法:of()、iterate()、generate()
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(4);
stream2.forEach(System.out::println);
Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(System.out::println);
stream是顺序流,由主线程按顺序对流执行操作,而parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。
如果流中的数据量足够大,并行流可以加快处速度。除了直接创建并行流,还可以通过parallel()把顺序流转换成并行流:
Optional<Integer> findFirst = list.stream()
.parallel()
.filter(x->x>6)
.findFirst();
2.使用
➡️ 筛选(filter)
筛选,是按照一定的规则校验流中的元素,将符合条件的元素提取到新的流中的操作。
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
List<String> stringsNotEmpty = strings.stream()
.filter(p -> !p.isEmpty())
.collect(Collectors.toList());
➡️ 遍历/匹配(foreach/find/match)
Stream也是支持类似集合的遍历和匹配元素的,只是Stream中的元素是以Optional类型存在的。
List<Integer> list = Arrays.asList(7, 6, 9, 3, 8, 2, 1);
// 输出符合条件的元素
List<Integer> collect = list.stream().filter(x -> x > 6).collect(Collectors.toList());
// 匹配第一个
Optional<Integer> findFirst = list.stream().filter(x -> x > 6).findFirst();
// 匹配任意(适用于并行流)
Optional<Integer> findAny = list.parallelStream().filter(x -> x > 6).findAny();
// 是否包含符合特定条件的元素
boolean anyMatch = list.stream().anyMatch(x -> x > 6);
// 遍历打印元素值
list.stream.foreach(p -> System.out.println(p));
list.stream.foreach(System.out::println);
➡️ 聚合(max/min/count)
max、min、count这些大家都不陌生,在mysql中我们常用它们进行数据运算和统计。Java stream中也引入了这些概念和用法,极大地方便了我们对集合、数组的数据统计工作。
List<Person> personList = new ArrayList<>();
personList.add(new Person("张三", 1000, 20, "男", "北京"));
personList.add(new Person("李四", 2000, 21, "男", "南京"));
personList.add(new Person("王五", 3000, 20, "女", "合肥"));
personList.add(new Person("赵六", 4000, 22, "男", "四川"));
personList.add(new Person("孙七", 5000, 25, "女", "上海"));
// 获取工资最高的员工
Optional<Person> max = personList.stream().max(Comparator.comparingInt(Person::getSalary));
// 计算工资大于2000的有多少人
long count = personList.stream().filter(p -> p.getSalary() > 2000).count();
// 计算所有员工工资总和
int sum = personList.stream().mapToInt(Person::getSalary).sum();
// 求集合中工资非空最大值
personList.stream().max(Comparator.comparing(Persion::getSalary
,Comparator.nullsFirst(BigDecimal::compareTo))
).get();
➡️ 映射(map/flatMap)
在SQL中,借助SELECT关键字后面添加需要的字段名称,可以仅输出我们需要的字段数据,而流式处理的映射操作也是实现这一目的,在java8的流式处理中,主要包含两类映射操作:map和flatMap。
map 可以将你想要的字段映射成集合
List<String> names = students.stream()
.filter(student -> "计算机科学".equals(student.getMajor()))
.map(Student::getName)
.collect(Collectors.toList());
除此之外,java8还提供了mapToDouble(ToDoubleFunction<? super T> mapper),mapToInt(ToIntFunction<? super T> mapper),mapToLong(ToLongFunction<? super T> mapper),这些映射分别返回对应类型的流,java8为这些流设定了一些特殊的操作,比如我们希望计算所有专业为计算机科学学生的年龄之和,那么我们可以实现如下:
int totalAge = students.stream()
.filter(student -> "计算机科学".equals(student.getMajor()))
.mapToInt(Student::getAge).sum();
flatMap与map的区别在于 flatMap是将一个流中的每个值都转成一个个流,然后再将这些流扁平化成为一个流 。举例说明,假设我们有一个字符串数组String[] strs = {"java8", "is", "easy", "to", "use"};,我们希望输出构成这一数组的所有非重复字符,那么我们可能首先会想到如下实现(map):
List<String[]> distinctStrs = Arrays.stream(strs)
.map(str -> str.split("")) // 映射成为Stream<String[]>
.distinct()
.collect(Collectors.toList());
但是此时的输出:
[j, a, v, a, 8]
[i, s]
[e, a, s, y]
[t, o]
[u, s, e]
为什么呢?因为经过map处理以后每一个处理前的字符串会形成一个流,或者说它把结果处理成多个String[],而flatmap可以把这些流扁平成一个流。(flatMap是把单个元素换成的所有元素进行串起来)。
List<String> distinctStrs = Arrays.stream(strs)
.map(str -> str.split("")) // 映射成为Stream<String[]>
.flatMap(Arrays::stream) // 扁平化为Stream<String>
.distinct()
.collect(Collectors.toList());
➡️ 规约(reduce)
归约,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。
// 前面例子中的方法
int totalAge = students.stream()
.filter(student -> "计算机科学".equals(student.getMajor()))
.mapToInt(Student::getAge).sum();
// 归约操作
int totalAge = students.stream()
.filter(student -> "计算机科学".equals(student.getMajor()))
.map(Student::getAge)
.reduce(0, (a, b) -> a + b);
// 进一步简化
int totalAge2 = students.stream()
.filter(student -> "计算机科学".equals(student.getMajor()))
.map(Student::getAge)
.reduce(0, Integer::sum);
// 采用无初始值的重载版本,需要注意返回Optional
Optional<Integer> totalAge = students.stream()
.filter(student -> "计算机科学".equals(student.getMajor()))
.map(Student::getAge)
.reduce(Integer::sum); // 去掉初始值
另外收集器也提供了相应的规约操作,但是与reduce在内部实现上是有区别的,收集器更加适用于可变容器上的归约操作,这些收集器广义上均基于Collectors.reducing()实现。(后面会详细介绍)
long count = students.stream().collect(Collectors.counting());
➡️ 收集(collect)
归集(toList/toSet/toMap)
因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toList、toSet和toMap比较常用,另外还有toCollection、toConcurrentMap等复杂一些的用法。
List<Integer> list = Arrays.asList(1, 3, 4, 8, 6, 2, 20, 13); List<Integer> list1 = list.stream() .filter(a -> a % 2 == 0) .collect(Collectors.toList()); Set<Integer> list2 = list.stream() .filter(a -> a % 2 == 0) .collect(Collectors.toSet()); List<Person> personList = new ArrayList<>(); personList.add(new Person("张三", 1000, 20, "男", "北京")); personList.add(new Person("李四", 2000, 21, "男", "南京")); personList.add(new Person("王五", 3000, 20, "女", "合肥")); personList.add(new Person("赵六", 4000, 22, "男", "四川")); personList.add(new Person("孙七", 5000, 25, "女", "上海")); // 工资大于3000元的员工 Map<String, Integer> map = personList.stream() .filter(person -> person.getSalary() > 3000) .collect(Collectors.toMap(Person::getName, person -> person.getSalary()));
统计(count/averaging)
计数:count
平均值:averagingInt、averagingLong、averagingDouble
最值:maxBy、minBy
求和:summingInt、summingLong、summingDouble
统计以上所有:summarizingInt、summarizingLong、summarizingDouble
List<Person> personList = new ArrayList<>(); personList.add(new Person("张三", 1000, 20, "男", "北京")); personList.add(new Person("李四", 2000, 21, "男", "南京")); personList.add(new Person("王五", 3000, 20, "女", "合肥")); personList.add(new Person("赵六", 4000, 22, "男", "四川")); personList.add(new Person("孙七", 5000, 25, "女", "上海")); // 统计员工人数、平均工资、工资总额、最高工资 // 员工总人数 long count = personList.stream().count(); // 平均工资 Double average = personList .stream() .collect(Collectors.averagingDouble(Person::getSalary)); // 最高工资 Optional<Integer> max = personList .stream() .map(Person::getSalary).max(Integer::compare); // 工资之和 int sum = personList .stream() .mapToInt(Person::getSalary).sum(); // 一次性统计所有信息 元素个数、总和、均值、最大值、最小值 DoubleSummaryStatistics collect = personList.stream() .collect(Collectors.summarizingDouble(Person::getSalary));
分组(partitioningBy/groupingBy)
分区:将stream按条件分为两个Map,比如员工按薪资是否高于8000分为两部分。
分组:将集合分为多个Map,比如员工按性别分组。有单级分组和多级分组。
List<Person> personList = new ArrayList<>(); personList.add(new Person("张三", 1000, 20, "男", "北京")); personList.add(new Person("李四", 2000, 21, "男", "南京")); personList.add(new Person("王五", 3000, 20, "女", "合肥")); personList.add(new Person("赵六", 4000, 22, "男", "合肥")); personList.add(new Person("孙七", 5000, 25, "女", "上海")); // 分组方式一:按薪资高于3000分组 Map<Boolean, List<Person>> salaryGroup = personList.stream() .collect(Collectors.partitioningBy(p -> p.getSalary() > 3000)); List<Person> group1 = salaryGroup.get(true); List<Person> group2 = salaryGroup.get(false); // 分组方式二:按性别分组 Map<String, List<Person>> sexGroup = personList.stream() .collect(Collectors.groupingBy(Person::getSex)); List<Person> group3 = sexGroup.get("男"); List<Person> group4 = sexGroup.get("女"); // 多条件分组一:将员工先按性别分组,再按地区分组 Map<String, Map<String, List<Person>>> group = personList .stream() .collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getArea))); Map<String, List<Person>> manGroup = group.get("男"); Map<String, List<Person>> womenGroup = group.get("女"); List<Person> group5 = manGroup.get("合肥"); List<Person> group6 = womenGroup.get("上海"); // 多条件分组二:将员工先按性别分组,再按地区分组 Function<Person,List<Object>> compositeKey = e->Arrays.asList(e.getSex(),e.getArea()); Map<List<Objct>,List<Person>> collect = personList.stream().collect(Collectors.groupingBy(compositeKey,Collectors.toList())); //分组求和:按照性别分组,再加总工资 personList.stream() .collect(Collectors.groupingBy(Person::getSex,Collectors.mapping(Persion::getSalary,Collectors.reducing(0,Integer::add)))); //分组求最值:按照性别分组,再取组内工资最大值 Map<String,String> map = personList.stream().collect( Collectors.groupingBy(Person::getSex ,Collectors.collectiongAndThen(Collectors.maxBy(Comparator.comparing(Person::getSalary)) ,e->e.map(Person::getSalary).orElse(null))));
接合(joining)
将stream中的元素用特定的连接符(没有的话,则直接连接)连接成一个字符串。
List<Person> personList = new ArrayList<>(); personList.add(new Person("张三", 1000, 20, "男", "北京")); personList.add(new Person("李四", 2000, 21, "男", "南京")); personList.add(new Person("王五", 3000, 20, "女", "合肥")); personList.add(new Person("赵六", 4000, 22, "男", "合肥")); personList.add(new Person("孙七", 5000, 25, "女", "上海")); String persons = personList .stream() .map(p -> p.getName() + "-" + p.getSex() + "-" + p.getSalary()) .collect(Collectors.joining(","));
相比于stream本身的reduce方法,增加了对自定义归约的支持。
List<Person> personList = new ArrayList<>(); personList.add(new Person("张三", 6000, 20, "男", "北京")); personList.add(new Person("李四", 6500, 21, "男", "南京")); personList.add(new Person("王五", 7300, 20, "女", "合肥")); personList.add(new Person("赵六", 8000, 22, "男", "合肥")); personList.add(new Person("孙七", 9860, 25, "女", "上海")); // 每个员工减去起征点后的薪资之和(这里个税的算法并不正确,但没想到更好的例子) Integer sum = personList.stream().map(Person::getSalary).reduce(0, (i, j) -> (i + j - 5000));
排序(sorted)
sorted,中间操作。有两种排序:
sorted():自然排序,流中元素需实现Comparable接
sorted(Comparator com):Comparator排序器自定义排序
List<Person> personList = new ArrayList<>(); personList.add(new Person("张三", 16000, 20, "男", "北京")); personList.add(new Person("李四", 8500, 21, "男", "南京")); personList.add(new Person("王五", 7300, 20, "女", "合肥")); personList.add(new Person("赵六", 8000, 22, "男", "合肥")); personList.add(new Person("孙七", 15860, 25, "女", "上海")); // 按工资升序排序(自然排序) List<String> newList = personList.stream() .sorted(Comparator.comparing(Person::getSalary)) .map(Person::getName) .collect(Collectors.toList()); // 按工资倒序排序 List<String> newList2 = personList.stream() .sorted(Comparator.comparing(Person::getSalary).reversed()) .map(Person::getName) .collect(Collectors.toList()); // 先按工资再按年龄升序排序 List<String> newList3 = personList .stream() .sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)) .map(Person::getName) .collect(Collectors.toList()); // 先按工资再按年龄自定义排序(降序) List<String> newList4 = personList.stream().sorted((p1, p2) -> { if (p1.getSalary().equals(p2.getSalary())) { return p2.getAge() - p1.getAge(); } else { return p2.getSalary() - p1.getSalary(); } }).map(Person::getName).collect(Collectors.toList());
提取
流也可以进行合并(concat)、去重(distinct)、限制(limit)、跳过(skip)等操作。
String[] arr1 = {"a", "b", "c", "d"}; String[] arr2 = {"d", "e", "f", "g"}; Stream<String> stream1 = Stream.of(arr1); Stream<String> stream2 = Stream.of(arr2); // concat:合并两个流 distinct:去重 List<String> newList = Stream.concat(stream1, stream2) .distinct() .collect(Collectors.toList()); // limit:限制从流中获得前n个数据 List<Integer> collect = Stream.iterate(1, x -> x + 2) .limit(10) .collect(Collectors.toList()); // skip:跳过前n个数据 List<Integer> collect2 = Stream.iterate(1, x -> x + 2) .skip(1) .limit(5) .collect(Collectors.toList());rate(1, x -> x + 2) .skip(1) .limit(5) .collect(Collectors.toList());
3.调试
peek主要被用在debug用途。为什么说用来调试呢?请看下面的例子
Stream.of("one", "two", "three","four").peek(u -> u.toUpperCase())
.forEach(System.out::println);
通过上面的代码将每个字符串转为大写,但是此时输出结果:
one
two
three
four
当然也有例外的情况,假如我们操作的是对象:
@Data
@AllArgsConstructor
static class User{
private String name;
}
List<User> userList=Stream.of(new User("a"),new User("b"),new User("c"))
.peek(u->u.setName("kkk"))
.collect(Collectors.toList());
此时输出的结果:
PeekUsage.User(name=kkk), PeekUsage.User(name=kkk), PeekUsage.User(name=kkk)]
因为peek接收一个Consumer,而map接收一个Function。
Stream<T> peek(Consumer<? super T> action)
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Consumer是没有返回值的,它只是对Stream中的元素进行某些操作,但是操作之后的数据并不返回到Stream中,所以Stream中的元素还是原来的元素。
而Function是有返回值的,这意味着对于Stream的元素的所有操作都会作为新的结果返回到Stream中。
这就是为什么peek String不会发生变化而peek Object会发送变化的原因。