基础18-Java Stream API:高效处理集合数据

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与集合的区别

特性集合(Collection)流(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为基本类型(intlongdouble)提供了专门的Stream类型(IntStreamLongStreamDoubleStream),避免了自动装箱/拆箱的性能开销。

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]
    }
}

flatMapmap的区别: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)归约为单个值。有三种重载形式:

  1. reduce(T identity, BinaryOperator<T> accumulator):以identity为初始值,通过accumulator累加元素。
  2. reduce(BinaryOperator<T> accumulator):无初始值,返回Optional(可能为空)。
  3. reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner):用于并行流的归约,combiner用于合并多个线程的结果。

示例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

可以看到,对于耗时操作或大量数据,并行流能显著提高处理速度。

并行流的注意事项
  1. 线程安全:并行流在多线程环境下执行,若操作共享变量,需确保线程安全(或使用无状态操作)。

    // 错误示例:共享变量在并行流下不安全
    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
    

    正确的做法是使用reducecollect等无状态操作:

    // 正确:使用reduce求和
    int safeSum = numbers.parallelStream().reduce(0, Integer::sum);
    
  2. 性能权衡

    • 并行流的优势在大数据量或复杂操作时才明显,小数据量可能因线程开销而更慢。
    • 避免在并行流中使用forEachOrdered,它会强制顺序执行,抵消并行优势。
  3. 流的来源

    • ArrayList、数组等随机访问数据源更适合并行流(分割成本低)。
    • LinkedList等非随机访问数据源不适合并行流(分割成本高)。
  4. 自定义并行度
    并行流的默认线程数等于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性能的因素

  1. 数据源类型

    • 数组、ArrayList等随机访问数据源的Stream性能优于LinkedList等非随机访问数据源。
    • 基本类型Stream(IntStream、LongStream等)性能优于对象Stream(避免装箱/拆箱)。
  2. 操作类型

    • 中间操作的复杂度:简单操作(如filtermap)的性能损耗小,复杂操作(如sorted)的性能损耗大。
    • 短路操作(如anyMatchlimit)能提前终止流,性能更好。
  3. 并行流的使用

    • 数据量小或操作简单时,并行流的线程开销可能超过其带来的收益。
    • 数据量大或操作复杂时,并行流能显著提升性能。
    • 避免在并行流中使用同步操作或共享可变状态,否则会导致性能下降。
  4. 终端操作

    • count()sum()等简单终端操作性能优于collect(Collectors.toList())等复杂操作。
    • forEach的性能略低于collect(因forEach有副作用)。

3. 性能优化建议

  1. 优先使用基本类型Stream:如IntStreamLongStream,避免自动装箱/拆箱。
  2. 合理选择数据源:对于大量数据,优先使用数组或ArrayList。
  3. 适度使用并行流:仅在数据量大或操作复杂时使用,小数据量使用顺序流更高效。
  4. 避免不必要的装箱操作:如Stream.of(1, 2, 3)应改为IntStream.of(1, 2, 3)
  5. 使用短路操作:在可能的情况下,使用limit()anyMatch()等短路操作减少处理元素数量。
  6. 避免在Stream中修改共享变量:共享变量会导致线程安全问题,且影响并行性能。
  7. 复杂操作考虑拆分:将复杂的中间操作拆分为多个简单操作,Stream能更好地优化执行计划。

六、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处理复杂聚合需求的能力,通过groupingByteeing的组合,简洁地实现了多维度统计。

七、Stream API最佳实践与常见陷阱

1. 最佳实践

  • 优先使用Stream API:对于集合处理,优先考虑使用Stream API,使代码更简洁、可读。
  • 保持Stream操作链简洁:过长的操作链会降低可读性,可适当拆分或提取中间操作。
  • 使用方法引用提高可读性:如String::toUpperCases -> 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);
    
  • 误解mapflatMap的区别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等),开发者应持续关注其新特性,以便更好地利用这一强大工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值