一、 Java 8 的 Stream API
Stream API 是 Java 8 引入的用于处理集合数据(如 List、Set)的新方式。
它提供了一个高效、易读、链式的方式来操作数据源(集合、数组等)。
Stream = 数据流水线
数据经过一系列处理,最后汇聚成结果,中间不会修改原集合,且支持并行处理,性能高!
二、Stream 处理的四个步骤
- 获取 Stream:从集合、数组等数据源生成 Stream。
- 中间操作:对数据进行一系列处理(过滤、映射、排序等),返回新的 Stream。
- 终止操作:真正触发计算,得到结果(forEach、collect、count等)。
- 特点:惰性执行(中间操作不会立即执行,遇到终止操作时才执行)。
三、具体使用
1:创建 Stream
import java.util.*;
import java.util.stream.*;
public class StreamCreateDemo {
public static void main(String[] args) {
// 1. 从集合创建
List<String> list = Arrays.asList("Apple", "Banana", "Cherry");
Stream<String> stream1 = list.stream();
// 2. 从数组创建
String[] arr = {"Java", "Python", "C++"};
Stream<String> stream2 = Arrays.stream(arr);
// 3. 使用Stream.of()
Stream<Integer> stream3 = Stream.of(1, 2, 3, 4, 5);
// 4. 无限流(只演示,不常用)
Stream<Double> stream4 = Stream.generate(Math::random).limit(5);
stream4.forEach(System.out::println); // 打印5个随机数
}
}
2:常见中间操作(filter、map、sorted)
import java.util.*;
import java.util.stream.*;
public class StreamMiddleOperationDemo {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana", "Avocado", "Cherry", "Blueberry");
// 示例:过滤以"A"开头的水果,转为大写并排序
fruits.stream()
.filter(fruit -> fruit.startsWith("A")) // 过滤出A开头
.map(String::toUpperCase) // 转为大写
.sorted() // 按字母排序
.forEach(System.out::println); // 遍历打印
}
}
输出:
APPLE
AVOCADO
3:终止操作(collect、count、anyMatch)
import java.util.*;
import java.util.stream.*;
public class StreamTerminalOperationDemo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Dog", "Cat", "Elephant", "Tiger");
// 1. collect:收集结果为 List
List<String> filteredList = list.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
System.out.println(filteredList); // [Elephant, Tiger]
// 2. count:统计数量
long count = list.stream()
.filter(name -> name.contains("i"))
.count();
System.out.println("包含'i'的数量: " + count); // 1
// 3. anyMatch:是否存在满足条件的元素
boolean hasElephant = list.stream()
.anyMatch(name -> name.equalsIgnoreCase("elephant"));
System.out.println("有elephant吗?" + hasElephant); // true
}
}
4:复杂流处理
import java.util.*;
import java.util.stream.*;
public class StreamComplexExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 需求:找到所有偶数,平方后大于10的,收集成列表
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0) // 过滤偶数
.map(n -> n * n) // 平方
.filter(n -> n > 10) // 筛选平方后大于10
.collect(Collectors.toList()); // 收集成List
System.out.println(result); // [16, 36, 64, 100]
}
}
四、常用的 Stream 操作汇总表
分类 | 方法 | 说明 |
---|---|---|
获取流 | stream() , of() , generate() | 创建Stream |
中间操作 | filter() | 过滤元素 |
中间操作 | map() | 元素转换 |
中间操作 | sorted() | 排序 |
中间操作 | distinct() | 去重 |
中间操作 | limit() , skip() | 截取或跳过元素 |
终止操作 | forEach() | 遍历元素 |
终止操作 | collect() | 收集到集合 |
终止操作 | count() | 统计数量 |
终止操作 | anyMatch() , allMatch() , noneMatch() | 匹配判断 |
终止操作 | findFirst() , findAny() | 查找元素 |
五、Stream 流程图(简版)
数据源 → 中间操作1 → 中间操作2 → ... → 终止操作(结果)
List → filter → map → collect
六、Stream 高阶操作
1. groupingBy()
— 分组操作
把元素按照某个规则进行分类成 Map,key 是分组依据,value 是元素集合。
- 按字符串长度分组
import java.util.*;
import java.util.stream.*;
public class StreamGroupingByDemo {
public static void main(String[] args) {
List<String> words = Arrays.asList("Apple", "Banana", "Cat", "Dog", "Elephant");
// 按字符串长度分组
Map<Integer, List<String>> grouped = words.stream()
.collect(Collectors.groupingBy(String::length));
// 打印分组结果
grouped.forEach((length, wordList) ->
System.out.println(length + "个字符: " + wordList)
);
}
}
输出:
5个字符: [Apple]
6个字符: [Banana]
3个字符: [Cat, Dog]
8个字符: [Elephant]
2. partitioningBy()
— 分区操作(true/false两组)
按照某个布尔条件,把集合一分为二(符合/不符合)。
- 分成偶数和奇数
import java.util.*;
import java.util.stream.*;
public class StreamPartitioningByDemo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// 分区:是否为偶数
Map<Boolean, List<Integer>> partitioned = numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
System.out.println("偶数: " + partitioned.get(true));
System.out.println("奇数: " + partitioned.get(false));
}
}
输出:
偶数: [2, 4, 6]
奇数: [1, 3, 5]
3. reduce()
— 归约操作(聚合)
把流中的元素反复结合起来,最终得到一个值,类似数学中的累加/累乘。
- 求所有数字的和
import java.util.*;
import java.util.stream.*;
public class StreamReduceDemo {
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
// reduce 第一个参数是初始值0,第二个参数是累加逻辑
int sum = nums.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("总和: " + sum); // 15
}
}
reduce
的核心就是不断地把两个元素合成一个,直到只剩一个!
4. flatMap()
— 拍平多个集合
把每个元素映射成流,然后展开成一个统一流。
flatMap = 映射 + 扁平化处理
- 把多个List合并成一个大List
import java.util.*;
import java.util.stream.*;
public class StreamFlatMapDemo {
public static void main(String[] args) {
List<List<String>> nestedList = Arrays.asList(
Arrays.asList("A", "B"),
Arrays.asList("C", "D"),
Arrays.asList("E")
);
// 使用flatMap展开
List<String> flatList = nestedList.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(flatList); // [A, B, C, D, E]
}
}
5、Stream 高阶用法对照总结表
方法 | 作用 | 示例 |
---|---|---|
groupingBy() | 按规则分组 | 按长度分组字符串 |
partitioningBy() | 按true/false分区 | 分偶数奇数 |
reduce() | 元素归约 | 求和、求最大值 |
flatMap() | 扁平化流 | 合并嵌套集合 |
6、综合流操作
来一个更实际、综合的例子(开发中常遇到):
给定一组员工数据,筛选出工资大于6000的,按部门分组,并列出每个部门的员工姓名列表。
import java.util.*;
import java.util.stream.*;
class Employee {
private String name;
private String department;
private double salary;
// 构造器 & getter方法
public Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
}
public class StreamEmployeeExample {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Alice", "IT", 7000),
new Employee("Bob", "HR", 5000),
new Employee("Charlie", "IT", 8000),
new Employee("David", "Finance", 9000),
new Employee("Eve", "HR", 6500)
);
// 筛选工资>6000, 按部门分组,列出名字
Map<String, List<String>> result = employees.stream()
.filter(emp -> emp.getSalary() > 6000) // 筛选工资高的
.collect(Collectors.groupingBy(
Employee::getDepartment, // 按部门分组
Collectors.mapping(Employee::getName, Collectors.toList()) // 只保留名字
));
// 打印结果
result.forEach((dept, names) ->
System.out.println(dept + "部门: " + names)
);
}
}
输出:
IT部门: [Alice, Charlie]
Finance部门: [David]
HR部门: [Eve]
7、理解
关键词 | 一句话解释 |
---|---|
filter | 过滤留下 |
map | 映射改变 |
flatMap | 打碎扁平 |
reduce | 累加归一 |
collect | 收集保存 |
groupingBy | 分类分组 |
partitioningBy | 分成两堆 |
8、大数据列表的分页处理(模拟分页查询)
- 有一个超大List(比如10万条数据)
- 前端需要分页展示(比如每页20条)
- 需要用Stream优雅、高效地分页处理
解决思路
使用skip(offset).limit(size)
实现分页。
skip(n)
:跳过前n个元素
limit(m)
:取接下来的m个元素
import java.util.*;
import java.util.stream.*;
public class StreamPagingExample {
public static void main(String[] args) {
// 模拟一个超大List(10万条)
List<Integer> bigList = IntStream.rangeClosed(1, 100_000)
.boxed()
.collect(Collectors.toList());
int pageSize = 20; // 每页大小
int pageNumber = 3; // 第3页(注意:第一页是pageNumber=1)
List<Integer> pageData = bigList.stream()
.skip((pageNumber - 1) * pageSize) // 跳过前2页
.limit(pageSize) // 取第3页数据
.collect(Collectors.toList());
System.out.println("第 " + pageNumber + " 页数据:" + pageData);
}
}
第 3 页数据:[41, 42, 43, ..., 60]
性能提示
- 如果集合特别大,考虑用并行流(
parallelStream()
)提升速度 - 小集合(几千条以内)一般用普通
stream()
就够了,避免并行开销 - 分页参数(skip、limit)写在链式操作越靠前越好,减少后续流的元素数量
9、复杂对象的多级分组与统计
- 有一批员工数据
- 要求:
- 按部门分组
- 每个部门再按职位分组
- 每组统计人数
import java.util.*;
import java.util.stream.*;
class Employee {
private String name;
private String department;
private String position;
public Employee(String name, String department, String position) {
this.name = name;
this.department = department;
this.position = position;
}
public String getDepartment() { return department; }
public String getPosition() { return position; }
}
public class StreamMultiLevelGrouping {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Alice", "IT", "Developer"),
new Employee("Bob", "IT", "Developer"),
new Employee("Charlie", "IT", "Manager"),
new Employee("David", "HR", "Manager"),
new Employee("Eve", "HR", "Recruiter"),
new Employee("Frank", "Finance", "Analyst")
);
// 多级分组+人数统计
Map<String, Map<String, Long>> grouped = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.counting()
)
));
// 打印结果
grouped.forEach((dept, posMap) -> {
System.out.println("部门:" + dept);
posMap.forEach((pos, count) ->
System.out.println(" 职位:" + pos + ",人数:" + count)
);
});
}
}
部门:IT
职位:Developer,人数:2
职位:Manager,人数:1
部门:HR
职位:Manager,人数:1
职位:Recruiter,人数:1
部门:Finance
职位:Analyst,人数:1
10、List去重(基于对象属性)
- 有个对象集合(List<User>)
- 需要根据某个字段(比如email)去重!
import java.util.*;
import java.util.function.Function;
import java.util.stream.*;
class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getEmail() { return email; }
@Override
public String toString() { return name + " <" + email + ">"; }
}
public class StreamDistinctByKey {
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("Alice", "alice@example.com"),
new User("Bob", "bob@example.com"),
new User("Alice Duplicate", "alice@example.com") // email重复
);
List<User> distinctUsers = users.stream()
.filter(distinctByKey(User::getEmail)) // 根据email去重
.collect(Collectors.toList());
distinctUsers.forEach(System.out::println);
}
// 核心辅助方法:根据Key去重
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
}
Alice <alice@example.com>
Bob <bob@example.com>
性能提示
ConcurrentHashMap.newKeySet()
保证线程安全,即使在parallelStream()
中也能用!- 如果数据量小,可以直接用普通
HashSet
。
11、Stream 进阶总结
技巧 | 核心方法 | 场景示例 |
---|---|---|
分页处理 | skip() + limit() | 大List分页 |
多级分组 | groupingBy() -> groupingBy() | 部门+职位统计 |
对象属性去重 | filter(distinctByKey()) | 按email去重 |
动态条件过滤 | filter(predicate组合) | 灵活复杂查询 |
流性能提升 | parallelStream() | 大数据量并行处理 |
六、Stream 性能优化实战
1、 及时终止中间操作(short-circuiting)
- 流式处理默认是惰性求值(只有终止操作才真正执行)
- 短路操作(比如
limit()
,findFirst()
,anyMatch()
)可以让流尽早结束
// 查找第一个符合条件的人
Optional<String> firstMatch = Stream.of("Tom", "Jerry", "Spike")
.filter(name -> name.startsWith("J"))
.findFirst(); // findFirst短路:找到第一个就停止后续filter
小流量数据,尽量使用 findFirst()
, anyMatch()
,可以避免不必要的计算。
2、 使用 parallelStream()
parallelStream()
利用多核CPU并行处理- 但线程切换开销和数据分片成本可能适得其反!
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000).boxed().collect(Collectors.toList());
long start = System.currentTimeMillis();
numbers.parallelStream() // 并行流
.reduce(0, Integer::sum);
long end = System.currentTimeMillis();
System.out.println("并行计算耗时:" + (end - start) + "ms");
大数据量(10万以上)、CPU密集型任务,适合并行流
小数据量或IO密集型操作,不建议用parallelStream()
!
3、 避免流中嵌套流(Stream里面forEach)
- 流里再套流,容易失去流的优势,还增加时间复杂度!
list.stream().forEach(a -> anotherList.stream().forEach(b -> doSomething(a, b)));
正确写法:考虑flatMap()
平铺一层!
4、 优先使用原始类型流 IntStream / LongStream
Stream<Integer>
会频繁装箱/拆箱(Integer ↔ int)IntStream
直接操作基本类型,更快!
// 不推荐
Stream<Integer> integerStream = Stream.of(1, 2, 3);
// 推荐
IntStream intStream = IntStream.of(1, 2, 3);
做大量数学计算时,IntStream
/LongStream
效果明显!
5 减少中间集合,直接链式操作
- 每次
.collect()
会生成中间集合,增加内存压力 - 可以流式链式操作到底,最后一次性 collect
推荐链式写法
List<String> result = people.stream()
.filter(p -> p.getAge() > 18)
.map(Person::getName)
.sorted()
.collect(Collectors.toList());
八、面试 Stream 高阶问题总结
1:Stream 和普通 for 循环有什么区别?
- Stream关注声明式处理(what)而非命令式处理(how)
- 支持链式调用、懒惰求值、并行加速
- 更易读、更适合大数据量处理,但在小规模场景下性能差异不大
2:parallelStream 真的是越快越好吗?
- 不一定!小数据量时,由于线程调度开销,parallelStream反而慢
- CPU密集型、大数据量任务适合;IO密集型、不规则任务慎用
3:Stream操作是线程安全的吗?
- 流本身是不安全的
parallelStream()
更要注意共享变量的竞争问题- 如果要保证安全,需要用同步集合或使用无状态操作
4:什么是短路操作?有哪些常见短路方法?
- 短路操作是指在满足一定条件时提前终止流处理,提高效率
- 常见短路方法有:
findFirst()
findAny()
anyMatch()
allMatch()
noneMatch()
limit(n)
5:Stream 能重复使用吗?
- 不能!
- 流一旦消费或关闭(比如执行终止操作
collect()
),就不能再操作了 - 需要重新生成新的流!
6:进阶总结版 Stream
分类 | 技能点 | 常用方法 |
---|---|---|
创建流 | list、数组、生成器 | stream() , of() , iterate() , generate() |
中间操作 | 过滤、映射、排序 | filter() , map() , flatMap() , sorted() , distinct() |
终止操作 | 收集、聚合、遍历 | collect() , reduce() , forEach() , count() |
并行流 | 多核加速处理 | parallelStream() |
分组/分区 | 高级收集器 | groupingBy() , partitioningBy() |
性能优化 | 懒执行、短路、基本类型流 | skip() + limit() , IntStream |
九、Stream 常见错误大全 + 正确写法总结
1:流被消费后再次使用
- 错误:
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println);
stream.forEach(System.out::println); // 报错:stream has already been operated upon or closed
- 正确:
Stream<String> stream1 = Stream.of("a", "b", "c");
stream1.forEach(System.out::println);
Stream<String> stream2 = Stream.of("a", "b", "c");
stream2.forEach(System.out::println); // 新建新的流
流只能使用一次,要多次使用,重新生成!
2:在并行流中操作非线程安全集合
- 错误:
List<Integer> list = new ArrayList<>();
IntStream.range(0, 1000).parallel().forEach(list::add); // 线程不安全,数据丢失
- 正确:
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
IntStream.range(0, 1000).parallel().forEach(list::add);
并行流操作共享资源,要用线程安全集合!
3:流中forEach里嵌套Stream操作
- 错误:
list.stream().forEach(a -> anotherList.stream().forEach(b -> doSomething(a, b)));
- 正确:
list.stream()
.flatMap(a -> anotherList.stream().map(b -> combine(a, b)))
.forEach(System.out::println);
避免 forEach 套 Stream,应该用 flatMap
平铺处理!
4:滥用 parallelStream 导致性能更差
- 错误:
List<String> smallList = Arrays.asList("A", "B", "C");
smallList.parallelStream().forEach(System.out::println); // 小数据量,不适合并行!
小数据量不要用 parallelStream()
!
十、Stream 模板
标准流式处理骨架(可直接套用)
List<User> users = fetchUsersFromDB();
// 流式处理模板
List<String> adultUserNames = Optional.ofNullable(users)
.orElse(Collections.emptyList())
.stream()
.filter(user -> Objects.nonNull(user.getAge()) && user.getAge() >= 18) // 过滤合法且年满18岁
.sorted(Comparator.comparing(User::getName)) // 按名字排序
.map(User::getName) // 提取名字
.distinct() // 去重
.collect(Collectors.toList()); // 收集到List
- 流程分解:
步骤 | 说明 |
---|---|
1. 安全处理空集合 | Optional.ofNullable().orElse(Collections.emptyList()) |
2. 过滤数据 | filter() |
3. 排序 | sorted() |
4. 转换映射 | map() |
5. 去重 | distinct() |
6. 收集 | collect() |
- 建议
项目实践 | 推荐做法 |
---|---|
空集合保护 | Optional.ofNullable(list).orElse(Collections.emptyList()) |
Null保护 | 在filter() 中做Objects.nonNull() 校验 |
小批量处理 | 不要用parallelStream() |
复杂映射 | 用flatMap() 展开 |
日志排查 | 尽量在流前后打印日志,方便排查异常 |
- 总结
场景 | 示例 |
---|---|
过滤 | filter(x -> x > 0) |
排序 | sorted(Comparator.comparing(x -> x.getAge())) |
映射 | map(x -> x.getName()) |
平铺 | flatMap(list -> list.stream()) |
终止 | collect(Collectors.toList()) |
分组 | Collectors.groupingBy(x -> x.getType()) |
聚合 | reduce(0, Integer::sum) |