在日常开发中,你是否经常遇到需要处理集合数据的场景?是否厌倦了写大量的 for 循环?今天,让我们一起探索 Java Stream 流,这个让代码更优雅、更简洁的强大工具。
一、Stream 流是什么?
Stream 是 Java 8 引入的一个新特性,它允许我们以声明式的方式处理数据集合。想象一下,数据就像一条河流,我们可以在这条河流上进行各种操作,比如过滤、转换、聚合等。
// 传统方式
List<String> filteredList = new ArrayList<>();
for (String str : list) {
if (str.length() > 5) {
filteredList.add(str);
}
}
// Stream 方式
List<String> filteredList = list.stream()
.filter(str -> str.length() > 5)
.collect(Collectors.toList());
二、Stream 流的创建方式
在 Java 中,有多种方式可以创建 Stream 流。选择合适的方式取决于你的数据源类型和具体需求。
- 1. 从集合创建
从集合创建 Stream 是最常用的方式之一。Java 8 为 Collection 接口添加了 stream() 方法,可以方便地将集合转换为流。
List<String> list = Arrays.asList("Java", "Python", "Go");
Stream<String> stream = list.stream();
- 2. 从数组创建
对于数组类型的数据,可以使用 Arrays.stream() 方法创建流。这种方式特别适合处理基本类型数组。
String[] array = {"Java", "Python", "Go"};
Stream<String> stream = Arrays.stream(array);
// 处理基本类型数组
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);
- 3. 使用 Stream.of()
Stream.of() 方法可以接受任意数量的参数,创建一个包含这些元素的流。这种方式适合创建包含少量元素的流。
Stream<String> stream = Stream.of("Java", "Python", "Go");
// 也可以创建基本类型的流
IntStream intStream = IntStream.of(1, 2, 3, 4, 5);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);
- 4. 使用 Stream.iterate() 创建无限流
Stream.iterate() 方法可以创建无限流,它接受一个初始值和一个函数,通过不断应用这个函数来生成流中的元素。通常需要配合 limit() 方法来限制流的长度。
// 生成前10个偶数
Stream<Integer> stream = Stream.iterate(0, n -> n + 2)
.limit(10);
// 生成斐波那契数列
Stream<Long> fibonacci = Stream.iterate(
new long[]{0, 1},
f -> new long[]{f[1], f[0] + f[1]}
).map(f -> f[0])
.limit(10);
- 5. 使用 Stream.generate() 创建无限流
Stream.generate() 方法接受一个 Supplier 函数式接口,用于生成流中的元素。与 iterate() 不同,generate() 生成的每个元素都是独立的。
/ 生成随机数流
Stream<Double> randomStream = Stream.generate(Math::random)
.limit(5);
// 生成常量流
Stream<String> constantStream = Stream.generate(() -> "constant")
.limit(3);
- 6. 从文件创建流
Java 8 提供了 Files.lines() 方法,可以方便地从文件中创建流,每行文本作为流中的一个元素。
/ 读取文件内容
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
lines.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
- 7. 从字符串创建流
可以使用 Pattern.splitAsStream() 方法将字符串分割成流。
// 将字符串按空格分割成流
Stream<String> words = Pattern.compile("\\s+")
.splitAsStream("Java Python Go");
- 8. 从 Stream.Builder 创建流
Stream.Builder 提供了更灵活的方式来构建流,特别适合在循环中动态添加元素。
Stream.Builder<String> builder = Stream.builder();
builder.add("Java")
.add("Python")
.add("Go");
Stream<String> stream = builder.build();
- 9. 从 Optional 创建流
Java 9 引入了 Optional.stream() 方法,可以将 Optional 转换为流。
Optional<String> optional = Optional.of("Java");
Stream<String> stream = optional.stream();
- 10. 从迭代器创建流
可以使用 StreamSupport.stream() 方法从迭代器创建流。
Iterator<String> iterator = Arrays.asList("Java", "Python", "Go").iterator();
Stream<String> stream = StreamSupport.stream(
Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED),
false
);
三、Stream 流的操作类型
1. 中间操作(Intermediate Operations)
中间操作会返回一个新的 Stream,允许我们继续对数据进行处理。这些操作是惰性的,只有在执行终端操作时才会真正执行。
- 1.1 filter:过滤
filter 操作用于根据条件筛选元素,只保留满足条件的元素。它接收一个 Predicate 函数式接口作为参数,返回一个布尔值。
List<String> longWords = words.stream()
.filter(word -> word.length() > 5)
.collect(Collectors.toList());
- 1.2 map:转换
map 操作用于将流中的每个元素转换为另一个元素。它接收一个 Function 函数式接口作为参数,可以将一种类型转换为另一种类型。
List<Integer> lengths = words.stream()
.map(String::length)
.collect(Collectors.toList());
- 1.3 flatMap:扁平化
flatMap 操作用于将流中的每个元素转换为另一个流,然后将所有流合并为一个流。它特别适用于处理嵌套集合或需要展开的数据结构。
List<String> allWords = sentences.stream()
.flatMap(sentence -> Arrays.stream(sentence.split(" ")))
.collect(Collectors.toList());
- 1.4 distinct:去重
distinct 操作用于去除流中的重复元素,基于元素的 equals() 方法进行判断。它对于需要唯一值的场景非常有用。
List<String> uniqueWords = words.stream()
.distinct()
.collect(Collectors.toList());
- 1.5 sorted:排序
sorted 操作用于对流中的元素进行排序。它可以使用自然排序(元素必须实现 Comparable 接口)或自定义比较器。
List<String> sortedWords = words.stream()
.sorted()
.collect(Collectors.toList());
// 自定义排序
List<String> customSorted = words.stream()
.sorted((a, b) -> b.length() - a.length())
.collect(Collectors.toList());
- 1.6 peek:调试
peek 操作用于查看流中的元素,通常用于调试目的。它接收一个 Consumer 函数式接口,可以对每个元素执行操作而不改变流的内容。
List<String> result = words.stream()
.filter(word -> word.length() > 5)
.peek(word -> System.out.println("过滤后的单词: " + word))
.collect(Collectors.toList());
2. 终端操作(Terminal Operations)
终端操作会消耗流并产生一个结果或副作用。执行终端操作后,流就不能再被使用。
- 2.1 collect:收集
collect 操作用于将流中的元素收集到不同的集合中。它是最常用的终端操作之一,可以收集到 List、Set、Map 等集合中。
// 收集到List
List<String> list = stream.collect(Collectors.toList());
// 收集到Set
Set<String> set = stream.collect(Collectors.toSet());
// 收集到Map
Map<String, Integer> map = stream.collect(
Collectors.toMap(
word -> word,
String::length
)
);
- 2.2 reduce:归约
reduce 操作用于将流中的元素组合起来,产生一个单一的结果。它特别适用于求和、求积、找出最大值等聚合操作。
// 求和
int sum = numbers.stream()
.reduce(0, Integer::sum);
// 找出最长的单词
Optional<String> longest = words.stream()
.reduce((a, b) -> a.length() > b.length() ? a : b);
- 2.3 forEach:遍历
forEach 操作用于遍历流中的每个元素并执行操作。它通常用于打印结果或执行其他副作用操作。
words.stream()
.forEach(System.out::println);
- 2.4 anyMatch/allMatch/noneMatch:匹配
这些操作用于检查流中的元素是否满足特定条件:
- • anyMatch:检查是否存在至少一个元素满足条件
- • allMatch:检查是否所有元素都满足条件
- • noneMatch:检查是否没有元素满足条件
boolean hasLongWord = words.stream()
.anyMatch(word -> word.length() > 10);
boolean allLongWords = words.stream()
.allMatch(word -> word.length() > 5);
boolean noShortWords = words.stream()
.noneMatch(word -> word.length() < 3);
- 2.5 findFirst/findAny:查找
这些操作用于查找流中的元素:
- • findFirst:返回流中的第一个元素
- • findAny:返回流中的任意一个元素(在并行流中特别有用)
Optional<String> first = words.stream()
.findFirst();
Optional<String> any = words.stream()
.findAny();
3. 高级操作
- 3.1 分组操作(groupingBy)
groupingBy 操作用于将流中的元素按照指定的条件进行分组,返回一个 Map。
// 按部门分组
Map<String, List<Employee>> deptGroups = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
// 按部门分组并统计每个部门的员工数量
Map<String, Long> deptCount = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()
));
// 按部门分组并计算每个部门的平均工资
Map<String, Double> deptAvgSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
// 多级分组:先按部门分组,再按职位分组
Map<String, Map<String, List<Employee>>> deptAndPositionGroups = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(Employee::getPosition)
));
- 3.2 分区操作(partitioningBy)
partitioningBy 操作用于将流中的元素按照指定的条件分为两组,返回一个 Map<Boolean, List>。
// 将员工分为高薪和低薪两组(以10000为界限)
Map<Boolean, List<Employee>> salaryGroups = employees.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() > 10000));
// 分区并统计数量
Map<Boolean, Long> salaryCount = employees.stream()
.collect(Collectors.partitioningBy(
e -> e.getSalary() > 10000,
Collectors.counting()
));
- 3.3 统计操作
Stream 提供了丰富的统计操作,可以方便地进行数值计算。
// 计算工资的统计信息
DoubleSummaryStatisticssalaryStats= employees.stream()
.collect(Collectors.summarizingDouble(Employee::getSalary));
// 获取统计结果
doubleavgSalary= salaryStats.getAverage();
doublemaxSalary= salaryStats.getMax();
doubleminSalary= salaryStats.getMin();
doubletotalSalary= salaryStats.getSum();
longcount= salaryStats.getCount();
- 3.4 连接操作(joining)
joining 操作用于将流中的元素连接成字符串。
// 将所有员工名字用逗号连接
String allNames = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "));
// 使用前缀和后缀
String namesWithBrackets = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", ", "[", "]"));
- 3.5 自定义收集器
除了使用内置的收集器,我们还可以自定义收集器。
// 自定义收集器:收集所有员工的工资到List
List<Double> salaries = employees.stream()
.collect(Collectors.collectingAndThen(
Collectors.mapping(
Employee::getSalary,
Collectors.toList()
),
Collections::unmodifiableList
));
- 3.6 流转换操作
Stream 提供了多种流转换操作,可以处理不同类型的流。
// 转换为IntStream
IntStreamintStream= employees.stream()
.mapToInt(Employee::getAge);
// 转换为DoubleStream
DoubleStreamdoubleStream= employees.stream()
.mapToDouble(Employee::getSalary);
// 转换为LongStream
LongStreamlongStream= employees.stream()
.mapToLong(Employee::getId);
- 3.7 流合并操作
Stream 提供了合并多个流的操作。
// 合并两个流
Stream<String> stream1 = Stream.of("A", "B", "C");
Stream<String> stream2 = Stream.of("D", "E", "F");
Stream<String> combined = Stream.concat(stream1, stream2);
// 使用flatMap合并多个流
List<List<String>> lists = Arrays.asList(
Arrays.asList("A", "B"),
Arrays.asList("C", "D"),
Arrays.asList("E", "F")
);
List<String> flattened = lists.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
- 3.8 流限制和跳过操作
Stream 提供了限制和跳过元素的操作。
// 限制流中的元素数量
List<Employee> firstFive = employees.stream()
.limit(5)
.collect(Collectors.toList());
// 跳过前N个元素
List<Employee> skipFirstFive = employees.stream()
.skip(5)
.collect(Collectors.toList());
// 分页操作
intpageSize=10;
intpageNumber=2;
List<Employee> page = employees.stream()
.skip((pageNumber - 1) * pageSize)
.limit(pageSize)
.collect(Collectors.toList());
四、Stream 流的并行处理
1. 并行流创建
并行流可以充分利用多核处理器的优势,提高处理大量数据时的性能。使用 parallelStream() 方法可以创建并行流。
List<String> result = words.parallelStream()
.filter(word -> word.length() > 5)
.collect(Collectors.toList());
2. 性能考虑
在使用并行流时,需要注意以下几点:
- • 数据量要足够大,才能体现并行处理的优势
- • 操作应该是无状态的,避免共享可变状态
- • 操作应该是可并行的,避免顺序依赖
- • 考虑线程安全的问题
// 适合并行处理的场景
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.parallelStream()
.mapToInt(Integer::intValue)
.sum();
五、实际应用场景
1. 数据统计
// 计算平均年龄
double avgAge = employees.stream()
.mapToInt(Employee::getAge)
.average()
.orElse(0.0);
// 按部门分组统计
Map<String, Long> deptCount = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()
));
2. 数据转换
// 对象转换
List<EmployeeDTO> dtos = employees.stream()
.map(employee -> new EmployeeDTO(
employee.getId(),
employee.getName(),
employee.getSalary()
))
.collect(Collectors.toList());
3. 数据过滤和聚合
// 找出高薪员工
List<Employee> highPaid = employees.stream()
.filter(e -> e.getSalary() > 10000)
.sorted(Comparator.comparing(Employee::getSalary).reversed())
.collect(Collectors.toList());
4. 复杂数据处理
// 按部门分组,计算每个部门的工资统计信息
Map<String, DoubleSummaryStatistics> deptSalaryStats = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.summarizingDouble(Employee::getSalary)
));
// 找出每个部门工资最高的员工
Map<String, Optional<Employee>> deptHighestPaid = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparing(Employee::getSalary))
));
// 按工资范围分组统计
Map<String, Long> salaryRangeCount = employees.stream()
.collect(Collectors.groupingBy(
e -> {
doublesalary= e.getSalary();
if (salary < 5000) return"低薪";
if (salary < 10000) return"中薪";
return"高薪";
},
Collectors.counting()
));
六、Stream 流的最佳实践
- 1. 保持链式调用的可读性
- 2. 合理使用并行流
- 3. 注意中间操作和终端操作的区别
- 4. 适当使用 Optional 处理空值
- 5. 优先使用内置的收集器
七、总结
Java Stream 流为我们提供了一种更优雅、更函数式的数据处理方式。通过本文的学习,我们了解了:
- 1. Stream 流的基本概念和创建方式
- 2. 丰富的中间操作和终端操作
- 3. 并行处理的能力
- 4. 实际应用场景和最佳实践
掌握 Stream 流,可以让我们的代码更加简洁、可读性更强,同时也能提高开发效率。希望本文能帮助你在日常开发中更好地使用 Stream 流。