一,Stream流概述
Stream 流 是 Java 8 引入的一个强大的数据处理工具,它允许以 声明式(告诉计算机“做什么”) 和 函数式(无副作用、链式调用) 的方式对集合、数组或 I/O 数据源进行高效操作。它的核心目标是让开发者写出更简洁、更具表达力的代码,同时支持并行化处理以提升性能。
Stream 的三大核心特点:
- 非存储:不存储数据,只定义操作流程。
- 不可变:每次操作生成新流,原数据不受影响。
- 延迟执行:中间操作不会立即执行,只有触发终端操作时才计算。
使用步骤:
-
先得到一条 Stream 流,并把数据放上去
-
利用 Stream 流中的 API 进行各种操作
过滤,转换,统计,打印等操作
这些方法可分为:中间方法和终结方法
中间方法:方法调用完毕后,还可调用其他方法。比如过滤
终结方法:方法调用完毕后,不能调用其他方法了,比如输出打印
二,Stream流使用
2.1 获取Stream流对象
| 获取方式 | 方法名 | 说明 |
|---|---|---|
| Collection单列集合 | default Stream stream() | Collection中的默认方法 |
| Map双列集合 | 无 | 无法直接使用stream流 |
| 数组 | public static Stream stream(T[] array) | Arrays工具类中的静态方法 |
| 一堆零散的数据 | public static Stream of(T...Values) | Stream接口中的静态方法,T...Value是可变参数 |
代码举例
import java.util.ArrayList;
import java.util.Collections;
public class Main {
public static void main(String[] args) {
//1.Collection单列集合创建Stream流
ArrayList<String> list=new ArrayList<>();
Collections.addAll(list,"a","b","c","d");
list.stream().forEach(s-> System.out.println(s));//其中list.stream()就已经获取了stream流对象了
//.forEach是Stream流中的终结方法
//2.Map集合创建Stream流
//Map集合创建Stream流需要先获取其中的keySet或者EntrySet从而创建流
HashMap<String,String> map=new HashMap<>();
map.put("a","A");
map.put("b","B");
map.put("c","C");
map.keySet().stream()
.forEach(s-> System.out.println(s+"="+map.get(s)));//这段是Stream流的方法
map.entrySet().stream()
.forEach(s-> System.out.println(s.getKey()+"="+s.getValue()));//这段是Stream流的方法
//3.数组获取Stream流
//数组获取Stream流就需要借助Arrays工具类来创建
int[] numbers=new int[]{1,2,3,4,5};
Arrays.stream(numbers)
.forEach(n-> System.out.println(n));//这段是Stream流的方法
//4.一堆零散的数据获取Stream流(必须是相同数据类型)
Stream.of(1,2,3,4,5)
.forEach(n-> System.out.println(n));//这段是Stream流的方法
}
}
2.2 Stream流的中间方法
2.2.1 常用中间方法
| 方法名 | 说明 |
|---|---|
| Stream<T> filter<Predicate<? super T> predicate> | 过滤 |
| Stream<T> limit(long maxSize) | 获取前个元素 |
| Stream<T> skip(long n) | 跳过前几个元素 |
| Stream<T> distinct() | 元素去重(依赖hashCode和equals方法) |
| static Stream<T> concat(Stream a,Stream b) | 合并a和b两个流为一个流 |
| Stream<R> map(Function<T,R> mapper) | 转换流中的数据类型 |
注意:
-
中间方法,返回新的 Stream 流,原来的 Stream 流只能使用一次,建议采用链式编程。
虽然流只能使用一次!但是如果没有使用终结方法!可以当作变量存储起来,用于 concat 流合并中使用!
-
修改 Stream 流中的数据,不会影响到原数组或集合中的数据
-
filter 过滤方法中的参数是一个函数式接口,使用 Lambda 表达式即可
-
map 中的参数也是一个函数式接口,需要使用 Lambda 表达式简化
用法举例:
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
Collections.addAll(list,"1","2","3","11","22","33","111","222","333");
//filter方法详解
list.stream().filter(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length()==1;//s表示流中每一个数据,如果返回值为true表示留下数据,false表示不留下数据
}
}).forEach(s-> System.out.println(s));
//简化版filter
list.stream()
.filter(s->s.length()==1)//过滤,只留下长度为1的数据
.forEach(s-> System.out.println(s));//遍历输出结果:1,2,3
//其他方法
list.stream()
.limit(4)//截取前4个元素
.forEach(s-> System.out.println(s)); //遍历结果:1,2,3,11
list.stream()
.skip(4)//跳过前4个元素
.forEach(s-> System.out.println(s));//遍历结果:"22","33","111","222","333"
list.add("1");
list.stream()
.distinct()//去重
.forEach(s-> System.out.println(s));//遍历
//合并流
ArrayList<String> list2=new ArrayList<>();
Collections.addAll(list,"a","b","c","d","e");
Stream.concat(list.stream(),list2.stream())//利用Stream流中静态方法发将两个小流合并为一条大流
.forEach(s-> System.out.println(s));
//map方法转换数据类型
list.stream()
.map(s-> Integer.parseInt(s))//将原本的数据类型转化为Int类型
.forEach(s-> System.out.println(s));
//map方法详细写法
list.stream().map(new Function<String, Integer>() {//第一个泛型是数据原本的类型,第二个泛型是要转化的类型
@Override
public Integer apply(String s) {//这个s表示流里面每一个数据
return Integer.parseInt(s);
}
}).forEach(s-> System.out.println(s)); //在这里的forEatch中s就是整数类型了
}
}
2.2.2 peek
peek()是 Stream API 中的一个中间操作(intermediate operation),主要用于调试和观察流处理过程中的元素变化,一般 peek 是我们在 debug 的时候看看指定集合的数据变化用的,不会在代码里面写peek
核心特点
-
非终结操作,延迟执行
List<String> list = Arrays.asList("a", "b", "c"); // 只有peek,没有终端操作 - 不会执行! list.stream() .peek(s -> System.out.println("peek: " + s)); // 控制台不会输出任何内容 -
需要终端操作触发
List<String> list = Arrays.asList("a", "b", "c"); // 有终端操作才会执行 list.stream() .peek(s -> System.out.println("Before: " + s)) .map(String::toUpperCase) .peek(s -> System.out.println("After: " + s)) .count(); // 终端操作触发流执行输出
Before: a After: A Before: b After: B Before: c After: C
主要用途
-
调试和日志记录(主要用途)
List<String> result = Stream.of("apple", "banana", "cherry") .filter(s -> s.length() > 5) .peek(s -> System.out.println("Filter passed: " + s)) // 调试filter .map(String::toUpperCase) .peek(s -> System.out.println("After map: " + s)) // 调试map .collect(Collectors.toList()); // 输出: // Filter passed: banana // After map: BANANA // Filter passed: cherry // After map: CHERRY -
观察中间状态
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream() .peek(n -> System.out.println("原始: " + n)) .map(n -> n * 2) .peek(n -> System.out.println("乘以2后: " + n)) .filter(n -> n > 5) .peek(n -> System.out.println("过滤后: " + n)) .mapToInt(Integer::intValue) .sum(); System.out.println("总和: " + sum);
注意点:不要把这个写到代码里面去!这个是用来调试看看流里面数据流转状态用的!
2.2.3 anyMatch和allMatch
介绍
anyMatch:检查流中是否有任意一个元素满足条件allMatch:检查流中是否所有元素都满足条件
就像考试:
anyMatch:班上有没有人考了100分?allMatch:班上同学是不是都及格了?
简单示例
List<Integer> scores = Arrays.asList(80, 75, 90, 60, 85);
// anyMatch: 有没有人考了100分?
boolean hasPerfectScore = scores.stream()
.anyMatch(score -> score == 100);
System.out.println("有没有人考100分? " + hasPerfectScore); // false
// allMatch: 是不是所有人都及格了(>=60)?
boolean allPassed = scores.stream()
.allMatch(score -> score >= 60);
System.out.println("是不是都及格了? " + allPassed); // true
2.3 Stream流中的终结方法
2.3.1 常用终结方法
| 方法名 | 说明 |
|---|---|
| void forEach(Consumer action) | 遍历 |
| long count() | 统计流内元素个数 |
| toArray(IntFunction<> function) | 收集流中数据,放到数组中 |
| collect(Collector collector) | 收集流中数据,放到集合中 |
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
//遍历
ArrayList<String> list=new ArrayList<>();
Collections.addAll(list,"1","2","3","11","22","33","111","222","333");
list.stream().forEach(s-> System.out.println(s));//遍历,这玩意是个函数式接口,方法和集合的forEach是一模一样
//统计
long count = list.stream().count();//统计流中数据个数
System.out.println(count);
//toArray方法详写
list.stream().toArray(new IntFunction<String[]>() {//IntFunction泛型:具体转化指定数组
@Override
public String[] apply(int value) {//value流中数据的个数
return new String[value];//方法体:创建数组
}//返回值:具体的数组
});
//Lambda表达式简化
list.stream().toArray(value->new String[value]);
//collect方法(可以收集流中数据并放到集合中,可以放到List,Set)
//1.收集到List集合中(实际创建的是ArrayList)
List<String> newList = list.stream()//收集到list集合中
.filter(s -> s.length() == 2)
.collect(Collectors.toList());//Collectors是Stream接口中的工具类,toList是工具类中的方法
System.out.println(newList);
//2.收集到Set中(实际创建的是HashSet)
Set<String> newSet = list.stream()//注意Set集合特性,无重复元素!!
.limit(3)
.collect(Collectors.toSet());
System.out.println(newSet);
}
}
2.3.2 reduce方法
reduce 操作是一种通用的归约操作, 它可以实现从 Stream 中生成一个值, 其生成的值不是随意的, 而是根据指定的计算模型,它是终结方法
,它将流中的元素组合起来,生成一个单一的结果。
代码举例:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int result = numbers
.stream()
.reduce(0, (subtotal, element) -> subtotal + element);
assertThat(result).isEqualTo(21);
java 中对 reduce 有三个重载方法:
-
形式 1:
T reduce(T identity, BinaryOperator<T> accumulator)最基本的 reduce 操作
// 计算1到10的和 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); int sum = numbers.stream() .reduce(0, (a, b) -> a + b); // identity = 0 System.out.println("Sum: " + sum); // 输出: 55工作原理:
初始值: identity = 0 第一次: 0 + 1 = 1 第二次: 1 + 2 = 3 第三次: 3 + 3 = 6 ... 第十次: 45 + 10 = 55重要特性:
identity(初始值)既是起点,也是流为空时的返回值- 对于并行流,
identity会被多次使用,必须是幂等的
-
形式 2:
Optional<T> reduce(BinaryOperator<T> accumulator)没有初始值的 reduce
// 计算最大值(没有初始值) Optional<Integer> max = numbers.stream() .reduce((a, b) -> a > b ? a : b); // 使用max()方法更直观 Optional<Integer> max2 = numbers.stream() .max(Integer::compareTo); // 安全地获取值 if (max.isPresent()) { System.out.println("Max: " + max.get()); } else { System.out.println("Stream is empty"); } // 简洁写法 max.ifPresent(m -> System.out.println("Max: " + m));特点:
- 返回
Optional,因为流可能为空 - 适用于没有明显初始值的场景
- 返回
-
形式 3:
U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)最通用的形式,支持类型转换和并行操作
// 计算字符串长度总和 List<String> words = Arrays.asList("Java", "Stream", "API", "Reduce"); // 使用reduce转换类型并计算 Integer totalLength = words.stream() .reduce( 0, // identity: 初始长度总和为0 (sum, word) -> sum + word.length(), // accumulator: 累加长度 (sum1, sum2) -> sum1 + sum2 // combiner: 合并部分结果 ); System.out.println("Total length: " + totalLength); // 输出: 16 // 简写版本 Integer totalLength2 = words.stream() .reduce(0, (sum, word) -> sum + word.length(), Integer::sum);
reduce 三个参数解析:
想象一下:
- 你班上有10个同学
- 每个人都有一个考试分数
- 你要计算全班的平均分
情况 1:你自己一个人算(顺序流)
// 你一个人算,需要一个本子(初始值)
int total = 0; // 这是identity(初始值)
// 然后你一个个问同学分数,累加到本子上
total = total + 张三的分数; // 这是accumulator(累加器)
total = total + 李四的分数;
// ... 一直到第10个同学
情况 2:你们小组分工算(并行流)
// 你们3个人分工:
// 你算第1-3个同学,A同学算第4-6个,B同学算第7-10个
// 每个人拿一个空本子(初始值)
int 你的本子 = 0; // identity
int A的本子 = 0; // identity
int B的本子 = 0; // identity
// 各自计算(并行执行)
你的本子 = 0 + 分数1 + 分数2 + 分数3; // accumulator
A的本子 = 0 + 分数4 + 分数5 + 分数6; // accumulator
B的本子 = 0 + 分数7 + 分数8 + 分数9 + 分数10; // accumulator
// 最后把三个本子的结果加起来
int 最终总分 = 你的本子 + A的本子 + B的本子; // 这是combiner(合并器)
参数对照表
| 参数 | 含义 | 类比 | 顺序流中的作用 | 并行流中的作用 |
|---|---|---|---|---|
| identity | 初始值 | 空本子 | 计算的起点 | 每个线程的起点 |
| accumulator | 累加器 | 往本子上写 | 累加每个元素 | 每个线程内部累加 |
| combiner | 合并器 | 合并本子 | 基本不用 | 合并各线程结果 |
代码举例
-
计算总分
List<Integer> scores = Arrays.asList(80, 90, 70, 85, 95); // 你自己一个人算 int total1 = scores.stream() .reduce( 0, // 初始值:总分从0开始 (sum, score) -> { // 累加器:每次怎么加? System.out.println("sum=" + sum + ", score=" + score); return sum + score; // 当前总分 + 新分数 }, (a, b) -> { // 合并器(顺序流不会用到) System.out.println("合并: " + a + " + " + b); return a + b; } ); System.out.println("总分: " + total1);输出:
sum=0, score=80 sum=80, score=90 sum=170, score=70 sum=240, score=85 sum=325, score=95 总分: 420 -
用并行流计算(看到 combiner 的作用 )
int total2 = scores.parallelStream() .reduce( 0, (sum, score) -> { System.out.println(Thread.currentThread().getName() + ": sum=" + sum + ", score=" + score); return sum + score; }, (a, b) -> { System.out.println(Thread.currentThread().getName() + ": 合并 " + a + " + " + b); return a + b; } );可能的输出:
ForkJoinPool.commonPool-worker-1: sum=0, score=70 main: sum=0, score=80 ForkJoinPool.commonPool-worker-2: sum=0, score=90 ForkJoinPool.commonPool-worker-1: sum=70, score=85 main: sum=80, score=95 ForkJoinPool.commonPool-worker-1: 合并 70 + 155 main: 合并 80 + 90 main: 合并 225 + 170 总分: 420
2.2.3 Collector 详解
Collector 就是把流里的东西 "收集" 成你想要的样子
想象你有一堆散乱的乐高积木(Stream 中的元素),Collector 就是:
- 一个盒子:把积木装起来(
toList()) - 一个分类盒:按颜色分类放(
groupingBy()) - 一个统计器:数数有多少块(
counting()) - 一个拼接器:把积木拼成一句话(
joining())
最简单的收集:collect() 方法
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 1. 收集到 List
List<String> list = names.stream()
.collect(Collectors.toList());
// 2. 收集到 Set(自动去重)
Set<String> set = names.stream()
.collect(Collectors.toSet());
// 3. 收集到特定的集合类型
LinkedList<String> linkedList = names.stream()
.collect(Collectors.toCollection(LinkedList::new));
Collector 的三大核心操作
每个 Collector 都做这三件事:
// 伪代码理解:
.collect(
供应商::new, // 1. 创建一个空容器(Supplier)
容器::添加元素, // 2. 把元素添加到容器(Accumulator)
容器1::合并容器2 // 3. 合并多个容器(Combiner)- 并行时用
)
最常用的 Collector 方法
-
转换成集合
List<String> fruits = Arrays.asList("apple", "banana", "apple", "orange"); // 转 List(保持顺序,允许重复) List<String> list = fruits.stream() .collect(Collectors.toList()); // [apple, banana, apple, orange] // 转 Set(去重,不保证顺序) Set<String> set = fruits.stream() .collect(Collectors.toSet()); // [banana, orange, apple] 顺序可能不同 // 转特定集合 TreeSet<String> treeSet = fruits.stream() .collect(Collectors.toCollection(TreeSet::new)); // 排序的Set:[apple, banana, orange] // 转 Map(需要两个函数:key提取器,value提取器) Map<String, Integer> lengthMap = fruits.stream() .distinct() // 去重,否则key冲突 .collect(Collectors.toMap( fruit -> fruit, // key: 水果名 fruit -> fruit.length() // value: 名字长度 )); // {orange=6, banana=6, apple=5} -
字符串拼接 -
joining()List<String> words = Arrays.asList("Hello", "World", "Java"); // 简单拼接 String result1 = words.stream() .collect(Collectors.joining()); // HelloWorldJava // 用分隔符拼接 String result2 = words.stream() .collect(Collectors.joining(", ")); // Hello, World, Java // 用分隔符,并添加前缀后缀 String result3 = words.stream() .collect(Collectors.joining(", ", "[", "]")); // [Hello, World, Java] -
分组 -
groupingBy()(超级常用!)class Person { String name; int age; String city; // 构造方法、getter省略 } List<Person> people = Arrays.asList( new Person("Alice", 25, "北京"), new Person("Bob", 30, "上海"), new Person("Charlie", 25, "北京"), new Person("David", 30, "上海"), new Person("Eve", 25, "广州") ); // 按城市分组 Map<String, List<Person>> byCity = people.stream() .collect(Collectors.groupingBy(Person::getCity)); /* { 北京=[Alice(25), Charlie(25)], 上海=[Bob(30), David(30)], 广州=[Eve(25)] } */ // 按年龄分组 Map<Integer, List<Person>> byAge = people.stream() .collect(Collectors.groupingBy(Person::getAge)); /* { 25=[Alice(北京), Charlie(北京), Eve(广州)], 30=[Bob(上海), David(上海)] } */ // 如果确定每个key只有一个value,直接用toMap Map<String, Person> personById = people.stream() .collect(Collectors.toMap( Person::getId, // key提取器 person -> person, // value提取器 (oldVal, newVal) -> newVal // 合并函数:key冲突时用新的 )); // {1=Alice, 2=Bob, 3=Charlie} -
分区 -
partitioningBy()特殊的分组:只分两组(true/false)
// 把人们分成:成年人(>=18)和未成年人 Map<Boolean, List<Person>> adults = people.stream() .collect(Collectors.partitioningBy(p -> p.getAge() >= 18)); // { // true=[所有成年人], // false=[所有未成年人] // } // 示例:成绩及格/不及格 List<Integer> scores = Arrays.asList(45, 80, 90, 55, 30, 75); Map<Boolean, List<Integer>> passed = scores.stream() .collect(Collectors.partitioningBy(score -> score >= 60)); // { // true=[80, 90, 75], // 及格 // false=[45, 55, 30] // 不及格 // } -
映射后再收集 -
mapping()
```java
// 先映射,再收集
List<String> names = people.stream()
.collect(Collectors.mapping(
Person::getName, // 先映射成名字
Collectors.toList() // 再收集成List
));
// [Alice, Bob, Charlie, David, Eve]
// 分组 + 映射:每个城市有哪些人名
Map<String, List<String>> namesByCity = people.stream()
.collect(Collectors.groupingBy(
Person::getCity,
Collectors.mapping(Person::getName, Collectors.toList())
));
/*
{
北京=[Alice, Charlie],
上海=[Bob, David],
广州=[Eve]
}
*/
```
高级组合用法
-
多级分组(Group By 的 Group By)
// 先按城市分组,再按年龄分组 Map<String, Map<Integer, List<Person>>> nestedGroup = people.stream() .collect(Collectors.groupingBy( Person::getCity, Collectors.groupingBy(Person::getAge) )); /* { 北京={ 25=[Alice, Charlie] }, 上海={ 30=[Bob, David] }, 广州={ 25=[Eve] } } */ -
分组后统计
// 每个城市有多少人? Map<String, Long> countByCity = people.stream() .collect(Collectors.groupingBy( Person::getCity, Collectors.counting() )); // {北京=2, 上海=2, 广州=1} // 每个城市的平均年龄? Map<String, Double> avgAgeByCity = people.stream() .collect(Collectors.groupingBy( Person::getCity, Collectors.averagingInt(Person::getAge) )); // {北京=25.0, 上海=30.0, 广州=25.0} // 每个城市的人名列表(用逗号拼接) Map<String, String> namesByCityJoined = people.stream() .collect(Collectors.groupingBy( Person::getCity, Collectors.mapping( Person::getName, Collectors.joining(", ") ) )); // {北京=Alice, Charlie, 上海=Bob, David, 广州=Eve} -
找出每个组的最大值
// 每个城市最年长的人 Map<String, Optional<Person>> oldestByCity = people.stream() .collect(Collectors.groupingBy( Person::getCity, Collectors.maxBy(Comparator.comparingInt(Person::getAge)) )); // 或者直接获取(不用Optional) Map<String, Person> oldestByCity2 = people.stream() .collect(Collectors.groupingBy( Person::getCity, Collectors.collectingAndThen( Collectors.maxBy(Comparator.comparingInt(Person::getAge)), Optional::get ) ));
2.4 并行流
2.4.1 什么是并行流?
并行流(Parallel Stream)是 Java 8 引入的多线程数据处理方式,能自动将数据拆分到多个 CPU 核心并行处理。与普通流(顺序流)相比:
| 特性 | 顺序流 | 并行流 |
|---|---|---|
| 线程使用 | 单线程 | 多线程(ForkJoinPool) |
| 处理方式 | 顺序执行 | 数据分片并行处理 |
| 适用场景 | 小数据量/简单操作 | 大数据量/复杂计算 |
并行流的工作原理:背后是 ForkJoinPool
// 查看并行流使用的线程池
List<Integer> numbers = IntStream.range(1, 101)
.boxed()
.collect(Collectors.toList());
numbers.parallelStream()
.forEach(num -> {
System.out.println(Thread.currentThread().getName() + ": " + num);
});
// 输出可能类似:
// ForkJoinPool.commonPool-worker-1: 3
// ForkJoinPool.commonPool-worker-2: 1
// main: 2
// ForkJoinPool.commonPool-worker-3: 4
ForkJoinPool = 工作窃取(Work-Stealing)的线程池
就像:
- 普通线程池:一个老板(线程池)分配任务给工人(线程)
- ForkJoinPool:工人们自己从公共任务队列里偷任务干,谁闲谁就多干
基本概念
核心思想:分而治之(Divide and Conquer)
// 传统做法:一个人干所有活 public int sum(int[] array) { int sum = 0; for (int num : array) { sum += num; } return sum; } // ForkJoin做法:把大任务拆成小任务,多人并行干 // 1. Fork(拆分):把大数组拆成两个小数组 // 2. Join(合并):把两个结果加起来
2.4.2 并行流的创建方法
// 方式1:直接创建并行流
List<String> result = list.parallelStream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// 方式2:将顺序流转并行
List<String> result = list.stream()
.parallel() // 转换为并行流
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
注意要点:
- 使用线程安全的数据结构(如
ConcurrentHashMap) - 避免修改外部共享变量
- 状态无关的操作优先(无依赖的 map/filter)
2.4.3 什么时候适合使用并行流?
2.4.3.1 适合使用并行流的场景
-
大数据量计算
// 计算1千万个数字的和 long start = System.currentTimeMillis(); // 顺序流 long sum1 = LongStream.rangeClosed(1, 10_000_000) .sum(); long time1 = System.currentTimeMillis() - start; start = System.currentTimeMillis(); // 并行流(快很多!) long sum2 = LongStream.rangeClosed(1, 10_000_000) .parallel() .sum(); long time2 = System.currentTimeMillis() - start; System.out.println("顺序流: " + time1 + "ms"); System.out.println("并行流: " + time2 + "ms"); // 并行流可能快3-5倍! -
CPU 密集型操作
// 模拟CPU密集型计算:计算每个数字的复杂函数 List<Double> results = IntStream.range(1, 10000) .parallel() // 并行计算,充分利用多核CPU .mapToDouble(n -> { // 模拟复杂计算 double result = 0; for (int i = 0; i < 1000; i++) { result += Math.sin(n) * Math.cos(n) * Math.tan(n); } return result; }) .boxed() .collect(Collectors.toList()); -
独立无状态操作
// 处理图片文件(每个文件处理独立) List<File> imageFiles = getAllImageFiles(); List<ProcessedImage> processed = imageFiles.parallelStream() .map(file -> { // 每个图片处理互不影响 return processImage(file); }) .collect(Collectors.toList());
2.4.3.2 不适合使用并行流的场景
-
小数据量
// 只有几个元素,线程创建开销 > 并行收益 List<String> smallList = Arrays.asList("a", "b", "c"); // ❌ 不要用并行流 long count = smallList.parallelStream() .count(); // 可能更慢! -
顺序依赖的操作
// findFirst, limit, skip 等需要顺序的操作 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // findFirst需要第一个元素,并行可能乱序 Optional<Integer> first = numbers.parallelStream() .filter(n -> n > 2) .findFirst(); // 可能是3,也可能是4或5! // 用顺序流 Optional<Integer> firstCorrect = numbers.stream() .filter(n -> n > 2) .findFirst(); // 一定是3 -
有状态操作
// 错误的例子:有状态的Lambda List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); List<Integer> result = new ArrayList<>(); // 危险!并行时ArrayList不是线程安全的 list.parallelStream() .forEach(result::add); // 可能丢失数据或抛出异常! // 正确:使用线程安全的收集器 List<Integer> safeResult = list.parallelStream() .collect(Collectors.toList()); -
I/O 密集型操作
// 从网络读取数据(I/O等待,CPU空闲) List<URL> urls = getUrls(); // 并行流可能创建太多线程,导致资源耗尽 List<String> contents = urls.parallelStream() .map(url -> { // 大部分时间在等待网络I/O return readFromUrl(url); }) .collect(Collectors.toList()); // 更适合用CompletableFuture或专用线程池
三,方法引用
方法引用就是把已有的方法拿过来用,当作函数式接口中的抽象方法的方法体。
方法引用的使用需要满足以下四个条件:
- 引用处必须是函数式接口,即
@FunctionalInterface注解的只有一个方法的接口 - 被引用的方法必须已经存在
- 被引用的方法的形参和返回值需要和抽象方法保持一致
- 被引用方法的功能要满足当前需求
举例:
import java.util.Arrays;
public class main {
public static void main(String[] args) {
Integer[] arr={3,5,4,1,6,2};
Arrays.sort(arr,main::subtraction);//方法引用,表示引用main类中的subtraction方法
Arrays.stream(arr).forEach(s-> System.out.println(s));
}
public static int subtraction(int num1,int num2){
return num2-num1;
}
}
3.1 引用静态方法
格式:类名::静态方法名
import java.util.Arrays;
public class Text {
public static void main(String[] args) {
String[] arr={"3","5","4","1","6","2"};
Arrays.stream(arr).forEach(Integer::parseInt);
}
}
3.2 引用成员方法
引用成员方法根据引用的类的类型不同分为以下三种:
- 其他类:
其他类对象名::方法名 - 本类:
this::方法名 - 父类:
super::方法名
需要注意的是这三种引用不能引用静态方法
3.3 引用构造方法
格式:类名::new
引用构造方法的时候,要保证构造方法的对象和抽象方法的返回值保持一致。
举例:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class Text {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
Collections.addAll(list,"张无忌,15","周正若,14","赵敏,13","张强,100","张三丰,24");
List<Student> lists = list.stream().map(new Function<String, Student>() {
@Override
public Student apply(String s) {//这里的形参为String s,但Student中没有这个形参的返回值,故需要手动加一个
String[] arr = s.split(",");
String name = arr[0];
int age = Integer.parseInt(arr[1]);
return new Student(age, name);
}
}).collect(Collectors.toList());
List<Student> lists = list.stream()
.map(Student::new)//简写好的,就是使用自己写的构造方法创建Student对象并返回给map
.collect(Collectors.toList());
System.out.println(lists);
}
}
//Student类,没写get,set方法,重点看看构造方法!
public class Student {
private int age;
private String name;
public Student() {
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
public Student(String s){//这个部分就是我们调用的
String[] arr = s.split(",");
this.name = arr[0];
this.age = Integer.parseInt(arr[1]);
}
}
3.4 其他
3.4.1 使用类名引用成员方法
格式:类名::成员方法
import java.util.ArrayList;
import java.util.Collections;
public class Text {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
Collections.addAll(list,"aaa","bbb","ccc","ddd");
//详细写
list.stream().map(new Function<String, String>() {
@Override
public String apply(String s) {//这里的第一个参数决定了在使用类名引用方法中只能使用哪个类的方法。
//这里是String类,故只能使用String类的方法。
return s.toUpperCase();
}
}).forEach(s-> System.out.println(s));
//简写
list.stream()
.map(String::toUpperCase)
.forEach(s-> System.out.println(s));
}
}
使用类名引用成员方法和使用对象名字引用成员方法要遵循的规则不一样:
使用类名引用成员方法的规则:
- 需要有函数式接口
- 被引用的方法必须已经存在
- 被引用方法的形参,需要根抽象方法的第二个形参到最后一个形参保持一致,返回值也要保持一致
- 被引用方法的功能需要满足当前需求。
抽象方法形参详解:
第一个参数:表示被引用方法的调用者,决定了可以引用哪些类中的方法。
在 Stream 流中,第一个参数一般都表示流中每一个数据,假设流中数据是字符串,那么使用这种方式引用,只能引用 String 类中方法。
第二个参数到最后一个参数:跟被引用方法的形参保持一致。
如果没有第二个参数,说明被引用的方法需要是无参成员方法。
局限性:
- 不能引用所有类中的成员方法
- 是跟抽象方法的第一个参数有关,这个参数是什么类型,那么就只能使用这个类的方法。
3.4.2 引用数组的构造方法
格式:数据类型[]::new
举例:
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.IntFunction;
public class Text {
public static void main(String[] args) {
ArrayList<Integer> list=new ArrayList<>();
Collections.addAll(list,1,2,3,4,5,6,7);
//原本的写法
Integer[] i=list.stream().toArray(new IntFunction<Integer[]>() {
@Override
public Integer[] apply(int value) {
return new Integer[value];
}
});
//新写法
Integer[] i1=list.stream().toArray(Integer[]::new);
//数组的类型需要跟流中数据的类型保持一致
}
}