云计算百科
云计算领域专业知识百科平台

基础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为基本类型(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)归约为单个值。有三种重载形式:

  • reduce(T identity, BinaryOperator<T> accumulator):以identity为初始值,通过accumulator累加元素。
  • reduce(BinaryOperator<T> accumulator):无初始值,返回Optional(可能为空)。
  • 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

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

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

    // 错误示例:共享变量在并行流下不安全
    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:如IntStream、LongStream,避免自动装箱/拆箱。
  • 合理选择数据源:对于大量数据,优先使用数组或ArrayList。
  • 适度使用并行流:仅在数据量大或操作复杂时使用,小数据量使用顺序流更高效。
  • 避免不必要的装箱操作:如Stream.of(1, 2, 3)应改为IntStream.of(1, 2, 3)。
  • 使用短路操作:在可能的情况下,使用limit()、anyMatch()等短路操作减少处理元素数量。
  • 避免在Stream中修改共享变量:共享变量会导致线程安全问题,且影响并行性能。
  • 复杂操作考虑拆分:将复杂的中间操作拆分为多个简单操作,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处理复杂聚合需求的能力,通过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等),开发者应持续关注其新特性,以便更好地利用这一强大工具。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 基础18-Java Stream API:高效处理集合数据
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!