在日常开发中,你是否经常遇到需要处理集合数据的场景?是否厌倦了写大量的 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 流。