Stream 基础入门
Java Stream API 基础入门
前置知识
在开始本教程之前,建议您具备以下基础知识:
- Java 8+ 基础语法
- Java 集合框架基础
- Lambda 表达式基础
- 函数式接口概念
什么是 Stream API?
Stream API 是 Java 8 引入的一个重要特性,它提供了一种函数式编程的方式来处理集合数据。Stream 不是一种数据结构,而是一种从源生成的元素序列,支持各种数据处理操作。
Stream API 主要提供以下功能:
- 声明式编程:以声明式而非命令式的方式处理数据
- 链式操作:支持多个操作的链式调用,提高代码可读性
- 并行处理:轻松实现并行数据处理,提高性能
- 惰性计算:中间操作不会立即执行,而是等到终结操作被调用时才执行
Stream 与集合的区别
特性 | Stream | 集合 |
---|---|---|
数据存储 | 不存储数据,只是数据的视图 | 存储所有元素 |
计算方式 | 惰性计算 | 立即计算 |
操作类型 | 函数式操作(filter, map等) | 增删改查操作 |
迭代方式 | 内部迭代 | 外部迭代 |
消费特性 | 一次性消费 | 可重复访问 |
Stream 操作分类
Stream API 的操作可以分为两类:
1. 中间操作(Intermediate Operations)
中间操作返回一个新的 Stream,允许多个操作链接。这些操作是惰性的,不会立即执行,而是在终结操作被调用时才执行。
常见的中间操作包括:
filter
:过滤元素map
:转换元素flatMap
:扁平化嵌套流distinct
:去除重复元素sorted
:排序peek
:查看元素limit
:限制元素数量skip
:跳过元素
2. 终结操作(Terminal Operations)
终结操作会遍历流并生成一个结果或副作用。执行终结操作后,流就被消费掉了,不能再使用。
常见的终结操作包括:
forEach
:遍历每个元素count
:计算元素个数collect
:将流转换为其他形式reduce
:将流中元素组合起来min
/max
:查找最小/最大值findFirst
/findAny
:查找元素anyMatch
/allMatch
/noneMatch
:匹配判断
创建 Stream
Stream 可以通过多种方式创建:
1. 从集合创建
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamCreationExample {
public static void main(String[] args) {
// 从集合创建
List<String> list = Arrays.asList("Apple", "Banana", "Cherry");
Stream<String> streamFromList = list.stream();
// 使用流
streamFromList.forEach(System.out::println);
}
}
2. 从数组创建
import java.util.Arrays;
import java.util.stream.Stream;
public class StreamFromArrayExample {
public static void main(String[] args) {
// 从数组创建
String[] array = {"Apple", "Banana", "Cherry"};
Stream<String> streamFromArray = Arrays.stream(array);
// 使用流
streamFromArray.forEach(System.out::println);
}
}
3. 使用 Stream.of() 方法
import java.util.stream.Stream;
public class StreamOfExample {
public static void main(String[] args) {
// 使用 Stream.of()
Stream<String> streamOfElements = Stream.of("Apple", "Banana", "Cherry");
// 使用流
streamOfElements.forEach(System.out::println);
}
}
4. 使用 Stream.generate() 和 Stream.iterate()
import java.util.stream.Stream;
public class StreamGenerateExample {
public static void main(String[] args) {
// 使用 Stream.generate() 创建无限流
Stream<Double> randomNumbers = Stream.generate(Math::random);
// 限制元素数量
randomNumbers.limit(5).forEach(System.out::println);
// 使用 Stream.iterate() 创建无限流
Stream<Integer> evenNumbers = Stream.iterate(0, n -> n + 2);
// 限制元素数量
evenNumbers.limit(5).forEach(System.out::println);
}
}
5. 从文件创建
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
public class StreamFromFileExample {
public static void main(String[] args) {
try {
// 从文件创建流
Stream<String> lines = Files.lines(Paths.get("example.txt"));
// 使用流
lines.forEach(System.out::println);
// 关闭流
lines.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
6. 空流
import java.util.stream.Stream;
public class EmptyStreamExample {
public static void main(String[] args) {
// 创建空流
Stream<String> emptyStream = Stream.empty();
// 使用流
long count = emptyStream.count();
System.out.println("Empty stream count: " + count); // 输出: Empty stream count: 0
}
}
常用中间操作
1. filter - 过滤元素
filter
方法接收一个 Predicate 函数式接口,用于过滤流中的元素。
import java.util.Arrays;
import java.util.List;
public class FilterExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 过滤出偶数
numbers.stream()
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
}
}
2. map - 转换元素
map
方法接收一个 Function 函数式接口,用于将流中的元素转换为另一种类型。
import java.util.Arrays;
import java.util.List;
public class MapExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("Java", "Stream", "API");
// 转换为大写
words.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
// 获取字符串长度
words.stream()
.map(String::length)
.forEach(System.out::println);
}
}
3. flatMap - 扁平化嵌套流
flatMap
方法用于将嵌套的流扁平化为单个流。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class FlatMapExample {
public static void main(String[] args) {
List<List<Integer>> nestedNumbers = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
// 使用 flatMap 扁平化嵌套列表
nestedNumbers.stream()
.flatMap(List::stream)
.forEach(System.out::println);
}
}
一个更实际的例子:
import java.util.Arrays;
import java.util.List;
public class FlatMapPracticalExample {
public static void main(String[] args) {
List<String> sentences = Arrays.asList(
"Hello World",
"Java Stream API",
"Functional Programming"
);
// 将每个句子拆分为单词,并扁平化为单个单词流
sentences.stream()
.flatMap(sentence -> Arrays.stream(sentence.split(" ")))
.forEach(System.out::println);
}
}
4. distinct - 去除重复元素
distinct
方法用于去除流中的重复元素。
import java.util.Arrays;
import java.util.List;
public class DistinctExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 4, 4, 4);
// 去除重复元素
numbers.stream()
.distinct()
.forEach(System.out::println);
}
}
5. sorted - 排序
sorted
方法用于对流中的元素进行排序。
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class SortedExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Banana", "Apple", "Cherry", "Date");
// 自然排序(字母顺序)
System.out.println("自然排序:");
fruits.stream()
.sorted()
.forEach(System.out::println);
// 自定义排序(按长度)
System.out.println("\n按长度排序:");
fruits.stream()
.sorted(Comparator.comparing(String::length))
.forEach(System.out::println);
// 反向排序
System.out.println("\n反向排序:");
fruits.stream()
.sorted(Comparator.reverseOrder())
.forEach(System.out::println);
}
}
6. peek - 查看元素
peek
方法主要用于调试,它允许我们查看流中的元素,而不会改变流本身。
import java.util.Arrays;
import java.util.List;
public class PeekExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用 peek 查看过滤前后的元素
long count = numbers.stream()
.peek(n -> System.out.println("Before filter: " + n))
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("After filter: " + n))
.count();
System.out.println("Count: " + count);
}
}
7. limit - 限制元素数量
limit
方法用于限制流中元素的数量。
import java.util.stream.Stream;
public class LimitExample {
public static void main(String[] args) {
// 生成无限流,但只取前 5 个元素
Stream.iterate(1, n -> n + 1)
.limit(5)
.forEach(System.out::println);
}
}
8. skip - 跳过元素
skip
方法用于跳过流中的前 n 个元素。
import java.util.Arrays;
import java.util.List;
public class SkipExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 跳过前 5 个元素
numbers.stream()
.skip(5)
.forEach(System.out::println);
}
}
组合使用中间操作
Stream API 的强大之处在于可以组合多个操作,形成一个处理管道。
import java.util.Arrays;
import java.util.List;
public class CombinedOperationsExample {
public static void main(String[] args) {
List<String> words = Arrays.asList(
"Java", "Stream", "API", "Programming",
"Functional", "Lambda", "Collection", "Framework"
);
// 组合多个操作
words.stream()
.filter(word -> word.length() > 5) // 过滤长度大于 5 的单词
.map(String::toUpperCase) // 转换为大写
.sorted() // 排序
.limit(3) // 只取前 3 个
.forEach(System.out::println); // 打印
}
}
实际应用示例
1. 处理员工数据
完整示例代码
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Data
@AllArgsConstructor
class Employee {
private String name;
private String department;
private double salary;
private int age;
}
public class EmployeeStreamExample {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("John Doe", "IT", 75000, 30),
new Employee("Jane Smith", "HR", 65000, 28),
new Employee("Bob Johnson", "IT", 85000, 35),
new Employee("Alice Brown", "Finance", 90000, 32),
new Employee("Charlie Davis", "HR", 60000, 26),
new Employee("Eva Wilson", "Finance", 95000, 40),
new Employee("Frank Miller", "IT", 80000, 33)
);
// 1. 找出 IT 部门的员工
System.out.println("IT 部门员工:");
employees.stream()
.filter(e -> e.getDepartment().equals("IT"))
.forEach(e -> System.out.println(e.getName()));
// 2. 计算所有员工的平均薪资
double averageSalary = employees.stream()
.mapToDouble(Employee::getSalary)
.average()
.orElse(0);
System.out.println("\n平均薪资:" + averageSalary);
// 3. 按部门分组
Map<String, List<Employee>> employeesByDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
System.out.println("\n按部门分组:");
employeesByDepartment.forEach((dept, empList) -> {
System.out.println(dept + " 部门:");
empList.forEach(e -> System.out.println(" - " + e.getName()));
});
// 4. 找出薪资最高的员工
Employee highestPaid = employees.stream()
.max((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()))
.orElse(null);
System.out.println("\n薪资最高的员工:" + (highestPaid != null ? highestPaid.getName() : "无"));
// 5. 计算每个部门的平均薪资
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
System.out.println("\n各部门平均薪资:");
avgSalaryByDept.forEach((dept, avg) -> System.out.println(dept + ": " + avg));
// 6. 找出年龄大于 30 且薪资高于 80000 的员工
System.out.println("\n年龄大于 30 且薪资高于 80000 的员工:");
employees.stream()
.filter(e -> e.getAge() > 30 && e.getSalary() > 80000)
.forEach(e -> System.out.println(e.getName()));
}
}
2. 处理文本数据
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class TextProcessingExample {
public static void main(String[] args) {
String text = "Stream API is a powerful feature of Java that enables functional-style operations on streams of elements. "
+ "A stream is a sequence of objects that supports various methods which can be pipelined to produce the desired result.";
// 1. 将文本分割为单词并转换为小写
List<String> words = Arrays.stream(text.split("\\s+"))
.map(word -> word.replaceAll("[^a-zA-Z]", "").toLowerCase())
.filter(word -> !word.isEmpty())
.collect(Collectors.toList());
// 2. 统计单词出现频率
Map<String, Long> wordFrequency = words.stream()
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
));
System.out.println("单词频率统计:");
wordFrequency.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(10)
.forEach(entry -> System.out.println(entry.getKey() + ": " + entry.getValue()));
// 3. 找出最长的单词
String longestWord = words.stream()
.max((w1, w2) -> Integer.compare(w1.length(), w2.length()))
.orElse("");
System.out.println("\n最长的单词:" + longestWord);
// 4. 计算平均单词长度
double averageLength = words.stream()
.mapToInt(String::length)
.average()
.orElse(0);
System.out.println("\n平均单词长度:" + averageLength);
}
}
最佳实践
性能考虑
- 避免过度使用 Stream:简单操作使用传统循环可能更高效
- 注意装箱/拆箱开销:对于基本类型,优先使用专门的流(IntStream, LongStream, DoubleStream)
- 并行流慎用:小数据量或 IO 密集型操作不适合并行处理
- 避免副作用:保持操作的纯函数特性,不要修改外部状态
实施建议
- 保持可读性:适当换行和缩进,使流操作清晰可读
- 合理组合操作:尽早进行过滤操作,减少后续处理的元素数量
- 使用方法引用:当可能时,使用方法引用代替 Lambda 表达式
- 考虑使用 Collectors 工具类:熟悉 Collectors 类提供的各种收集器
常见问题
1. Stream 能否重复使用?
Stream 是一次性使用的,一旦执行了终结操作,就不能再使用该流。如果尝试重复使用,会抛出 IllegalStateException
。
import java.util.Arrays;
import java.util.stream.Stream;
public class StreamReuseExample {
public static void main(String[] args) {
Stream<String> stream = Arrays.asList("a", "b", "c").stream();
// 第一次使用
stream.forEach(System.out::println);
try {
// 尝试重复使用(会抛出异常)
stream.forEach(System.out::println);
} catch (IllegalStateException e) {
System.out.println("捕获异常:" + e.getMessage());
}
}
}
2. 如何处理 Stream 中的异常?
Stream 操作不直接支持抛出受检异常。可以使用以下方法处理:
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class StreamExceptionHandlingExample {
public static void main(String[] args) {
List<String> files = Arrays.asList("file1.txt", "file2.txt", "file3.txt");
// 方法 1:使用包装方法处理异常
files.stream()
.forEach(handleCheckedExceptionConsumer(file -> {
// 可能抛出受检异常的代码
readFile(file);
}));
// 方法 2:在 Lambda 内部处理异常
files.stream()
.forEach(file -> {
try {
readFile(file);
} catch (IOException e) {
throw new RuntimeException("Error reading file: " + file, e);
}
});
}
// 模拟读取文件的方法
private static void readFile(String file) throws IOException {
if (file.equals("file2.txt")) {
throw new IOException("Error reading file: " + file);
}
System.out.println("Reading file: " + file);
}
// 包装方法,将受检异常转换为运行时异常
private static <T> Consumer<T> handleCheckedExceptionConsumer(CheckedExceptionConsumer<T> consumer) {
return obj -> {
try {
consumer.accept(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
// 自定义函数式接口,允许抛出异常
@FunctionalInterface
interface CheckedExceptionConsumer<T> {
void accept(T t) throws Exception;
}
}
总结
本教程详细介绍了 Java Stream API 的基础知识,包括:
- ✅ Stream 的概念和特点:声明式编程、链式操作、惰性计算
- ✅ 创建 Stream 的多种方式:从集合、数组、生成器等
- ✅ 常用中间操作:filter、map、flatMap、distinct、sorted 等
- ✅ 组合使用中间操作:构建数据处理管道
- ✅ 实际应用示例:处理员工数据、文本处理
- ✅ 最佳实践和常见问题:性能考虑、实施建议和异常处理
下一步学习
希望这个教程对您有所帮助!如果您有任何问题,欢迎在评论区讨论。