各位听众,各位码农,大家好!我是老码,今天咱们聊聊Java Stream API,这玩意儿就像魔法棒一样,能让你的集合操作变得高效又优雅。
前言:集合的烦恼,以及Stream的救赎
话说这程序员的世界里,跟集合打交道那是家常便饭。List、Set、Map,哪个不是咱们的老朋友?可是,当数据量一大,要对这些集合进行过滤、转换、聚合的时候,传统的循环遍历就显得笨重不堪,代码又臭又长,看得人眼花缭乱,心情烦躁,恨不得摔键盘!
想想看,你要从一个学生列表中找出所有年龄大于18岁的学生,然后提取他们的姓名,最后按照姓名排序… 传统的写法:
List<Student> students = ...; // 一堆学生
List<String> adultStudentNames = new ArrayList<>();
for (Student student : students) {
if (student.getAge() > 18) {
adultStudentNames.add(student.getName());
}
}
Collections.sort(adultStudentNames);
这段代码是不是让你感觉回到了上个世纪? 就像在泥泞的道路上艰难跋涉,每一步都留下沉重的脚印。
这时,Stream API就像一位身披金甲的骑士,驾着风火轮来拯救我们了! 它以一种声明式的方式,让我们能专注于“做什么”,而不是“怎么做”。上面的例子用Stream API可以这样写:
List<String> adultStudentNames = students.stream()
.filter(student -> student.getAge() > 18)
.map(Student::getName)
.sorted()
.collect(Collectors.toList());
看到没?代码瞬间变得简洁明了,就像一首轻快的诗歌,优美而流畅。 🤩
Stream API是什么? 它的核心理念是什么?
Stream API是Java 8引入的一个强大的新特性,它允许你以一种声明式的方式处理数据集合。 它的核心理念是:
- 声明式编程: 告诉程序 "做什么",而不是 "怎么做"。就像你告诉厨师 "我要一份宫保鸡丁",而不是告诉他 "先把鸡肉切丁,然后…,最后…”。
- 链式操作: 像流水线一样,将多个操作串联起来,每个操作都对数据进行一次转换,最终得到想要的结果。
- 延迟执行: Stream API中的很多操作都是惰性的,只有在需要结果的时候才会执行。这可以避免不必要的计算,提高效率。
- 内部迭代: Stream API负责管理数据的迭代过程,你只需要专注于对数据的处理逻辑。就像你坐上了自动驾驶汽车,只需要告诉它 "去XX地方",而不用自己控制方向盘和油门。
Stream API的基本组成:三大金刚
Stream API由三个主要部分组成,我们称之为“三大金刚”:
- 数据源 (Source): 这是Stream的起点,可以是集合、数组、I/O通道等等。就像河流的源头,是整个流的起点。
- 中间操作 (Intermediate Operations): 这些操作会对Stream中的数据进行转换、过滤、排序等处理,返回一个新的Stream。这些操作可以链式调用,形成一个操作管道。就像河流中的水流,经过各种弯道、瀑布,最终到达目的地。
- 终端操作 (Terminal Operations): 这些操作会消费Stream,产生一个最终的结果,或者触发Stream的执行。就像河流的入海口,是整个流的终点。
用表格来总结一下:
操作类型 | 说明 | 示例 | 返回值 |
---|---|---|---|
数据源 (Source) | Stream的起点,提供数据。 | List.stream() , Arrays.stream(array) |
Stream<T> |
中间操作 | 对Stream中的数据进行转换、过滤、排序等处理,返回一个新的Stream。可以链式调用。 | filter(predicate) , map(function) , sorted() , distinct() , limit(n) , skip(n) |
Stream<T> (通常) |
终端操作 | 消费Stream,产生一个最终的结果,或者触发Stream的执行。 | forEach(consumer) , collect(collector) , reduce(identity, accumulator) , count() , min(comparator) , max(comparator) , anyMatch(predicate) , allMatch(predicate) , noneMatch(predicate) , findFirst() , findAny() |
各种类型,取决于具体操作。 例如: void (forEach), List<T> (collect), Optional<T> (findFirst, findAny), long (count), boolean (anyMatch, allMatch, noneMatch), T (reduce) |
中间操作:Stream的魔术师
中间操作是Stream API的核心,它们提供了各种各样的转换和过滤功能,就像魔术师一样,可以把数据变成你想要的样子。
-
filter(Predicate predicate): 过滤Stream中的元素,只保留满足条件的元素。
Predicate
是一个函数式接口,接受一个参数,返回一个boolean值。 就像一个筛子,把不符合条件的沙子过滤掉,只留下金子。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> evenNumbers = numbers.stream() .filter(number -> number % 2 == 0) // 过滤出偶数 .collect(Collectors.toList()); // evenNumbers: [2, 4, 6, 8, 10]
-
map(Function function): 将Stream中的每个元素转换成另一种类型。
Function
是一个函数式接口,接受一个参数,返回一个结果。 就像一个变形金刚,把汽车变成飞机。List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); List<Integer> nameLengths = names.stream() .map(String::length) // 获取每个字符串的长度 .collect(Collectors.toList()); // nameLengths: [5, 3, 7]
-
flatMap(Function<T, Stream> function): 将Stream中的每个元素转换成一个Stream,然后将所有的Stream合并成一个Stream。 就像一个拆箱专家,把所有的盒子都打开,把里面的东西都拿出来。
List<List<Integer>> numbers = Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4, 5)); List<Integer> flattenedNumbers = numbers.stream() .flatMap(List::stream) // 将List<List<Integer>> 转换为 List<Integer> .collect(Collectors.toList()); // flattenedNumbers: [1, 2, 3, 4, 5]
-
sorted(): 对Stream中的元素进行排序。 可以使用默认的自然排序,也可以自定义排序规则。 就像一个整理大师,把杂乱无章的东西整理得井井有条。
List<String> names = Arrays.asList("Charlie", "Alice", "Bob"); List<String> sortedNames = names.stream() .sorted() // 按照字母顺序排序 .collect(Collectors.toList()); // sortedNames: [Alice, Bob, Charlie] List<Student> students = ...; // 一堆学生 List<Student> sortedStudents = students.stream() .sorted(Comparator.comparing(Student::getAge).reversed()) // 按照年龄降序排序 .collect(Collectors.toList());
-
distinct(): 去除Stream中重复的元素。 就像一个去重神器,把重复的东西都扔掉,只留下独一无二的。
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 4, 4, 4); List<Integer> distinctNumbers = numbers.stream() .distinct() // 去除重复元素 .collect(Collectors.toList()); // distinctNumbers: [1, 2, 3, 4]
-
limit(long maxSize): 截取Stream中的前maxSize个元素。 就像一个剪刀手,把多余的部分剪掉,只留下精华。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> limitedNumbers = numbers.stream() .limit(5) // 截取前5个元素 .collect(Collectors.toList()); // limitedNumbers: [1, 2, 3, 4, 5]
-
skip(long n): 跳过Stream中的前n个元素。 就像一个跳远运动员,跳过前面的障碍,直接到达目的地。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> skippedNumbers = numbers.stream() .skip(5) // 跳过前5个元素 .collect(Collectors.toList()); // skippedNumbers: [6, 7, 8, 9, 10]
终端操作:Stream的终结者
终端操作会消费Stream,产生一个最终的结果,或者触发Stream的执行。 就像电影的结尾,给故事画上一个句号。
-
forEach(Consumer consumer): 对Stream中的每个元素执行一个操作。
Consumer
是一个函数式接口,接受一个参数,不返回任何值。 就像一个播音员,把每个人的名字都念一遍。List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); names.stream() .forEach(System.out::println); // 打印每个名字
-
collect(Collector collector): 将Stream中的元素收集到一个集合中。
Collector
是一个接口,提供了各种各样的收集方式,例如toList、toSet、toMap等等。 就像一个收纳盒,把所有的东西都放进去。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> evenNumbers = numbers.stream() .filter(number -> number % 2 == 0) .collect(Collectors.toList()); // 收集到List中 Set<Integer> oddNumbers = numbers.stream() .filter(number -> number % 2 != 0) .collect(Collectors.toSet()); // 收集到Set中 Map<String, Integer> nameLengthMap = names.stream() .collect(Collectors.toMap(name -> name, String::length)); // 收集到Map中
-
reduce(identity, accumulator): 将Stream中的元素归约为一个值。
identity
是初始值,accumulator
是一个函数式接口,接受两个参数,返回一个结果。 就像一个搅拌机,把所有的东西都混合在一起,变成一种新的东西。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream() .reduce(0, (a, b) -> a + b); // 求和 // sum: 15 String concatenatedNames = names.stream() .reduce("", (a, b) -> a + ", " + b); // 连接字符串 // concatenatedNames: ", Alice, Bob, Charlie"
-
count(): 返回Stream中元素的个数。 就像一个计数器,统计有多少个东西。
long count = numbers.stream() .count(); // 统计元素的个数 // count: 5
-
min(Comparator comparator): 返回Stream中最小的元素。
Comparator
是一个接口,用于比较两个元素的大小。 就像一个选美大赛,选出最漂亮的那个。Optional<Integer> min = numbers.stream() .min(Integer::compareTo); // 找到最小的元素 // min: Optional[1]
-
max(Comparator comparator): 返回Stream中最大的元素。 就像一个大力士比赛,选出最强壮的那个。
Optional<Integer> max = numbers.stream() .max(Integer::compareTo); // 找到最大的元素 // max: Optional[5]
-
anyMatch(Predicate predicate): 判断Stream中是否存在任意一个元素满足条件。 就像一个侦探,寻找蛛丝马迹,只要找到一个就够了。
boolean hasEvenNumber = numbers.stream() .anyMatch(number -> number % 2 == 0); // 是否存在偶数 // hasEvenNumber: true
-
allMatch(Predicate predicate): 判断Stream中是否所有元素都满足条件。 就像一个考官,检查每个学生的答案,必须全部正确才行。
boolean allPositive = numbers.stream() .allMatch(number -> number > 0); // 是否所有元素都大于0 // allPositive: true
-
noneMatch(Predicate predicate): 判断Stream中是否没有元素满足条件。 就像一个医生,检查病人是否健康,必须没有任何疾病才行。
boolean noNegative = numbers.stream() .noneMatch(number -> number < 0); // 是否没有负数 // noNegative: true
-
findFirst(): 返回Stream中的第一个元素。 就像一个寻宝者,找到第一个宝藏就满足了。
Optional<Integer> first = numbers.stream() .findFirst(); // 找到第一个元素 // first: Optional[1]
-
findAny(): 返回Stream中的任意一个元素。 在并行Stream中,可以更高效地找到一个元素。 就像一个随机抽奖,随便抽一个就行了。
Optional<Integer> any = numbers.parallelStream() // 使用并行Stream .findAny(); // 找到任意一个元素 // any: Optional[1] (结果不确定)
Stream的性能考量:并非银弹
Stream API 确实很强大,但它也不是万能的。在使用Stream API的时候,我们需要注意一些性能问题:
- 中间操作的开销: 每个中间操作都会创建一个新的Stream,这会带来一定的开销。 因此,我们应该尽量减少中间操作的次数。
- 并行Stream的线程安全: 在使用并行Stream的时候,我们需要注意线程安全问题。 如果Stream中的操作不是线程安全的,可能会导致数据竞争和错误。
- 过度使用Stream: 虽然Stream API很方便,但是过度使用Stream可能会导致代码可读性下降。 在一些简单的场景下,传统的循环遍历可能更合适。
Stream API的实战案例:让代码飞起来
说了这么多理论,不如来几个实战案例,让大家感受一下Stream API的魅力。
案例1:统计学生成绩
假设我们有一个学生列表,每个学生都有姓名和成绩。 我们要统计所有及格学生的平均成绩。
class Student {
private String name;
private int score;
// 省略构造函数、getter和setter
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
public void setName(String name) {
this.name = name;
}
public void setScore(int score) {
this.score = score;
}
}
List<Student> students = Arrays.asList(
new Student("Alice", 80),
new Student("Bob", 50),
new Student("Charlie", 90),
new Student("David", 60),
new Student("Eve", 40)
);
double averageScore = students.stream()
.filter(student -> student.getScore() >= 60) // 过滤出及格的学生
.mapToInt(Student::getScore) // 获取成绩
.average() // 计算平均值
.orElse(0.0); // 如果没有及格的学生,返回0.0
System.out.println("及格学生的平均成绩:" + averageScore); // 输出:及格学生的平均成绩:76.66666666666667
案例2:查找最长的单词
假设我们有一个字符串列表,我们要找到其中最长的单词。
List<String> words = Arrays.asList("apple", "banana", "orange", "grapefruit");
Optional<String> longestWord = words.stream()
.max(Comparator.comparingInt(String::length)); // 按照长度比较
System.out.println("最长的单词:" + longestWord.orElse("")); // 输出:最长的单词:grapefruit
案例3:统计单词出现次数
假设我们有一段文本,我们要统计每个单词出现的次数。
String text = "This is a test. This is only a test.";
Map<String, Long> wordCounts = Arrays.stream(text.split("\s+")) // 将文本分割成单词
.map(word -> word.replaceAll("[^a-zA-Z]", "").toLowerCase()) // 去除标点符号并转换为小写
.filter(word -> !word.isEmpty()) // 过滤掉空字符串
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); // 统计单词出现次数
System.out.println("单词出现次数:" + wordCounts); // 输出:单词出现次数:{a=2, this=2, test=2, is=2, only=1}
总结:Stream API,让你的代码更上一层楼
Stream API是Java 8中一个非常重要的特性,它能让我们以一种声明式的方式处理数据集合,提高代码的可读性和效率。 掌握Stream API,就像拥有了一把锋利的宝剑,能让你在代码的世界里披荆斩棘,所向披靡。 💪
希望今天的分享对大家有所帮助,谢谢大家! 🍻
最后的彩蛋:Stream API的进阶用法
- 并行Stream: 利用多核CPU的优势,加速数据处理。
- 自定义Collector: 实现更复杂的收集逻辑。
- 无限Stream: 生成无限序列的数据。
这些高级用法,就留给大家自己去探索吧! 祝大家编码愉快! 😊