Java 8 Stream API:函数式编程与集合操作的性能优化——告别笨重循环,拥抱丝滑流畅!
各位亲爱的程序员们,大家撸码辛苦啦!有没有觉得每天对着那些冗长的for
循环、if-else
判断,头都大了几圈?是不是经常梦想着能用一种更优雅、更高效的方式来处理集合数据? 别着急,Java 8 的 Stream API
就是来拯救你们的!
今天,我们就来好好聊聊这个神奇的 Stream API
, 看看它如何用函数式编程的思想,为我们的集合操作带来性能上的飞跃,让我们告别笨重的循环,拥抱丝滑流畅的代码体验!
一、 什么是 Stream API? 你以为的河流,其实是数据管道!
想象一下,你站在一条缓缓流淌的河流旁,河里漂浮着各种各样的东西:树叶、小鱼、塑料瓶……你想把这些东西过滤一下,只留下树叶。传统的方式,你可能需要拿着网兜,一个个捞出来,然后判断是不是树叶。
而 Stream API
就像一条数据管道,你只需要告诉管道你需要什么,它就会自动把符合条件的东西筛选出来,然后送到你手里。这个管道可以进行各种各样的处理,比如过滤、排序、转换等等,而且这些处理都是并行进行的,速度飞快!
简单来说,Stream API
是 Java 8 引入的一套用于处理集合数据的 API。它基于函数式编程的思想,允许你以声明式的方式操作集合,而无需编写大量的循环代码。
二、 为什么我们需要 Stream API? 代码更优雅,性能更卓越!
传统的集合操作方式,通常使用循环来实现,代码冗长,可读性差,而且容易出错。更重要的是,循环是顺序执行的,无法充分利用多核 CPU 的优势。
Stream API
的出现,解决了这些问题。它具有以下优点:
- 代码简洁: 使用
Stream API
可以用更少的代码完成复杂的集合操作。 - 可读性强:
Stream API
使用链式调用,代码逻辑清晰易懂。 - 并行处理:
Stream API
可以自动将操作并行化,充分利用多核 CPU 的优势,提高性能。 - 延迟执行:
Stream API
的很多操作都是延迟执行的,只有在需要结果时才会执行,避免了不必要的计算。
举个例子,假设我们要从一个列表中筛选出所有大于 10 的偶数,并计算它们的平方和。
传统方式:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
List<Integer> evenNumbersGreaterThanTen = new ArrayList<>();
for (Integer number : numbers) {
if (number > 10 && number % 2 == 0) {
evenNumbersGreaterThanTen.add(number);
}
}
List<Integer> squaredNumbers = new ArrayList<>();
for (Integer number : evenNumbersGreaterThanTen) {
squaredNumbers.add(number * number);
}
int sum = 0;
for (Integer number : squaredNumbers) {
sum += number;
}
System.out.println("Sum of squares of even numbers greater than 10: " + sum);
Stream API 方式:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
int sum = numbers.stream()
.filter(n -> n > 10 && n % 2 == 0)
.map(n -> n * n)
.reduce(0, Integer::sum);
System.out.println("Sum of squares of even numbers greater than 10: " + sum);
看到区别了吗? Stream API
的代码更加简洁、易读,而且性能更好。
三、 Stream API 的核心概念: 掌握这些,你就是 Stream 大师!
Stream API
的核心概念主要有三个:
- Stream: 代表一个元素序列,可以来自集合、数组、I/O 管道等等。
- 中间操作: 对 Stream 进行转换的操作,例如
filter
、map
、sorted
等。中间操作会返回一个新的 Stream。 - 终端操作: 产生结果或副作用的操作,例如
forEach
、collect
、reduce
等。终端操作会消耗 Stream,执行完毕后 Stream 就不能再使用了。
可以用下图简单概括:
[数据源] --> [Stream] --> [中间操作1] --> [Stream] --> [中间操作2] --> [Stream] --> ... --> [终端操作] --> [结果]
1. Stream 的创建
创建 Stream 的方式有很多种:
-
从集合创建:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Stream<Integer> stream = numbers.stream(); // 顺序流 Stream<Integer> parallelStream = numbers.parallelStream(); // 并行流
-
从数组创建:
int[] numbers = {1, 2, 3, 4, 5}; IntStream stream = Arrays.stream(numbers);
-
使用
Stream.of()
创建:Stream<String> stream = Stream.of("a", "b", "c");
-
使用
Stream.iterate()
创建无限流:Stream<Integer> stream = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
-
使用
Stream.generate()
创建无限流:Stream<Double> stream = Stream.generate(Math::random); // 随机数流
-
从文件创建:
try (Stream<String> stream = Files.lines(Paths.get("file.txt"))) { // 处理文件中的每一行 } catch (IOException e) { e.printStackTrace(); }
2. 常见的中间操作
-
filter(Predicate<T> predicate)
: 过滤 Stream 中的元素,只保留符合条件的元素。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); numbers.stream() .filter(n -> n % 2 == 0) // 过滤出偶数 .forEach(System.out::println); // 输出 2, 4, 6, 8, 10
-
map(Function<T, R> mapper)
: 将 Stream 中的每个元素转换为另一种类型。List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); names.stream() .map(String::toUpperCase) // 将字符串转换为大写 .forEach(System.out::println); // 输出 ALICE, BOB, CHARLIE
-
flatMap(Function<T, Stream<R>> mapper)
: 将 Stream 中的每个元素转换为一个 Stream,然后将所有 Stream 合并成一个 Stream。List<List<Integer>> numbers = Arrays.asList( Arrays.asList(1, 2), Arrays.asList(3, 4), Arrays.asList(5, 6) ); numbers.stream() .flatMap(List::stream) // 将 List<List<Integer>> 转换为 Stream<Integer> .forEach(System.out::println); // 输出 1, 2, 3, 4, 5, 6
-
distinct()
: 去除 Stream 中重复的元素。List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 4, 4, 4); numbers.stream() .distinct() // 去除重复元素 .forEach(System.out::println); // 输出 1, 2, 3, 4
-
sorted()
: 对 Stream 中的元素进行排序。List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9, 4); numbers.stream() .sorted() // 升序排序 .forEach(System.out::println); // 输出 1, 2, 4, 5, 8, 9 numbers.stream() .sorted(Comparator.reverseOrder()) // 降序排序 .forEach(System.out::println); // 输出 9, 8, 5, 4, 2, 1
-
peek(Consumer<T> action)
: 对 Stream 中的每个元素执行一些操作,但不改变 Stream 本身。通常用于调试。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.stream() .peek(System.out::println) // 输出每个元素 .filter(n -> n % 2 == 0) .forEach(System.out::println); // 只输出偶数,但之前会先输出所有元素
-
limit(long maxSize)
: 截取 Stream 中的前maxSize
个元素。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); numbers.stream() .limit(5) // 只取前 5 个元素 .forEach(System.out::println); // 输出 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); numbers.stream() .skip(5) // 跳过前 5 个元素 .forEach(System.out::println); // 输出 6, 7, 8, 9, 10
3. 常见的终端操作
-
forEach(Consumer<T> action)
: 对 Stream 中的每个元素执行一些操作。List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); names.stream() .forEach(System.out::println); // 输出 Alice, Bob, Charlie
-
toArray()
: 将 Stream 中的元素转换为数组。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Integer[] array = numbers.stream() .toArray(Integer[]::new); System.out.println(Arrays.toString(array)); // 输出 [1, 2, 3, 4, 5]
-
collect(Collector<T, A, R> collector)
: 将 Stream 中的元素收集到集合中。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> evenNumbers = numbers.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); // 收集到 List 中 System.out.println(evenNumbers); // 输出 [2, 4] Set<Integer> oddNumbers = numbers.stream() .filter(n -> n % 2 != 0) .collect(Collectors.toSet()); // 收集到 Set 中 System.out.println(oddNumbers); // 输出 [1, 3, 5] String joinedString = names.stream() .collect(Collectors.joining(", ")); // 使用逗号连接字符串 System.out.println(joinedString); // 输出 Alice, Bob, Charlie double average = numbers.stream() .collect(Collectors.averagingInt(Integer::intValue)); // 计算平均值 System.out.println(average); // 输出 3.0
-
reduce(T identity, BinaryOperator<T> accumulator)
: 将 Stream 中的元素归约成一个值。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream() .reduce(0, Integer::sum); // 计算总和 System.out.println(sum); // 输出 15 Optional<Integer> max = numbers.stream() .reduce(Integer::max); // 找出最大值 System.out.println(max.get()); // 输出 5
-
min(Comparator<T> comparator)
: 找出 Stream 中的最小值。List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9, 4); Optional<Integer> min = numbers.stream() .min(Integer::compareTo); // 找出最小值 System.out.println(min.get()); // 输出 1
-
max(Comparator<T> comparator)
: 找出 Stream 中的最大值。List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9, 4); Optional<Integer> max = numbers.stream() .max(Integer::compareTo); // 找出最大值 System.out.println(max.get()); // 输出 9
-
count()
: 统计 Stream 中元素的个数。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); long count = numbers.stream() .count(); // 统计元素个数 System.out.println(count); // 输出 5
-
anyMatch(Predicate<T> predicate)
: 判断 Stream 中是否至少有一个元素符合条件。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); boolean hasEvenNumber = numbers.stream() .anyMatch(n -> n % 2 == 0); // 是否有偶数 System.out.println(hasEvenNumber); // 输出 true
-
allMatch(Predicate<T> predicate)
: 判断 Stream 中是否所有元素都符合条件。List<Integer> numbers = Arrays.asList(2, 4, 6, 8, 10); boolean allEven = numbers.stream() .allMatch(n -> n % 2 == 0); // 是否所有元素都是偶数 System.out.println(allEven); // 输出 true
-
noneMatch(Predicate<T> predicate)
: 判断 Stream 中是否没有元素符合条件。List<Integer> numbers = Arrays.asList(1, 3, 5, 7, 9); boolean noEven = numbers.stream() .noneMatch(n -> n % 2 == 0); // 是否没有偶数 System.out.println(noEven); // 输出 true
-
findFirst()
: 找出 Stream 中的第一个元素。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> first = numbers.stream() .findFirst(); // 找出第一个元素 System.out.println(first.get()); // 输出 1
-
findAny()
: 找出 Stream 中的任意一个元素。在并行流中,findAny()
的性能可能比findFirst()
更好。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> any = numbers.stream() .findAny(); // 找出任意一个元素 System.out.println(any.get()); // 输出 1 (也可能是其他元素,取决于具体实现)
四、 Stream API 的性能优化: 并行流的正确打开方式!
Stream API
的一个重要优势是可以进行并行处理,充分利用多核 CPU 的优势,提高性能。但是,使用并行流需要注意一些问题,否则可能会适得其反。
1. 什么时候使用并行流?
- 数据量大: 当数据量足够大时,并行处理才能体现出优势。
- 计算密集型: 当操作比较耗时,例如复杂的计算或 I/O 操作时,并行处理才能带来明显的性能提升。
- 无状态操作: 并行流更适合无状态操作,即操作的结果不依赖于之前的状态。例如
filter
、map
等。
2. 什么时候避免使用并行流?
- 数据量小: 当数据量很小时,并行处理的开销可能会超过收益。
- 有状态操作: 有状态操作,例如
sorted
、distinct
等,在并行处理时需要额外的同步开销,可能会降低性能。 - I/O 密集型: 如果操作主要是 I/O 操作,并行处理可能不会带来明显的性能提升,甚至可能降低性能。
- 共享可变状态: 如果多个线程同时访问和修改共享的可变状态,可能会导致数据竞争和死锁。
3. 如何正确使用并行流?
-
使用
parallelStream()
创建并行流:List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.parallelStream() // 创建并行流 .filter(n -> n % 2 == 0) .forEach(System.out::println);
-
使用
stream().parallel()
将顺序流转换为并行流:List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.stream() .parallel() // 转换为并行流 .filter(n -> n % 2 == 0) .forEach(System.out::println);
-
避免共享可变状态: 尽量避免在并行流中使用共享的可变状态。如果必须使用,需要使用适当的同步机制来保证线程安全。
-
使用
Spliterator
自定义数据分割:Spliterator
允许你自定义如何将数据分割成多个部分,以便并行处理。
4. 性能测试和分析
在使用并行流之前,一定要进行性能测试和分析,以确保它能够带来实际的性能提升。可以使用 JMH (Java Microbenchmark Harness) 等工具进行性能测试。
示例:使用并行流计算 1 到 1000000 的总和
import java.util.stream.IntStream;
public class ParallelStreamExample {
public static void main(String[] args) {
// 顺序流
long startTime = System.nanoTime();
int sumSequential = IntStream.rangeClosed(1, 1000000)
.sum();
long endTime = System.nanoTime();
System.out.println("Sequential sum: " + sumSequential + ", time: " + (endTime - startTime) / 1000000 + "ms");
// 并行流
startTime = System.nanoTime();
int sumParallel = IntStream.rangeClosed(1, 1000000)
.parallel()
.sum();
endTime = System.nanoTime();
System.out.println("Parallel sum: " + sumParallel + ", time: " + (endTime - startTime) / 1000000 + "ms");
}
}
在多核 CPU 的机器上运行这个程序,你会发现并行流的性能明显优于顺序流。
五、 Stream API 的一些高级用法: 进阶之路,永无止境!
除了上面介绍的基本用法,Stream API
还有一些高级用法,可以帮助你解决更复杂的问题。
-
自定义 Collector: 如果你需要将 Stream 中的元素收集到自定义的集合中,可以使用
Collector.of()
方法创建一个自定义的 Collector。 -
使用
groupingBy()
进行分组:Collectors.groupingBy()
方法可以将 Stream 中的元素按照某个属性进行分组。 -
使用
partitioningBy()
进行分区:Collectors.partitioningBy()
方法可以将 Stream 中的元素按照某个条件分成两组。 -
处理 Optional 值:
Stream API
可以很好地处理Optional
值,避免空指针异常。
六、 总结: Stream API,你值得拥有!
Java 8 Stream API
是一种强大而灵活的工具,可以帮助你更优雅、更高效地处理集合数据。它基于函数式编程的思想,代码简洁、可读性强,而且可以进行并行处理,提高性能。
掌握 Stream API
, 你就能告别笨重的循环,拥抱丝滑流畅的代码体验,成为一个更优秀的程序员!
希望这篇文章能够帮助你更好地理解和使用 Stream API
。 记住,学习是一个不断探索的过程, 实践才是检验真理的唯一标准。 赶紧动手试试吧, 相信你会爱上 Stream API
的!
码字不易,如果觉得这篇文章对你有帮助, 欢迎点赞、收藏、分享! 谢谢大家!