Java Stream API:高效处理集合数据
在Java编程中,集合操作是日常开发的核心任务之一。传统的集合处理方式往往需要编写大量迭代器和条件判断代码,不仅冗长繁琐,而且可读性和可维护性较差。Java 8引入的Stream API彻底改变了这一现状,它提供了一种声明式、函数式的集合处理方式,让开发者能够以更简洁、更高效的方式处理数据。
本文将全面解析Java Stream API的设计理念、核心功能和实战技巧,通过大量代码示例展示如何利用Stream API简化集合操作、提高代码质量,并深入探讨其性能优化机制。无论是刚接触Stream的初学者,还是希望深入掌握其高级特性的开发者,都能从本文中获得有价值的知识。
一、Stream API概述:为什么需要Stream?
在Java 8之前,处理集合数据通常需要使用for循环或Iterator进行迭代,然后通过if条件判断筛选元素,再进行转换或聚合操作。这种命令式编程风格存在诸多问题:
- 代码冗长:完成简单的数据处理任务也需要编写多行代码。
- 可读性差:迭代逻辑与业务逻辑混杂,难以快速理解代码意图。
- 并行处理复杂:手动实现集合的并行处理需要考虑线程安全、任务拆分等复杂问题。
- 容易出错:迭代过程中修改集合可能导致ConcurrentModificationException,边界条件处理容易出错。
Stream API的出现正是为了解决这些问题,它借鉴了函数式编程的思想,提供了一种高效、简洁的数据处理方式。
1. 什么是Stream?
Stream(流) 是Java 8引入的一个全新概念,它不是数据结构,也不存储数据,而是代表了一系列支持连续、并行聚合操作的元素序列。Stream API的核心思想是将集合的处理过程抽象为流水线式的操作,开发者只需关注"做什么",而无需关心"怎么做"(如迭代方式、并行处理等)。
Stream的特点可以概括为:
- 非存储性:Stream不存储数据,数据来源于集合、数组或其他生成器。
- 功能性:Stream操作不会修改源数据,而是返回一个新的Stream。
- 惰性求值:中间操作(如过滤、映射)不会立即执行,直到终端操作(如收集、计数)被调用时才会触发实际计算。
- 一次性:一个Stream只能被消费一次,再次使用会抛出IllegalStateException。
- 可并行:Stream API原生支持并行处理,无需编写复杂的多线程代码。
2. Stream与集合的区别
存储 | 存储数据元素 | 不存储数据,仅描述操作 |
关注点 | 数据的持有 | 数据的处理 |
迭代方式 | 外部迭代(显式使用for循环) | 内部迭代(Stream自动处理迭代) |
执行时机 | 即时执行 | 惰性执行(终端操作触发计算) |
可重用性 | 可多次遍历 | 只能遍历一次 |
并行处理 | 需要手动实现 | 原生支持(parallelStream()) |
3. Stream API的优势
- 代码简洁:用少量代码实现复杂的数据处理逻辑,提高开发效率。
- 可读性强:声明式编程风格使代码意图更清晰,便于维护。
- 易于并行化:只需调用parallel()方法即可实现并行处理,充分利用多核CPU。
- 函数式集成:与Lambda表达式、方法引用等函数式特性无缝集成。
- 丰富的操作:提供了大量内置操作(过滤、映射、聚合等),满足各种处理需求。
二、Stream的创建:从不同数据源生成流
在使用Stream之前,我们需要先创建它。Stream可以从多种数据源生成,最常见的包括集合、数组、值序列等。
1. 从集合创建Stream
Java集合框架(Collection)在Java 8中新增了两个方法用于创建Stream:
- stream():返回一个顺序流(串行处理)。
- parallelStream():返回一个并行流(多线程并行处理)。
示例1:从集合创建Stream
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamCreation {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");
// 创建顺序流
Stream<String> sequentialStream = fruits.stream();
// 创建并行流
Stream<String> parallelStream = fruits.parallelStream();
// 打印流中的元素(终端操作)
sequentialStream.forEach(System.out::println);
System.out.println("—– 并行流 —–");
parallelStream.forEach(System.out::println);
}
}
输出结果:
apple
banana
orange
grape
—– 并行流 —–
orange
grape
apple
banana
注意:并行流的输出顺序可能与源集合不同,因为多线程处理的顺序不确定。
2. 从数组创建Stream
Arrays类提供了stream()方法,可以将数组转换为Stream:
示例2:从数组创建Stream
import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class ArrayToStream {
public static void main(String[] args) {
// 对象数组
String[] animals = {"cat", "dog", "bird", "fish"};
Stream<String> animalStream = Arrays.stream(animals);
animalStream.forEach(System.out::println);
// 基本类型数组(int)
int[] numbers = {1, 2, 3, 4, 5};
IntStream numberStream = Arrays.stream(numbers);
numberStream.forEach(System.out::println);
// 截取数组的一部分创建Stream(从索引1到4,不包含4)
IntStream rangeStream = Arrays.stream(numbers, 1, 4);
rangeStream.forEach(System.out::println); // 输出:2, 3, 4
}
}
Java 8为基本类型(int、long、double)提供了专门的Stream类型(IntStream、LongStream、DoubleStream),避免了自动装箱/拆箱的性能开销。
3. 从值序列创建Stream
Stream类的静态方法of()可以直接从一系列值创建Stream:
示例3:从值序列创建Stream
import java.util.stream.Stream;
public class ValuesToStream {
public static void main(String[] args) {
// 单个值
Stream<String> singleValueStream = Stream.of("hello");
singleValueStream.forEach(System.out::println);
// 多个值
Stream<Integer> numbersStream = Stream.of(1, 2, 3, 4, 5);
numbersStream.forEach(System.out::println);
// 空Stream
Stream<String> emptyStream = Stream.empty();
System.out.println("空Stream的元素数量:" + emptyStream.count()); // 输出:0
}
}
4. 从生成器创建无限Stream
Stream提供了两个静态方法用于创建无限流(元素可以无限生成),通常需要配合limit()方法限制元素数量:
- generate(Supplier<T> s):通过Supplier生成无限流,元素无序。
- iterate(T seed, UnaryOperator<T> f):从初始值开始,通过UnaryOperator迭代生成无限流,元素有序。
示例4:创建无限Stream
import java.util.Random;
import java.util.stream.Stream;
public class InfiniteStream {
public static void main(String[] args) {
// 1. 使用generate()生成随机数(限制10个)
Random random = new Random();
Stream<Double> randomNumbers = Stream.generate(random::nextDouble).limit(10);
randomNumbers.forEach(num -> System.out.printf("%.2f ", num));
System.out.println();
// 2. 使用iterate()生成自然数序列(限制10个)
Stream<Integer> naturalNumbers = Stream.iterate(1, n -> n + 1).limit(10);
naturalNumbers.forEach(num -> System.out.print(num + " "));
System.out.println();
// 3. 使用iterate()生成斐波那契数列(限制10个)
Stream.iterate(new int[]{0, 1}, fib -> new int[]{fib[1], fib[0] + fib[1]})
.limit(10)
.map(fib -> fib[0]) // 提取数列中的第一个元素
.forEach(num -> System.out.print(num + " ")); // 输出:0 1 1 2 3 5 8 13 21 34
}
}
注意:无限流必须配合limit()等方法使用,否则终端操作会陷入无限循环。
5. 从文件创建Stream
Java NIO的Files类提供了lines()方法,可以将文件的每一行作为Stream的元素:
示例5:从文件创建Stream
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
public class FileToStream {
public static void main(String[] args) {
// 读取文件内容为Stream(每行一个元素)
try (Stream<String> lines = Files.lines(Paths.get("example.txt"))) {
// 统计文件行数
long lineCount = lines.count();
System.out.println("文件行数:" + lineCount);
} catch (IOException e) {
e.printStackTrace();
}
// 过滤包含特定关键字的行
try (Stream<String> lines = Files.lines(Paths.get("example.txt"))) {
lines.filter(line -> line.contains("java"))
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意:使用Files.lines()时应将Stream放在try-with-resources语句中,确保资源正确关闭。
三、Stream的操作:中间操作与终端操作
Stream的操作可以分为两大类:中间操作(Intermediate Operations) 和终端操作(Terminal Operations)。理解这两类操作的区别是掌握Stream API的关键。
1. 操作类型概述
- 中间操作:对Stream进行处理后返回一个新的Stream,支持链式调用。中间操作是惰性的,不会立即执行,只有当终端操作被调用时才会触发计算。
- 终端操作:触发Stream的计算并产生一个结果(或副作用),之后Stream便不可再使用。
Stream操作流水线示例:
List<String> result = list.stream() // 创建Stream(源)
.filter(s -> s.length() > 5) // 中间操作:过滤
.map(String::toUpperCase) // 中间操作:转换
.sorted() // 中间操作:排序
.collect(Collectors.toList()); // 终端操作:收集结果
2. 常用中间操作
过滤(filter)
filter(Predicate<T> predicate):保留满足Predicate条件的元素。
示例6:过滤操作
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class FilterExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 过滤出偶数
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println("偶数:" + evenNumbers); // 输出:[2, 4, 6, 8, 10]
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape", "kiwi");
// 过滤出长度大于5的水果名称
List<String> longNames = fruits.stream()
.filter(fruit -> fruit.length() > 5)
.collect(Collectors.toList());
System.out.println("名称长度大于5的水果:" + longNames); // 输出:[banana, orange]
}
}
映射(map)
map(Function<T, R> mapper):将Stream中的每个元素通过Function转换为另一种类型,返回一个包含转换后元素的新Stream。
示例7:映射操作
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MapExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("hello", "world", "java", "stream");
// 将字符串转换为其长度
List<Integer> wordLengths = words.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println("单词长度:" + wordLengths); // 输出:[5, 5, 4, 6]
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 将每个数字平方
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println("平方数:" + squares); // 输出:[1, 4, 9, 16, 25]
}
}
对于基本类型Stream(如IntStream),可以使用mapToInt()、mapToLong()、mapToDouble()等方法避免自动装箱:
import java.util.Arrays;
import java.util.List;
public class PrimitiveMapExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("hello", "world", "java", "stream");
// 计算所有单词的总长度(使用mapToInt避免装箱)
int totalLength = words.stream()
.mapToInt(String::length)
.sum(); // IntStream的sum()方法
System.out.println("总长度:" + totalLength); // 输出:20
}
}
扁平化映射(flatMap)
flatMap(Function<T, Stream<R>> mapper):将每个元素转换为一个Stream,然后将所有Stream合并为一个Stream(扁平化)。常用于处理嵌套集合。
示例8:flatMap操作
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class FlatMapExample {
public static void main(String[] args) {
// 嵌套集合:列表的列表
List<List<Integer>> numbers = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
// 使用flatMap将嵌套列表转换为扁平列表
List<Integer> flatNumbers = numbers.stream()
.flatMap(List::stream) // 将每个子列表转换为Stream
.collect(Collectors.toList());
System.out.println("扁平化后的列表:" + flatNumbers); // 输出:[1, 2, 3, 4, 5, 6, 7, 8, 9]
// 处理字符串中的字符
List<String> words = Arrays.asList("hello", "world");
// 提取所有不重复的字符
List<Character> uniqueChars = words.stream()
.flatMap(word -> {
// 将每个字符串转换为字符Stream
char[] chars = word.toCharArray();
Character[] characters = new Character[chars.length];
for (int i = 0; i < chars.length; i++) {
characters[i] = chars[i];
}
return Arrays.stream(characters);
})
.distinct() // 去重
.sorted() // 排序
.collect(Collectors.toList());
System.out.println("所有不重复的字符:" + uniqueChars); // 输出:[d, e, h, l, o, r, w]
}
}
flatMap与map的区别:map将一个元素转换为一个新元素,flatMap将一个元素转换为多个元素(通过Stream)。
排序(sorted)
sorted():使用自然顺序对元素排序(要求元素实现Comparable接口)。
sorted(Comparator<T> comparator):使用自定义比较器排序。
示例9:排序操作
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class SortedExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6);
// 自然排序(升序)
List<Integer> sortedNatural = numbers.stream()
.sorted()
.collect(Collectors.toList());
System.out.println("自然排序:" + sortedNatural); // 输出:[1, 1, 2, 3, 4, 5, 6, 9]
// 自定义排序(降序)
List<Integer> sortedDescending = numbers.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
System.out.println("降序排序:" + sortedDescending); // 输出:[9, 6, 5, 4, 3, 2, 1, 1]
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
// 按字符串长度排序(短到长)
List<String> sortedByLength = words.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
System.out.println("按长度排序:" + sortedByLength); // 输出:[apple, grape, banana, orange]
// 按字符串长度倒序,长度相同则按字母顺序
List<String> sortedComplex = words.stream()
.sorted(Comparator.comparingInt(String::length)
.reversed()
.thenComparing(Comparator.naturalOrder()))
.collect(Collectors.toList());
System.out.println("复杂排序:" + sortedComplex); // 输出:[banana, orange, apple, grape]
}
}
去重(distinct)
distinct():根据元素的equals()方法去除重复元素。
示例10:去重操作
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class DistinctExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 5, 5);
// 去除重复数字
List<Integer> uniqueNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println("去重后的数字:" + uniqueNumbers); // 输出:[1, 2, 3, 4, 5]
List<String> words = Arrays.asList("apple", "banana", "apple", "orange", "banana");
// 去除重复字符串
List<String> uniqueWords = words.stream()
.distinct()
.collect(Collectors.toList());
System.out.println("去重后的单词:" + uniqueWords); // 输出:[apple, banana, orange]
}
}
限制与跳过(limit/skip)
- limit(long maxSize):保留Stream中的前maxSize个元素。
- skip(long n):跳过Stream中的前n个元素。
示例11:limit与skip操作
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class LimitSkipExample {
public static void main(String[] args) {
// 生成1-20的数字
IntStream numbers = IntStream.rangeClosed(1, 20);
// 取前10个数字
List<Integer> first10 = numbers.limit(10)
.boxed() // 将IntStream转换为Stream<Integer>
.collect(Collectors.toList());
System.out.println("前10个数字:" + first10); // 输出:[1, 2, …, 10]
// 生成1-20的数字(重新生成,因为上一个Stream已被消费)
IntStream numbers2 = IntStream.rangeClosed(1, 20);
// 跳过前10个,取剩下的
List<Integer> after10 = numbers2.skip(10)
.boxed()
.collect(Collectors.toList());
System.out.println("10之后的数字:" + after10); // 输出:[11, 12, …, 20]
// 分页示例:取第2页,每页5条数据(跳过前5条,取5条)
List<Integer> page2 = IntStream.rangeClosed(1, 20)
.skip(5)
.limit(5)
.boxed()
.collect(Collectors.toList());
System.out.println("第2页数据:" + page2); // 输出:[6, 7, 8, 9, 10]
}
}
3. 常用终端操作
遍历(forEach)
forEach(Consumer<T> action):对Stream中的每个元素执行Consumer操作(终端操作,无返回值)。
示例12:forEach操作
import java.util.Arrays;
import java.util.List;
public class ForEachExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange");
// 遍历并打印元素
fruits.stream()
.forEach(System.out::println);
// 并行流的forEach可能乱序
System.out.println("—– 并行流遍历 —–");
fruits.parallelStream()
.forEach(System.out::println);
// forEachOrdered:保证顺序(即使是并行流),但可能降低并行效率
System.out.println("—– 并行流有序遍历 —–");
fruits.parallelStream()
.forEachOrdered(System.out::println);
}
}
注意:并行流中forEach()不保证元素处理顺序,forEachOrdered()可以保证顺序但可能损失并行性能。
收集(collect)
collect(Collector<T, A, R> collector):将Stream中的元素收集到容器中(如List、Set、Map等),是最常用的终端操作之一。Collectors工具类提供了大量预定义的Collector。
示例13:collect操作
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class CollectExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape", "apple");
// 收集到List
List<String> fruitList = fruits.stream()
.collect(Collectors.toList());
System.out.println("List: " + fruitList);
// 收集到Set(自动去重)
Set<String> fruitSet = fruits.stream()
.collect(Collectors.toSet());
System.out.println("Set: " + fruitSet);
// 收集到指定的集合(如LinkedList)
List<String> linkedList = fruits.stream()
.collect(Collectors.toCollection(java.util.LinkedList::new));
System.out.println("LinkedList: " + linkedList);
// 收集到Map(键为元素,值为长度)
Map<String, Integer> fruitLengthMap = fruits.stream()
.distinct() // 去重,避免键冲突
.collect(Collectors.toMap(
fruit -> fruit, // 键映射
String::length // 值映射
));
System.out.println("水果长度Map: " + fruitLengthMap);
}
}
计数(count)
count():返回Stream中元素的数量。
示例14:count操作
import java.util.Arrays;
import java.util.List;
public class CountExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");
// 统计元素总数
long total = fruits.stream().count();
System.out.println("元素总数:" + total); // 输出:4
// 统计长度大于5的元素数量
long longNamesCount = fruits.stream()
.filter(fruit -> fruit.length() > 5)
.count();
System.out.println("长度大于5的元素数量:" + longNamesCount); // 输出:2
}
}
匹配(anyMatch/allMatch/noneMatch)
- anyMatch(Predicate<T> predicate):判断是否至少有一个元素满足Predicate。
- allMatch(Predicate<T> predicate):判断是否所有元素都满足Predicate。
- noneMatch(Predicate<T> predicate):判断是否所有元素都不满足Predicate。
这些操作都是短路操作,一旦确定结果就会停止计算(如anyMatch找到一个匹配元素就返回true)。
示例15:匹配操作
import java.util.Arrays;
import java.util.List;
public class MatchExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(2, 4, 6, 8, 10);
// 是否存在偶数(实际上都是偶数)
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);
System.out.println("是否存在偶数:" + hasEven); // 输出:true
// 是否所有元素都是偶数
boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0);
System.out.println("是否所有元素都是偶数:" + allEven); // 输出:true
// 是否没有奇数
boolean noOdd = numbers.stream().noneMatch(n -> n % 2 != 0);
System.out.println("是否没有奇数:" + noOdd); // 输出:true
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
// 是否存在以"a"开头的单词
boolean hasStartWithA = words.stream().anyMatch(word -> word.startsWith("a"));
System.out.println("是否存在以'a'开头的单词:" + hasStartWithA); // 输出:true
// 是否所有单词长度都大于3
boolean allLongerThan3 = words.stream().allMatch(word -> word.length() > 3);
System.out.println("是否所有单词长度都大于3:" + allLongerThan3); // 输出:true
}
}
查找(findFirst/findAny)
- findFirst():返回Stream中的第一个元素(封装在Optional中)。
- findAny():返回Stream中的任意一个元素(封装在Optional中)。
findFirst()在顺序流中总是返回第一个元素,在并行流中也会尽量返回第一个元素,但findAny()在并行流中可能返回任意元素,性能更好。
示例16:查找操作
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class FindExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");
// 查找第一个元素
Optional<String> first = fruits.stream().findFirst();
first.ifPresent(fruit -> System.out.println("第一个元素:" + fruit)); // 输出:apple
// 查找任意一个长度大于5的元素
Optional<String> anyLong = fruits.stream()
.filter(fruit -> fruit.length() > 5)
.findAny();
anyLong.ifPresent(fruit -> System.out.println("长度大于5的元素:" + fruit)); // 可能是banana或orange
// 并行流中findAny()可能返回不同结果
System.out.println("—– 并行流查找 —–");
for (int i = 0; i < 5; i++) {
Optional<String> parallelAny = fruits.parallelStream()
.filter(fruit -> fruit.length() > 5)
.findAny();
parallelAny.ifPresent(fruit -> System.out.print(fruit + " "));
}
}
}
最值(max/min)
max(Comparator<T> comparator):根据比较器返回Stream中的最大值。
min(Comparator<T> comparator):根据比较器返回Stream中的最小值。
示例17:max与min操作
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
public class MaxMinExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6);
// 查找最大值
Optional<Integer> maxNumber = numbers.stream()
.max(Comparator.naturalOrder());
maxNumber.ifPresent(max -> System.out.println("最大值:" + max)); // 输出:9
// 查找最小值
Optional<Integer> minNumber = numbers.stream()
.min(Comparator.naturalOrder());
minNumber.ifPresent(min -> System.out.println("最小值:" + min)); // 输出:1
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
// 查找最长的单词
Optional<String> longestWord = words.stream()
.max(Comparator.comparingInt(String::length));
longestWord.ifPresent(word -> System.out.println("最长的单词:" + word)); // 输出:banana或orange(长度相同)
// 查找最短的单词
Optional<String> shortestWord = words.stream()
.min(Comparator.comparingInt(String::length));
shortestWord.ifPresent(word -> System.out.println("最短的单词:" + word)); // 输出:apple或grape(长度相同)
}
}
归约(reduce)
reduce():将Stream中的元素通过累加器(Accumulator)归约为单个值。有三种重载形式:
示例18:reduce操作
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class ReduceExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 1. 有初始值的求和(初始值为0)
int sumWithIdentity = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("求和(有初始值):" + sumWithIdentity); // 输出:15
// 2. 无初始值的求和(可能为空,返回Optional)
Optional<Integer> sumWithoutIdentity = numbers.stream()
.reduce(Integer::sum);
sumWithoutIdentity.ifPresent(sum -> System.out.println("求和(无初始值):" + sum)); // 输出:15
// 3. 求乘积
int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
System.out.println("乘积:" + product); // 输出:120
// 4. 拼接字符串
List<String> words = Arrays.asList("Hello", " ", "World", "!");
String sentence = words.stream()
.reduce("", String::concat);
System.out.println("拼接结果:" + sentence); // 输出:Hello World!
// 5. 并行流的归约(使用combiner)
List<Integer> largeNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int parallelSum = largeNumbers.parallelStream()
.reduce(
0, // 初始值
Integer::sum, // 累加器(线程内归约)
Integer::sum // 组合器(合并线程结果)
);
System.out.println("并行流求和:" + parallelSum); // 输出:55
}
}
注意:对于并行流,使用带combiner的reduce更高效,因为combiner可以合并多个线程的中间结果。
四、Stream的高级用法
掌握了Stream的基本操作后,我们可以学习一些更高级的用法,如并行流处理、Collectors的高级应用、Optional与Stream的结合等。
1. 并行流(Parallel Stream)
并行流是Stream API最强大的特性之一,它能够自动将数据分成多个片段,在多个线程上并行处理,最后合并结果。使用并行流无需编写任何多线程代码,只需调用parallel()方法(或直接使用parallelStream())。
示例19:并行流基础
import java.util.Arrays;
import java.util.List;
import java.util.stream.LongStream;
public class ParallelStreamExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape", "kiwi", "mango", "pineapple");
// 顺序流处理
long sequentialStart = System.currentTimeMillis();
fruits.stream()
.map(String::toUpperCase)
.forEach(s -> {
// 模拟耗时操作
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
});
long sequentialTime = System.currentTimeMillis() – sequentialStart;
System.out.println("顺序流处理时间:" + sequentialTime + "ms");
// 并行流处理
long parallelStart = System.currentTimeMillis();
fruits.parallelStream() // 等价于 fruits.stream().parallel()
.map(String::toUpperCase)
.forEach(s -> {
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
});
long parallelTime = System.currentTimeMillis() – parallelStart;
System.out.println("并行流处理时间:" + parallelTime + "ms");
}
// 测试并行流的性能优势(计算大量数据)
public static long parallelSum(long n) {
return LongStream.rangeClosed(1, n)
.parallel()
.sum();
}
}
输出结果(示例):
顺序流处理时间:723ms
并行流处理时间:215ms
可以看到,对于耗时操作或大量数据,并行流能显著提高处理速度。
并行流的注意事项
线程安全:并行流在多线程环境下执行,若操作共享变量,需确保线程安全(或使用无状态操作)。
// 错误示例:共享变量在并行流下不安全
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int[] sum = {0}; // 使用数组包装以允许在lambda中修改
numbers.parallelStream().forEach(n -> sum[0] += n); // 可能导致结果不正确
System.out.println("不安全的求和:" + sum[0]); // 结果可能不是15
正确的做法是使用reduce或collect等无状态操作:
// 正确:使用reduce求和
int safeSum = numbers.parallelStream().reduce(0, Integer::sum);
性能权衡:
- 并行流的优势在大数据量或复杂操作时才明显,小数据量可能因线程开销而更慢。
- 避免在并行流中使用forEachOrdered,它会强制顺序执行,抵消并行优势。
流的来源:
- ArrayList、数组等随机访问数据源更适合并行流(分割成本低)。
- LinkedList等非随机访问数据源不适合并行流(分割成本高)。
自定义并行度:
并行流的默认线程数等于CPU核心数,可通过ForkJoinPool修改:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "4"); // 设置并行度为4
2. Collectors的高级应用
Collectors工具类提供了丰富的收集器,除了基本的toList()、toSet(),还有分组、分区、聚合等高级功能。
分组(groupingBy)
groupingBy(Function<T, K> classifier):根据classifier将元素分组,返回Map<K, List<T>>。
示例20:分组操作
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
// 商品类
class Product {
private String name;
private String category;
private double price;
public Product(String name, String category, double price) {
this.name = name;
this.category = category;
this.price = price;
}
public String getName() { return name; }
public String getCategory() { return category; }
public double getPrice() { return price; }
@Override
public String toString() { return name + " (" + price + ")"; }
}
public class GroupingByExample {
public static void main(String[] args) {
List<Product> products = Arrays.asList(
new Product("笔记本电脑", "电子产品", 5999.99),
new Product("智能手机", "电子产品", 3999.99),
new Product("T恤", "服装", 99.99),
new Product("牛仔裤", "服装", 199.99),
new Product("篮球", "运动器材", 149.99)
);
// 1. 按类别分组
Map<String, List<Product>> byCategory = products.stream()
.collect(Collectors.groupingBy(Product::getCategory));
// 打印分组结果
byCategory.forEach((category, prods) -> {
System.out.println("类别:" + category);
prods.forEach(prod -> System.out.println(" " + prod));
});
// 2. 按价格区间分组(自定义分类器)
Map<String, List<Product>> byPriceRange = products.stream()
.collect(Collectors.groupingBy(product -> {
if (product.getPrice() < 200) return "低价";
else if (product.getPrice() < 1000) return "中价";
else return "高价";
}));
System.out.println("\\n按价格区间分组:");
byPriceRange.forEach((range, prods) -> {
System.out.println("价格区间:" + range);
prods.forEach(prod -> System.out.println(" " + prod));
});
}
}
输出结果:
类别:电子产品
笔记本电脑 (5999.99)
智能手机 (3999.99)
类别:服装
T恤 (99.99)
牛仔裤 (199.99)
类别:运动器材
篮球 (149.99)
按价格区间分组:
价格区间:低价
T恤 (99.99)
牛仔裤 (199.99)
篮球 (149.99)
价格区间:高价
笔记本电脑 (5999.99)
智能手机 (3999.99)
多级分组
groupingBy可以嵌套使用,实现多级分组:
// 先按类别分组,再按价格区间分组
Map<String, Map<String, List<Product>>> multiLevelGroup = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory, // 一级分组:类别
Collectors.groupingBy(product -> { // 二级分组:价格区间
if (product.getPrice() < 200) return "低价";
else if (product.getPrice() < 1000) return "中价";
else return "高价";
})
));
分区(partitioningBy)
partitioningBy(Predicate<T> predicate):根据Predicate将元素分为两组(满足条件和不满足条件),返回Map<Boolean, List<T>>。
示例21:分区操作
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class PartitioningByExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 按是否为偶数分区
Map<Boolean, List<Integer>> evenOddPartition = numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
System.out.println("偶数:" + evenOddPartition.get(true)); // 输出:[2, 4, 6, 8, 10]
System.out.println("奇数:" + evenOddPartition.get(false)); // 输出:[1, 3, 5, 7, 9]
List<Product> products = Arrays.asList(
new Product("笔记本电脑", "电子产品", 5999.99),
new Product("T恤", "服装", 99.99),
new Product("牛仔裤", "服装", 199.99),
new Product("篮球", "运动器材", 149.99)
);
// 按价格是否低于200分区
Map<Boolean, List<Product>> pricePartition = products.stream()
.collect(Collectors.partitioningBy(p -> p.getPrice() < 200));
System.out.println("\\n价格低于200的商品:");
pricePartition.get(true).forEach(System.out::println);
System.out.println("价格不低于200的商品:");
pricePartition.get(false).forEach(System.out::println);
}
}
聚合函数(summing/averaging/counting)
Collectors提供了多种聚合函数,用于对分组后的元素进行统计:
- summingInt(ToIntFunction<T> mapper):求和
- averagingInt(ToIntFunction<T> mapper):求平均值
- counting():计数
- maxBy(Comparator<T> comparator):求最大值
- minBy(Comparator<T> comparator):求最小值
- summarizingInt(ToIntFunction<T> mapper):生成包含总和、平均值、最值、数量的统计摘要
示例22:聚合操作
import java.util.Arrays;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
public class AggregationExample {
public static void main(String[] args) {
List<Product> products = Arrays.asList(
new Product("笔记本电脑", "电子产品", 5999.99),
new Product("智能手机", "电子产品", 3999.99),
new Product("T恤", "服装", 99.99),
new Product("牛仔裤", "服装", 199.99),
new Product("篮球", "运动器材", 149.99)
);
// 1. 按类别分组,计算每组商品的总价格
Map<String, Double> totalPriceByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
Collectors.summingDouble(Product::getPrice)
));
System.out.println("各类别总价格:" + totalPriceByCategory);
// 2. 按类别分组,计算每组商品的平均价格
Map<String, Double> avgPriceByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
Collectors.averagingDouble(Product::getPrice)
));
System.out.println("各类别平均价格:" + avgPriceByCategory);
// 3. 按类别分组,统计每组商品数量
Map<String, Long> countByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
Collectors.counting()
));
System.out.println("各类别商品数量:" + countByCategory);
// 4. 按类别分组,找出每组中价格最高的商品
Map<String, Optional<Product>> maxPriceProductByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
Collectors.maxBy(Comparator.comparingDouble(Product::getPrice))
));
System.out.println("各类别最高价商品:");
maxPriceProductByCategory.forEach((category, product) ->
product.ifPresent(p -> System.out.println(category + ": " + p))
);
// 5. 获取所有商品的价格统计摘要
DoubleSummaryStatistics priceStats = products.stream()
.collect(Collectors.summarizingDouble(Product::getPrice));
System.out.println("\\n价格统计摘要:");
System.out.println("总数量:" + priceStats.getCount());
System.out.println("总价格:" + priceStats.getSum());
System.out.println("平均价格:" + priceStats.getAverage());
System.out.println("最低价格:" + priceStats.getMin());
System.out.println("最高价格:" + priceStats.getMax());
}
}
字符串拼接(joining)
joining():将Stream中的字符串元素拼接起来,支持指定分隔符、前缀和后缀。
示例23:字符串拼接
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class JoiningExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");
// 直接拼接
String joined = fruits.stream().collect(Collectors.joining());
System.out.println("直接拼接:" + joined); // 输出:applebananaorangegrape
// 使用逗号分隔
String joinedWithComma = fruits.stream().collect(Collectors.joining(", "));
System.out.println("逗号分隔:" + joinedWithComma); // 输出:apple, banana, orange, grape
// 带前缀、后缀和分隔符
String joinedWithPrefixSuffix = fruits.stream()
.collect(Collectors.joining(", ", "Fruits: [", "]"));
System.out.println("带前缀后缀:" + joinedWithPrefixSuffix); // 输出:Fruits: [apple, banana, orange, grape]
// 对对象的某个字段进行拼接
List<Product> products = Arrays.asList(
new Product("笔记本电脑", "电子产品", 5999.99),
new Product("T恤", "服装", 99.99)
);
String productNames = products.stream()
.map(Product::getName)
.collect(Collectors.joining("、", "商品列表:", ""));
System.out.println(productNames); // 输出:商品列表:笔记本电脑、T恤
}
}
3. Optional与Stream的结合
Optional是Java 8引入的用于处理空值的容器类,与Stream结合使用可以优雅地处理可能为空的情况。
示例24:Optional与Stream
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class OptionalStreamExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("hello", "world", "java", "stream", null, "optional");
// 过滤掉null并查找长度大于5的第一个单词
Optional<String> longWord = words.stream()
.filter(word -> word != null) // 过滤null
.filter(word -> word.length() > 5)
.findFirst();
longWord.ifPresent(word -> System.out.println("长度大于5的单词:" + word)); // 输出:stream
// 转换为Optional并处理
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> max = numbers.stream().max(Integer::compare);
max.ifPresentOrElse(
value -> System.out.println("最大值:" + value),
() -> System.out.println("集合为空")
);
// 空集合的处理
List<Integer> emptyList = Arrays.asList();
Optional<Integer> emptyMax = emptyList.stream().max(Integer::compare);
emptyMax.ifPresentOrElse(
value -> System.out.println("最大值:" + value),
() -> System.out.println("集合为空") // 输出此句
);
}
}
4. 自定义Collector
除了Collectors提供的预定义收集器,我们还可以通过Collector.of()创建自定义收集器,满足特殊需求。
示例25:自定义Collector
import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
// 自定义收集器:将字符串收集到LinkedList,并转换为大写
class ToUpperCaseLinkedListCollector implements Collector<String, LinkedList<String>, LinkedList<String>> {
// 提供初始容器
@Override
public Supplier<LinkedList<String>> supplier() {
return LinkedList::new;
}
// 累加操作:将元素转换为大写并添加到容器
@Override
public BiConsumer<LinkedList<String>, String> accumulator() {
return (list, str) -> list.add(str.toUpperCase());
}
// 合并操作(并行流中使用)
@Override
public BinaryOperator<LinkedList<String>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
};
}
// 最终转换(此处无需转换,直接返回容器)
@Override
public Function<LinkedList<String>, LinkedList<String>> finisher() {
return Function.identity();
}
// 收集器特性
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(
Characteristics.IDENTITY_FINISH, // 表示finisher是恒等函数
Characteristics.CONCURRENT // 表示可以并行收集(需容器支持并发)
));
}
}
public class CustomCollectorExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("hello", "world", "java", "stream");
// 使用自定义收集器
LinkedList<String> upperCaseWords = words.stream()
.collect(new ToUpperCaseLinkedListCollector());
System.out.println("转换为大写的LinkedList:" + upperCaseWords); // 输出:[HELLO, WORLD, JAVA, STREAM]
}
}
自定义收集器适用于复杂的收集逻辑,简单场景使用预定义收集器即可。
五、Stream API性能分析
Stream API不仅代码简洁,其性能在大多数情况下也优于传统的迭代方式,尤其是在并行处理时。但性能表现受多种因素影响,了解这些因素有助于写出更高效的Stream代码。
1. Stream vs 传统迭代
在单线程环境下,对于简单操作,Stream的性能与传统for循环接近或略差(因Stream有额外的封装开销);但对于复杂操作或链式操作,Stream的性能往往更优,因为其内部实现经过了优化。
在多线程环境下,并行流通常远优于手动实现的多线程迭代,因为Stream API的并行机制(基于Fork/Join框架)能更高效地利用CPU资源。
性能测试示例:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.LongStream;
public class StreamPerformanceTest {
private static final int SIZE = 10_000_000; // 1000万数据
public static void main(String[] args) {
// 准备数据
List<Long> numbers = new ArrayList<>(SIZE);
for (long i = 0; i < SIZE; i++) {
numbers.add(i);
}
// 传统for循环求和
long loopStart = System.currentTimeMillis();
long loopSum = 0;
for (long num : numbers) {
loopSum += num;
}
long loopTime = System.currentTimeMillis() – loopStart;
System.out.println("传统for循环:" + loopTime + "ms,结果:" + loopSum);
// 顺序流求和
long sequentialStart = System.currentTimeMillis();
long sequentialSum = numbers.stream().mapToLong(Long::longValue).sum();
long sequentialTime = System.currentTimeMillis() – sequentialStart;
System.out.println("顺序流:" + sequentialTime + "ms,结果:" + sequentialSum);
// 并行流求和
long parallelStart = System.currentTimeMillis();
long parallelSum = numbers.parallelStream().mapToLong(Long::longValue).sum();
long parallelTime = System.currentTimeMillis() – parallelStart;
System.out.println("并行流:" + parallelTime + "ms,结果:" + parallelSum);
// 直接使用LongStream(性能最优)
long primitiveStart = System.currentTimeMillis();
long primitiveSum = LongStream.rangeClosed(0, SIZE – 1).sum();
long primitiveTime = System.currentTimeMillis() – primitiveStart;
System.out.println("LongStream:" + primitiveTime + "ms,结果:" + primitiveSum);
}
}
输出结果(示例):
传统for循环:46ms,结果:49999995000000
顺序流:38ms,结果:49999995000000
并行流:12ms,结果:49999995000000
LongStream:3ms,结果:49999995000000
可以看到:
- 顺序流性能略优于传统for循环(因Stream内部优化)。
- 并行流性能远优于单线程方式(充分利用多核CPU)。
- 基本类型Stream(如LongStream)性能最优,避免了装箱/拆箱开销。
2. 影响Stream性能的因素
数据源类型:
- 数组、ArrayList等随机访问数据源的Stream性能优于LinkedList等非随机访问数据源。
- 基本类型Stream(IntStream、LongStream等)性能优于对象Stream(避免装箱/拆箱)。
操作类型:
- 中间操作的复杂度:简单操作(如filter、map)的性能损耗小,复杂操作(如sorted)的性能损耗大。
- 短路操作(如anyMatch、limit)能提前终止流,性能更好。
并行流的使用:
- 数据量小或操作简单时,并行流的线程开销可能超过其带来的收益。
- 数据量大或操作复杂时,并行流能显著提升性能。
- 避免在并行流中使用同步操作或共享可变状态,否则会导致性能下降。
终端操作:
- count()、sum()等简单终端操作性能优于collect(Collectors.toList())等复杂操作。
- forEach的性能略低于collect(因forEach有副作用)。
3. 性能优化建议
六、Stream API实战案例
下面通过几个实际场景展示Stream API的强大功能,对比传统方式与Stream方式的代码差异。
案例1:数据过滤与转换
需求:从员工列表中筛选出部门为"研发部"且工资大于10000的员工,提取他们的姓名和邮箱,并按工资降序排序。
传统方式:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
class Employee {
private String name;
private String department;
private double salary;
private String email;
public Employee(String name, String department, double salary, String email) {
this.name = name;
this.department = department;
this.salary = salary;
this.email = email;
}
// getter方法
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
public String getEmail() { return email; }
}
class EmployeeInfo {
private String name;
private String email;
public EmployeeInfo(String name, String email) {
this.name = name;
this.email = email;
}
@Override
public String toString() {
return name + " (" + email + ")";
}
}
// 传统方式实现
public class TraditionalExample {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("张三", "研发部", 12000, "zhangsan@example.com"),
new Employee("李四", "市场部", 9000, "lisi@example.com"),
new Employee("王五", "研发部", 15000, "wangwu@example.com"),
new Employee("赵六", "研发部", 8000, "zhaoliu@example.com"),
new Employee("钱七", "财务部", 11000, "qianqi@example.com")
);
// 1. 筛选研发部且工资>10000的员工
List<Employee> filtered = new ArrayList<>();
for (Employee emp : employees) {
if ("研发部".equals(emp.getDepartment()) && emp.getSalary() > 10000) {
filtered.add(emp);
}
}
// 2. 按工资降序排序
Collections.sort(filtered, new Comparator<Employee>() {
@Override
public int compare(Employee o1, Employee o2) {
return Double.compare(o2.getSalary(), o1.getSalary()); // 降序
}
});
// 3. 提取姓名和邮箱
List<EmployeeInfo> result = new ArrayList<>();
for (Employee emp : filtered) {
result.add(new EmployeeInfo(emp.getName(), emp.getEmail()));
}
// 打印结果
for (EmployeeInfo info : result) {
System.out.println(info);
}
}
}
Stream方式:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("张三", "研发部", 12000, "zhangsan@example.com"),
new Employee("李四", "市场部", 9000, "lisi@example.com"),
new Employee("王五", "研发部", 15000, "wangwu@example.com"),
new Employee("赵六", "研发部", 8000, "zhaoliu@example.com"),
new Employee("钱七", "财务部", 11000, "qianqi@example.com")
);
List<EmployeeInfo> result = employees.stream()
// 筛选研发部且工资>10000
.filter(emp -> "研发部".equals(emp.getDepartment()) && emp.getSalary() > 10000)
// 按工资降序排序
.sorted((e1, e2) -> Double.compare(e2.getSalary(), e1.getSalary()))
// 提取姓名和邮箱
.map(emp -> new EmployeeInfo(emp.getName(), emp.getEmail()))
// 收集结果
.collect(Collectors.toList());
// 打印结果
result.forEach(System.out::println);
}
}
输出结果(两种方式相同):
王五 (wangwu@example.com)
张三 (zhangsan@example.com)
可以看到,Stream方式用不到10行代码完成了传统方式30多行代码的功能,且逻辑更清晰。
案例2:复杂数据聚合
需求:统计每个部门的员工人数、平均工资、最高工资和最低工资,并按部门名称排序。
Stream方式实现:
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
// 统计结果类
class DepartmentStats {
private String department;
private long count;
private double avgSalary;
private double maxSalary;
private double minSalary;
public DepartmentStats(String department, long count, double avgSalary, double maxSalary, double minSalary) {
this.department = department;
this.count = count;
this.avgSalary = avgSalary;
this.maxSalary = maxSalary;
this.minSalary = minSalary;
}
@Override
public String toString() {
return String.format(
"部门:%s,人数:%d,平均工资:%.2f,最高工资:%.2f,最低工资:%.2f",
department, count, avgSalary, maxSalary, minSalary
);
}
}
public class AggregationCase {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("张三", "研发部", 12000, "zhangsan@example.com"),
new Employee("李四", "市场部", 9000, "lisi@example.com"),
new Employee("王五", "研发部", 15000, "wangwu@example.com"),
new Employee("赵六", "研发部", 8000, "zhaoliu@example.com"),
new Employee("钱七", "财务部", 11000, "qianqi@example.com"),
new Employee("孙八", "市场部", 13000, "sunba@example.com")
);
// 按部门分组统计
Map<String, DepartmentStats> statsMap = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
// 使用TreeMap保证部门名称有序
TreeMap::new,
// 收集统计信息
Collectors.teeing(
Collectors.counting(), // 统计人数
Collectors.averagingDouble(Employee::getSalary), // 平均工资
Collectors.maxBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())), // 最高工资
Collectors.minBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())), // 最低工资
// 组合统计结果
(count, avg, maxEmp, minEmp) -> new DepartmentStats(
"", // 部门名称稍后设置
count,
avg,
maxEmp.map(Employee::getSalary).orElse(0),
minEmp.map(Employee::getSalary).orElse(0)
)
)
));
// 补充部门名称(因groupingBy的key是部门名称)
statsMap.forEach((dept, stats) -> stats.department = dept);
// 打印结果
statsMap.values().forEach(System.out::println);
}
}
注意:Collectors.teeing()是Java 12引入的功能,用于同时收集多个统计结果。
输出结果:
部门:财务部,人数:1,平均工资:11000.00,最高工资:11000.00,最低工资:11000.00
部门:研发部,人数:3,平均工资:11666.67,最高工资:15000.00,最低工资:8000.00
部门:市场部,人数:2,平均工资:11000.00,最高工资:13000.00,最低工资:9000.00
这个案例展示了Stream API处理复杂聚合需求的能力,通过groupingBy和teeing的组合,简洁地实现了多维度统计。
七、Stream API最佳实践与常见陷阱
1. 最佳实践
- 优先使用Stream API:对于集合处理,优先考虑使用Stream API,使代码更简洁、可读。
- 保持Stream操作链简洁:过长的操作链会降低可读性,可适当拆分或提取中间操作。
- 使用方法引用提高可读性:如String::toUpperCase比s -> s.toUpperCase()更简洁。
- 避免在Stream中使用副作用:forEach应仅用于终端操作(如打印),不应修改外部变量。
- 正确处理空值:使用filter(Objects::nonNull)过滤空值,避免NullPointerException。
- 选择合适的终端操作:如只需判断是否存在元素,使用anyMatch而非collect后检查大小。
- 并行流谨慎使用:确保操作线程安全,且数据量足够大以抵消线程开销。
- 利用基本类型Stream:减少装箱/拆箱开销,提高性能。
2. 常见陷阱
-
重复使用Stream:一个Stream只能被消费一次,再次使用会抛出IllegalStateException。
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println); // 错误:Stream已被关闭 -
忽略并行流的线程安全:并行流中操作共享变量可能导致数据不一致。
// 错误示例:共享变量在并行流下不安全
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
int[] sum = {0};
list.parallelStream().forEach(n -> sum[0] += n); // 结果可能不正确 -
过度使用collect(Collectors.toList()):很多时候无需收集为List,可直接使用Stream的终端操作。
// 低效:先收集为List再判断大小
boolean hasElements = list.stream().filter(...).collect(Collectors.toList()).size() > 0;// 高效:直接使用anyMatch
boolean hasElements = list.stream().filter(...).anyMatch(e -> true); -
误解map与flatMap的区别:map将一个元素转换为一个元素,flatMap将一个元素转换为多个元素。
-
在filter后使用map而非mapToXxx:对于基本类型转换,mapToInt等方法性能更优。
// 低效:会产生装箱开销
list.stream().filter(...).map(s -> s.length()).sum();// 高效:使用mapToInt
list.stream().filter(...).mapToInt(String::length).sum(); -
忽略Optional的处理:findFirst()、max()等返回Optional的方法,需正确处理空值情况。
八、总结
Java Stream API是Java 8引入的革命性特性,它彻底改变了集合数据的处理方式。通过声明式的函数式编程风格,Stream API使代码更简洁、更可读、更易维护,同时提供了强大的并行处理能力,充分利用多核CPU的优势。
本文从Stream的基本概念出发,详细介绍了Stream的创建方式、中间操作、终端操作,深入探讨了并行流、Collectors高级应用、自定义Collector等高级特性,并通过大量代码示例展示了Stream API在实际开发中的应用。同时,我们也分析了Stream的性能特点和最佳实践,帮助开发者避免常见陷阱。
掌握Stream API不仅能提高开发效率,更能培养函数式编程思维,使代码更加优雅和高效。无论是处理简单的集合过滤,还是复杂的数据聚合,Stream API都能成为开发者的得力工具。
随着Java版本的不断更新,Stream API也在持续进化(如Java 9的takeWhile/dropWhile,Java 12的teeing等),开发者应持续关注其新特性,以便更好地利用这一强大工具。
评论前必须登录!
注册