JAVA

Java Stream 流:让代码更优雅的利器

转载:Java Stream 流:让代码更优雅的利器

在日常开发中,你是否经常遇到需要处理集合数据的场景?是否厌倦了写大量的 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. 1. 保持链式调用的可读性
  2. 2. 合理使用并行流
  3. 3. 注意中间操作和终端操作的区别
  4. 4. 适当使用 Optional 处理空值
  5. 5. 优先使用内置的收集器

  

七、总结


Java Stream 流为我们提供了一种更优雅、更函数式的数据处理方式。通过本文的学习,我们了解了:

  1. 1. Stream 流的基本概念和创建方式
  2. 2. 丰富的中间操作和终端操作
  3. 3. 并行处理的能力
  4. 4. 实际应用场景和最佳实践

掌握 Stream 流,可以让我们的代码更加简洁、可读性更强,同时也能提高开发效率。希望本文能帮助你在日常开发中更好地使用 Stream 流。