目录
引言:为什么需要Stream流
想象一下,你需要从一堆水果中挑选出所有红色的、重量超过200克的苹果,并按重量排序取前三个。传统方式需要你写多个循环和条件判断,而使用Stream流,你可以这样做:
List<Apple> result = apples.stream()
.filter(a -> a.getColor().equals("红色"))
.filter(a -> a.getWeight() > 200)
.sorted((a1, a2) -> a1.getWeight() - a2.getWeight())
.limit(3)
.collect(Collectors.toList());
Stream流就像一条传送带,数据放上去后可以经过多个处理站,最终得到你想要的结果,让代码更简洁、可读性更强。
一、Stream流基础
1.1 什么是Stream流
Stream流是Java 8引入的处理集合数据的API,它让我们以声明式(描述要做什么,而不是怎么做)的方式处理数据。
核心特点:
- 声明式编程:更关注做什么,而不是怎么做
- 支持链式操作:可以像搭积木一样组合多个操作
- 支持并行处理:轻松切换到并行模式,提高性能
- 延迟执行:在最后一步(终端操作)才会真正执行
1.2 Stream流与集合的区别
简单对比:
- 集合就像一个仓库,专注于存储和访问数据
- Stream就像一条流水线,专注于对数据进行操作
特性 | 集合 Collection | Stream流 |
---|---|---|
存储方式 | 存储元素 | 不存储元素 |
操作时机 | 立即执行 | 延迟执行(惰性) |
遍历次数 | 可多次遍历 | 只能遍历一次 |
关注点 | 数据 | 计算 |
1.3 Stream流的使用步骤
Stream流的使用分为三个步骤:
- 创建Stream流:获取一个数据流
- 使用中间方法:对流中的数据进行操作
- 使用终结方法:对流水线上的数据进行最终操作
// 一个完整的Stream操作示例
List<String> names = Arrays.asList("小明", "小红", "小刚", "小华");
List<String> filteredNames = names.stream() // 1.创建Stream流
.filter(name -> name.length() > 1) // 2.中间操作
.collect(Collectors.toList()); // 3.终端操作
二、创建Stream流
2.1 从不同数据源创建Stream
我们可以从多种数据源创建Stream:
获取方式 | 方法名 | 说明 |
---|---|---|
单列集合 | default Stream<E> stream() | Collection中的默认方法 |
双列集合 | 无 | 无法直接使用stream流 |
数组 | public static <T> Stream<T> stream(T[] array) | Arrays工具类中的静态方法 |
一堆零散数据 | public static<T> Stream<T> of(T... values) | Stream接口中的静态方法 |
2.1.1 从单列集合创建Stream(最常用)
// 1.单列集合获取Stream流
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d", "e");
// 获取一条流水线,并把集合中的数据放到流水线上
Stream<String> stream1 = list.stream();
// 使用终结方法打印一下流水线上的所有数据
stream1.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
// s:依次表示流水线上的每一个数据
System.out.println(s);
}
});
// 使用Lambda简化
list.stream().forEach(s -> System.out.println(s));
2.1.2 从双列集合创建Stream
双列集合没有直接获取Stream流的方法,但可以间接获取:
// 1.创建双列集合
HashMap<String, Integer> hm = new HashMap<>();
// 2.添加数据
hm.put("红楼梦", 111);
hm.put("西游记", 222);
hm.put("水浒传", 333);
hm.put("三国演义", 444);
// 3.第一种获取stream流的方式:通过键
hm.keySet().stream().forEach(s -> System.out.println(s));
// 4.第二种获取stream流的方式:通过值
hm.values().stream().forEach(i -> System.out.println(i));
// 5.第三种获取stream流的方式:通过键值对
hm.entrySet().stream().forEach(entry -> System.out.println(entry.getKey() + "=" + entry.getValue()));
2.1.3 从数组创建Stream
// 1.创建数组
int[] arr1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
String[] arr2 = {"a", "b", "c"};
// 2.获取stream流
Arrays.stream(arr1).forEach(s -> System.out.println(s));
System.out.println("========================");
Arrays.stream(arr2).forEach(s -> System.out.println(s));
2.1.4 从零散数据创建Stream
// 一堆零散数据 public static<T> Stream<T> of(T... values) Stream接口中的静态方法
Stream.of(1, 2, 3, 4, 5).forEach(s -> System.out.println(s));
Stream.of("a", "b", "c", "d", "e").forEach(s -> System.out.println(s));
三、Stream流的中间操作
中间操作会返回一个新的Stream,可以链式调用。中间操作不会立即执行,而是等到终端操作时才一起执行。
Stream的中间方法包括:
名称 | 说明 |
---|---|
filter(Predicate<?> predicate) | 过滤 |
limit(long maxSize) | 获取前几个元素 |
skip(long n) | 跳过前几个元素 |
distinct() | 元素去重(依赖hashCode和equals方法) |
concat(Stream a, Stream b) | 合并a和b两个流为一个流 |
map(Function<T, R> mapper) | 转换流中的数据类型 |
3.1 筛选和切片操作
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 5);
// filter: 过滤元素
List<Integer> greaterThan3 = numbers.stream()
.filter(n -> n > 3)
.collect(Collectors.toList());
System.out.println("大于3的数: " + greaterThan3); // [4, 5]
// distinct: 去除重复
List<Integer> uniqueNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println("去重后: " + uniqueNumbers); // [1, 2, 3, 4, 5]
// limit: 限制数量
List<Integer> firstThree = numbers.stream()
.limit(3)
.collect(Collectors.toList());
System.out.println("前三个数: " + firstThree); // [1, 2, 2]
// skip: 跳过元素
List<Integer> skipFirst2 = numbers.stream()
.skip(2)
.collect(Collectors.toList());
System.out.println("跳过前两个: " + skipFirst2); // [2, 3, 4, 5]
// 组合使用
List<Integer> result = numbers.stream()
.distinct() // 先去重
.skip(1) // 跳过第一个
.limit(3) // 取3个
.collect(Collectors.toList());
System.out.println("去重后跳过第一个,再取三个: " + result); // [2, 3, 4]
3.2 映射操作
List<String> words = Arrays.asList("Java", "Stream", "API");
// map: 转换元素
List<Integer> wordLengths = words.stream()
.map(word -> word.length()) // 获取每个单词的长度
.collect(Collectors.toList());
System.out.println("单词长度: " + wordLengths); // [4, 6, 3]
// map的另一个示例:转换大写
List<String> upperCaseWords = words.stream()
.map(word -> word.toUpperCase())
.collect(Collectors.toList());
System.out.println("转换为大写: " + upperCaseWords); // [JAVA, STREAM, API]
// flatMap: 扁平化处理(将多个流合并为一个流)
List<List<Integer>> nestedList = Arrays.asList(
Arrays.asList(1, 2),
Arrays.asList(3, 4, 5)
);
List<Integer> flatList = nestedList.stream()
.flatMap(list -> list.stream()) // 将每个内部List转为Stream然后合并
.collect(Collectors.toList());
System.out.println("扁平化后: " + flatList); // [1, 2, 3, 4, 5]
// flatMap实际应用:提取所有单词的字符
List<String> words2 = Arrays.asList("Hello", "World");
List<String> letters = words2.stream()
.flatMap(word -> Arrays.stream(word.split("")))
.collect(Collectors.toList());
System.out.println("所有字符: " + letters);
// [H, e, l, l, o, W, o, r, l, d]
3.3 排序操作
List<String> names = Arrays.asList("小明", "小红", "小刚", "小华");
// 自然排序
List<String> sortedNames = names.stream()
.sorted()
.collect(Collectors.toList());
System.out.println("自然排序: " + sortedNames);
// 自定义排序(按字符串长度)
List<String> sortedByLength = names.stream()
.sorted((s1, s2) -> s1.length() - s2.length())
.collect(Collectors.toList());
System.out.println("按长度排序: " + sortedByLength);
// 逆序排序
List<Integer> numbers = Arrays.asList(5, 3, 8, 1, 4);
List<Integer> reverseSorted = numbers.stream()
.sorted((n1, n2) -> n2 - n1)
.collect(Collectors.toList());
System.out.println("逆序排序: " + reverseSorted); // [8, 5, 4, 3, 1]
// 多条件排序
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() { return name + ":" + age; }
}
List<Person> people = Arrays.asList(
new Person("小明", 20),
new Person("小红", 25),
new Person("小刚", 20)
);
// 先按年龄排序,年龄相同按姓名排序
List<Person> sortedPeople = people.stream()
.sorted((p1, p2) -> {
// 先比较年龄
int result = p1.getAge() - p2.getAge();
// 如果年龄相同,再比较姓名
if (result == 0) {
return p1.getName().compareTo(p2.getName());
}
return result;
})
.collect(Collectors.toList());
System.out.println("多条件排序: " + sortedPeople);
// [小刚:20, 小明:20, 小红:25]
3.4 查看操作(peek)
// peek: 用于调试,查看中间结果
List<String> result = Stream.of("one", "two", "three")
.peek(e -> System.out.println("原始元素: " + e))
.map(word -> word.toUpperCase())
.peek(e -> System.out.println("转换后: " + e))
.collect(Collectors.toList());
System.out.println("最终结果: " + result);
3.5 注意事项
使用Stream中间操作的注意事项:
- 中间方法返回新的Stream流,原来的Stream流只能使用一次,建议使用链式编程
- 修改Stream流中的数据,不会影响原来集合或者数组中的数据
四、Stream流的终结方法
终结方法会触发流的计算,并产生结果。流在终结操作后不能再被使用。
Stream的终结方法包括:
名称 | 说明 |
---|---|
void forEach(Consumer action) | 遍历 |
long count() | 统计 |
toArray() | 收集流中的数据,放到数组中 |
collect(Collector collector) | 收集流中的数据,放到集合中 |
findFirst()/findAny() | 查找第一个/任意一个元素 |
anyMatch()/allMatch()/noneMatch() | 匹配元素 |
max()/min() | 获取最大/最小值 |
reduce() | 归约操作 |
4.1 遍历操作
List<String> names = Arrays.asList("小明", "小红", "小刚");
// forEach: 遍历元素
names.stream().forEach(name -> System.out.println("姓名: " + name));
// 也可以使用增强for循环,效果相同
for (String name : names) {
System.out.println("姓名: " + name);
}
4.2 计数操作
List<String> names = Arrays.asList("小明", "小红", "小刚");
// count: 计数
long count = names.stream().count();
System.out.println("总人数: " + count); // 3
4.3 收集操作
List<String> names = Arrays.asList("小明", "小红", "小刚");
// toArray: 收集到数组
String[] nameArray = names.stream().toArray(size -> new String[size]);
System.out.println("数组长度: " + nameArray.length); // 3
// collect: 收集到集合(List)
// collect(Collector collector) 收集流中的数据,放到集合中 (List Set Map)
List<String> nameList = names.stream().collect(Collectors.toList());
System.out.println("列表: " + nameList); // [小明, 小红, 小刚]
// collect: 收集到集合(Set)
Set<String> nameSet = names.stream().collect(Collectors.toSet());
System.out.println("集合: " + nameSet); // [小明, 小红, 小刚]
4.4 匹配操作
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// anyMatch: 是否有任何元素匹配(有一个匹配就返回true)
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);
System.out.println("有偶数? " + hasEven); // true
// allMatch: 是否所有元素都匹配
boolean allPositive = numbers.stream().allMatch(n -> n > 0);
System.out.println("全是正数? " + allPositive); // true
// noneMatch: 是否没有元素匹配
boolean noNegative = numbers.stream().noneMatch(n -> n < 0);
System.out.println("没有负数? " + noNegative); // true
// 实际应用:检查集合中是否存在特定条件的元素
List<Person> persons = Arrays.asList(
new Person("小明", 17),
new Person("小红", 23),
new Person("小刚", 28)
);
boolean hasMinor = persons.stream().anyMatch(p -> p.getAge() < 18);
System.out.println("有未成年人? " + hasMinor); // true
boolean allAdult = persons.stream().allMatch(p -> p.getAge() >= 18);
System.out.println("全是成年人? " + allAdult); // false
4.5 查找操作
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// findFirst: 找到第一个元素
Optional<Integer> first = numbers.stream().findFirst();
System.out.println("第一个元素: " + first.orElse(0)); // 1
// findAny: 找到任意一个元素(并行流中很有用)
Optional<Integer> any = numbers.stream().findAny();
System.out.println("任意元素: " + any.orElse(0)); // 通常是1
// 结合filter使用
Optional<Integer> firstEven = numbers.stream()
.filter(n -> n % 2 == 0)
.findFirst();
System.out.println("第一个偶数: " + firstEven.orElse(0)); // 2
4.6 归约操作(reduce)
归约操作将流中的元素组合起来,得到一个值。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 求和
Optional<Integer> sum = numbers.stream()
.reduce((a, b) -> a + b);
System.out.println("总和: " + sum.orElse(0)); // 15
// 指定初始值的reduce
int sumWithInitial = numbers.stream()
.reduce(10, (a, b) -> a + b);
System.out.println("带初始值的总和: " + sumWithInitial); // 25
// 计算最大值
Optional<Integer> max = numbers.stream()
.reduce((a, b) -> a > b ? a : b);
System.out.println("最大值: " + max.orElse(0)); // 5
// 计算字符串连接
String concatStr = Stream.of("A", "B", "C")
.reduce("", (a, b) -> a + b);
System.out.println("连接结果: " + concatStr); // ABC
// 计算乘积
Optional<Integer> product = numbers.stream()
.reduce((a, b) -> a * b);
System.out.println("乘积: " + product.orElse(0)); // 120
4.7 最大值和最小值
List<Integer> numbers = Arrays.asList(5, 3, 9, 1, 7);
// 获取最大值
Optional<Integer> max = numbers.stream().max((a, b) -> a.compareTo(b));
System.out.println("最大值: " + max.orElse(0)); // 9
// 获取最小值
Optional<Integer> min = numbers.stream().min((a, b) -> a.compareTo(b));
System.out.println("最小值: " + min.orElse(0)); // 1
// 在实际对象中使用
List<String> people = Arrays.asList(
"小明-25",
"小红-30",
"小刚-22"
);
// 获取年龄最大的人
Optional<String> oldest = people.stream()
.max((p1, p2) -> {
int age1 = Integer.parseInt(p1.split("-")[1]);
int age2 = Integer.parseInt(p2.split("-")[1]);
return age1 - age2;
});
System.out.println("年龄最大的人: " + oldest.orElse("无")); // 小红-30
五、collect收集器详解
collect
是一个非常强大的终端操作,通过Collectors
类提供的工厂方法,我们可以将流转换为各种集合。
5.1 收集到List、Set、Map
// 假设有一个包含多个人名的列表
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
"刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");
// 收集到List
// 需求:收集所有男性的名字到List中
List<String> nameList = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toList());
// 收集到Set
// 需求:收集所有男性的名字到Set中
Set<String> nameSet = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toSet());
System.out.println(nameSet);
// 收集到Map
// 收集到Map,key为姓名,value为年龄
Map<String, Integer> nameToAge = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(
s -> s.split("-")[0], // Key映射函数:获取姓名
s -> Integer.parseInt(s.split("-")[2]) // Value映射函数:获取年龄
));
System.out.println(nameToAge);
// {刘德华=50, 杜子腾=15, 吴京=37, 风清扬=20, 黄渤=40, 成龙=61}
使用Collectors.toMap
时,需要提供两个Function
:
- 第一个
Function
用于指定如何从流中的元素获取Map的键(key) - 第二个
Function
用于指定如何从流中的元素获取Map的值(value)
5.2 收集并分组
// 使用之前的人员列表
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
"刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");
// 按性别分组
Map<String, List<String>> personsByGender = list.stream()
.collect(Collectors.groupingBy(s -> s.split("-")[1]));
System.out.println("男性列表: " + personsByGender.get("男").size()); // 6
System.out.println("女性列表: " + personsByGender.get("女").size()); // 3
// 按年龄段分组
Map<String, List<String>> personsByAgeGroup = list.stream()
.collect(Collectors.groupingBy(s -> {
int age = Integer.parseInt(s.split("-")[2]);
if (age < 18) return "未成年";
else if (age < 60) return "中年";
else return "老年";
}));
System.out.println("未成年人: " + personsByAgeGroup.get("未成年").size());
System.out.println("中年人: " + personsByAgeGroup.get("中年").size());
System.out.println("老年人: " + personsByAgeGroup.get("老年").size());
5.3 其他常用收集操作
// 使用之前的人员列表
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
"刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");
// 计算所有人的平均年龄
double avgAge = list.stream()
.mapToInt(s -> Integer.parseInt(s.split("-")[2]))
.average()
.orElse(0);
System.out.println("平均年龄: " + avgAge);
// 计算男性的平均年龄
double maleAvgAge = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.mapToInt(s -> Integer.parseInt(s.split("-")[2]))
.average()
.orElse(0);
System.out.println("男性平均年龄: " + maleAvgAge);
// 连接所有人的姓名
String nameString = list.stream()
.map(s -> s.split("-")[0])
.collect(Collectors.joining(", "));
System.out.println("所有姓名: " + nameString);
// 杜子腾, 柳岩, 范冰冰, 风清扬, 刘德华, 黄渤, 赵丽颖, 吴京, 成龙
六、Stream流的实际应用案例
6.1 案例一:过滤和转换
// 基于之前的人员列表
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
"刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");
// 需求:筛选出年龄大于35岁的男性,并提取他们的姓名
List<String> olderMaleNames = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.filter(s -> Integer.parseInt(s.split("-")[2]) > 35)
.map(s -> s.split("-")[0])
.collect(Collectors.toList());
System.out.println("35岁以上男性: " + olderMaleNames);
// [刘德华, 黄渤, 吴京, 成龙]
6.2 案例二:统计和聚合
// 基于之前的人员列表
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
"刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");
// 需求:计算男性和女性的平均年龄
Map<String, Double> averageAgeByGender = list.stream()
.collect(Collectors.groupingBy(
s -> s.split("-")[1],
Collectors.averagingInt(s -> Integer.parseInt(s.split("-")[2]))
));
System.out.println("男性平均年龄: " + averageAgeByGender.get("男"));
System.out.println("女性平均年龄: " + averageAgeByGender.get("女"));
// 需求:找出年龄最大和最小的人
String oldestPerson = list.stream()
.max((s1, s2) -> {
int age1 = Integer.parseInt(s1.split("-")[2]);
int age2 = Integer.parseInt(s2.split("-")[2]);
return age1 - age2;
})
.orElse("无");
String youngestPerson = list.stream()
.min((s1, s2) -> {
int age1 = Integer.parseInt(s1.split("-")[2]);
int age2 = Integer.parseInt(s2.split("-")[2]);
return age1 - age2;
})
.orElse("无");
System.out.println("年龄最大的人: " + oldestPerson.split("-")[0] +
", 年龄: " + oldestPerson.split("-")[2]);
System.out.println("年龄最小的人: " + youngestPerson.split("-")[0] +
", 年龄: " + youngestPerson.split("-")[2]);
6.3 案例三:排序和限制
// 基于之前的人员列表
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
"刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");
// 需求:按年龄排序,取出年龄最大的3个人
List<String> top3ByAge = list.stream()
.sorted((s1, s2) -> {
int age1 = Integer.parseInt(s1.split("-")[2]);
int age2 = Integer.parseInt(s2.split("-")[2]);
return age2 - age1; // 降序排列
})
.limit(3)
.collect(Collectors.toList());
System.out.println("年龄最大的三个人:");
top3ByAge.forEach(s -> {
String[] parts = s.split("-");
System.out.println(parts[0] + ", 年龄: " + parts[2]);
});
// 输出:
// 成龙, 年龄: 61
// 刘德华, 年龄: 50
// 黄渤, 年龄: 40
// 需求:按姓名长度排序
List<String> sortedByNameLength = list.stream()
.sorted((s1, s2) -> {
String name1 = s1.split("-")[0];
String name2 = s2.split("-")[0];
return name1.length() - name2.length(); // 按姓名长度升序排列
})
.collect(Collectors.toList());
System.out.println("按姓名长度排序:");
sortedByNameLength.forEach(s -> {
String[] parts = s.split("-");
System.out.println(parts[0] + ", 长度: " + parts[0].length());
});
七、并行流
并行流允许Stream并行执行操作,可以更好地利用多核处理器。
7.1 创建并行流
// 方式1:从集合直接创建并行流
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> parallelStream = numbers.parallelStream();
// 方式2:将顺序流转换为并行流
Stream<Integer> parallel = numbers.stream().parallel();
7.2 并行流示例
// 并行计算1到100万的和
long start = System.currentTimeMillis();
// 顺序流
long sum1 = IntStream.rangeClosed(1, 10_000_000)
.sum();
long end1 = System.currentTimeMillis();
System.out.println("顺序流耗时: " + (end1 - start) + "ms");
// 并行流
long start2 = System.currentTimeMillis();
long sum2 = IntStream.rangeClosed(1, 10_000_000)
.parallel()
.sum();
long end2 = System.currentTimeMillis();
System.out.println("并行流耗时: " + (end2 - start2) + "ms");
// 验证结果一致
System.out.println("结果一致: " + (sum1 == sum2));
7.3 并行流注意事项
- 并行不一定更快,小数据量可能更慢(有线程创建和管理开销)
- 确保操作是线程安全的(尤其是有状态操作)
- 使用测试来确定是否值得使用并行流
- 某些操作(如limit、findFirst)在并行流上可能比顺序流更慢,因为它们需要保持顺序
// 注意以下代码可能导致问题
List<Integer> result = new ArrayList<>();
// 错误:并行流中的非线程安全操作
numbers.parallelStream().forEach(n -> result.add(n)); // 可能出现并发问题
// 正确:使用线程安全的收集操作
List<Integer> safeResult = numbers.parallelStream()
.collect(Collectors.toList()); // 线程安全
八、常见问题与注意事项
8.1 Stream的一次性使用
Stream只能使用一次,否则会抛出异常:
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println);
// 下行代码会抛出异常
stream.forEach(System.out::println); // IllegalStateException: stream has already been operated upon or closed
正确的做法是重新获取流:
List<String> list = Arrays.asList("a", "b", "c");
// 第一次使用
list.stream().forEach(s -> System.out.println(s));
// 需要再次使用,重新获取流
list.stream().forEach(s -> System.out.println(s));
8.2 延迟执行特性
中间操作是延迟执行的,只有在终端操作调用时才会执行:
List<String> names = Arrays.asList("小明", "小红", "小刚");
// 创建流和定义中间操作
Stream<String> stream = names.stream()
.filter(name -> {
System.out.println("过滤: " + name);
return name.length() > 1;
});
System.out.println("流创建完毕,但未执行过滤操作");
// 此时没有任何输出,因为filter是中间操作,延迟执行
// 执行终端操作
List<String> result = stream.collect(Collectors.toList());
// 现在会输出
// 过滤: 小明
// 过滤: 小红
// 过滤: 小刚
8.3 避免副作用
Stream操作应避免修改外部状态(副作用),这会导致不可预测的行为,特别是在并行流中。
// 错误示例:有副作用
List<String> result = new ArrayList<>();
Stream.of("a", "b", "c").forEach(s -> result.add(s)); // 不推荐,有副作用
// 正确示例:无副作用
List<String> betterResult = Stream.of("a", "b", "c").collect(Collectors.toList());
// 错误示例:修改外部变量
int[] sum = {0};
Stream.of(1, 2, 3).forEach(i -> sum[0] += i); // 不推荐,特别是在并行流中
// 正确示例:使用reduce
int betterSum = Stream.of(1, 2, 3).reduce(0, (a, b) -> a + b);
8.4 调试Stream流
调试Stream流可能很困难,使用peek操作可以帮助观察中间结果。
List<Integer> result = Stream.of(1, 2, 3, 4, 5)
.peek(n -> System.out.println("原始值: " + n))
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("过滤后: " + n))
.map(n -> n * 2)
.peek(n -> System.out.println("映射后: " + n))
.collect(Collectors.toList());
System.out.println("最终结果: " + result);
8.5 性能考虑
Stream操作的性能与数据量、操作复杂性有关:
- 短路操作:尽可能使用短路操作(如findFirst, anyMatch)来提升性能
- 并行流:只在数据量大且操作独立时使用并行流
- 避免装箱拆箱:使用IntStream, LongStream等特化流避免装箱/拆箱操作
- 惰性操作:利用流的延迟执行特性进行优化
// 使用特化流避免装箱/拆箱
int sum = IntStream.range(1, 1000).sum(); // 比 boxed stream 更高效
// 利用短路操作
Optional<Integer> first = Stream.of(1, 2, 3, 4, 5)
.filter(n -> {
System.out.println("过滤: " + n);
return n > 3;
})
.findFirst(); // 短路,不会处理所有元素
九、面试常见问题
9.1 Stream的特点是什么?
- Stream不存储数据,而是按需计算
- Stream操作不会修改源数据
- Stream有延迟执行特性(惰性求值)
- Stream可以是无限的
- Stream只能遍历一次
- Stream可以并行处理
9.2 Stream流、Collection和数组的区别?
- Collection是一种数据结构,用于存储数据
- 数组是固定大小的数据结构
- Stream是用于处理数据的API,不存储数据
9.3 介绍一下Stream流的工作流程
- 创建流:从数据源获取流
- 中间操作:对流进行一系列转换和处理(延迟执行)
- 终端操作:触发流的计算,返回结果
9.4 什么是中间操作和终端操作?
- 中间操作:返回新Stream的操作,可以链式调用,不会立即执行
- 终端操作:返回非Stream的操作,会触发流的执行,一个流只能有一个终端操作
9.5 并行流和顺序流有什么区别?
- 顺序流:在单个线程上按顺序处理元素
- 并行流:利用多核处理器的优势,将流分割成多个部分,并行处理
十、总结
Java 8的Stream API为集合处理提供了强大的功能,使代码更简洁易读。
核心概念回顾:
- Stream流的三个步骤:创建流、中间操作、终端操作
- 中间操作:filter、map、sorted等,延迟执行
- 终端操作:forEach、collect、count等,触发流执行
实用建议:
- 学习并熟练运用Stream API,可以大大提高代码质量和开发效率
- 合理使用并行流,但需谨慎评估性能收益
- 避免在Stream操作中产生副作用
- 利用丰富的Collectors工具类处理复杂的收集需求
Stream API是现代Java编程的重要组成部分,掌握它可以让你的代码更具表达力和可维护性。