Java 完全指南 / 15 - Stream API:流操作、并行流、收集器、Optional
15 - Stream API:流操作、并行流、收集器、Optional
Stream 基础
import java.util.*;
import java.util.stream.*;
public class StreamBasics {
public static void main(String[] args) {
List<String> names = List.of("张三", "李四", "王五", "赵六", "钱七", "孙八");
// 1. 创建流
Stream<String> s1 = names.stream(); // 顺序流
Stream<String> s2 = names.parallelStream(); // 并行流
Stream<Integer> s3 = Stream.of(1, 2, 3, 4, 5); // 直接创建
Stream<Integer> s4 = Stream.iterate(0, n -> n + 2); // 无限流
Stream<Double> s5 = Stream.generate(Math::random); // 生成流
IntStream s6 = IntStream.range(1, 11); // 1..10
IntStream s7 = IntStream.rangeClosed(1, 10); // 1..10
// 2. 中间操作(惰性,返回新 Stream)
List<String> result = names.stream()
.filter(name -> name.length() > 1) // 过滤
.map(String::toUpperCase) // 映射
.sorted() // 排序
.limit(3) // 取前3个
.distinct() // 去重
.skip(1) // 跳过1个
.toList(); // 收集为 List
System.out.println(result);
// 3. 终端操作(触发执行)
long count = names.stream().filter(n -> n.startsWith("张")).count();
System.out.println("姓张的: " + count);
names.stream().forEach(System.out::println);
}
}
核心操作详解
filter / map / flatMap
public class StreamOperators {
record Employee(String name, String dept, double salary) {}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("张三", "技术部", 15000),
new Employee("李四", "技术部", 20000),
new Employee("王五", "市场部", 12000),
new Employee("赵六", "市场部", 18000),
new Employee("钱七", "技术部", 25000)
);
// filter —— 过滤
List<Employee> techEmps = employees.stream()
.filter(e -> e.dept().equals("技术部"))
.toList();
// map —— 一对一映射
List<String> names = employees.stream()
.map(Employee::name)
.toList();
// mapToDouble / mapToInt —— 基本类型流(避免装箱)
double totalSalary = employees.stream()
.mapToDouble(Employee::salary)
.sum();
double avgSalary = employees.stream()
.mapToDouble(Employee::salary)
.average()
.orElse(0);
// flatMap —— 一对多映射(扁平化)
List<List<Integer>> nested = List.of(
List.of(1, 2, 3),
List.of(4, 5),
List.of(6, 7, 8, 9)
);
List<Integer> flat = nested.stream()
.flatMap(Collection::stream)
.toList();
System.out.println(flat); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// peek —— 调试用(不改变流)
List<String> debugResult = names.stream()
.peek(n -> System.out.println("处理: " + n))
.filter(n -> n.length() > 1)
.peek(n -> System.out.println(" 通过过滤: " + n))
.toList();
}
}
收集器(Collectors)
import java.util.stream.Collectors;
public class CollectorDemo {
record Product(String category, String name, double price) {}
public static void main(String[] args) {
List<Product> products = List.of(
new Product("电子", "手机", 3999),
new Product("电子", "耳机", 299),
new Product("食品", "牛奶", 58),
new Product("食品", "面包", 12),
new Product("电子", "平板", 2999),
new Product("食品", "果汁", 15)
);
// toList / toSet
List<String> names = products.stream().map(Product::name).toList();
Set<String> categories = products.stream().map(Product::category).collect(Collectors.toSet());
// toMap
Map<String, Double> priceMap = products.stream()
.collect(Collectors.toMap(Product::name, Product::price));
// 分组 groupingBy
Map<String, List<Product>> grouped = products.stream()
.collect(Collectors.groupingBy(Product::category));
// {电子=[手机,耳机,平板], 食品=[牛奶,面包,果汁]}
// 分组 + 聚合
Map<String, Double> categoryTotal = products.stream()
.collect(Collectors.groupingBy(Product::category,
Collectors.summingDouble(Product::price)));
Map<String, Long> categoryCount = products.stream()
.collect(Collectors.groupingBy(Product::category, Collectors.counting()));
// 分区 partitioningBy(分为 true/false 两组)
Map<Boolean, List<Product>> expensive = products.stream()
.collect(Collectors.partitioningBy(p -> p.price() > 1000));
// joining 字符串拼接
String joined = products.stream()
.map(Product::name)
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(joined); // [手机, 耳机, 牛奶, 面包, 平板, 果汁]
// summarizing 统计
DoubleSummaryStatistics stats = products.stream()
.mapToDouble(Product::price)
.summaryStatistics();
System.out.printf("总数: %d, 总和: %.0f, 平均: %.0f, 最大: %.0f, 最小: %.0f%n",
stats.getCount(), stats.getSum(), stats.getAverage(),
stats.getMax(), stats.getMin());
}
}
收集器速查表
| 收集器 | 说明 | 示例 |
|---|---|---|
toList() | 收集为 List | .toList() |
toSet() | 收集为 Set | Collectors.toSet() |
toMap(keyFn, valFn) | 收集为 Map | Collectors.toMap(...) |
groupingBy(fn) | 按条件分组 | Collectors.groupingBy(...) |
partitioningBy(pred) | 按谓词分区 | Collectors.partitioningBy(...) |
joining(delim) | 字符串拼接 | Collectors.joining(",") |
counting() | 计数 | Collectors.counting() |
summingDouble(fn) | 求和 | Collectors.summingDouble(...) |
averagingDouble(fn) | 求平均 | Collectors.averagingDouble(...) |
maxBy(cmp) | 最大值 | Collectors.maxBy(...) |
minBy(cmp) | 最小值 | Collectors.minBy(...) |
reducing(init, fn) | 归约 | Collectors.reducing(0, ...) |
Optional
import java.util.Optional;
public class OptionalDemo {
public static void main(String[] args) {
// 创建 Optional
Optional<String> empty = Optional.empty();
Optional<String> present = Optional.of("hello");
Optional<String> nullable = Optional.ofNullable(null);
// 判断
System.out.println(present.isPresent()); // true
System.out.println(empty.isPresent()); // false
System.out.println(empty.isEmpty()); // true (JDK 11+)
// 获取值
String val1 = present.get(); // 直接获取(可能抛异常)
String val2 = present.orElse("default"); // 为空时返回默认值
String val3 = empty.orElse("default"); // "default"
String val4 = empty.orElseGet(() -> computeDefault()); // 延迟计算
// String val5 = empty.orElseThrow(); // 抛 NoSuchElementException
// 链式操作
String upper = Optional.of("hello")
.map(String::toUpperCase) // Optional<String>
.filter(s -> s.length() > 3) // Optional<String>
.orElse("HI");
System.out.println(upper); // HELLO
// flatMap —— 当映射函数返回 Optional 时使用
Optional<String> result = Optional.of("[email protected]")
.flatMap(OptionalDemo::extractDomain);
result.ifPresent(System.out::println); // email.com
// 实际用法:替代 null 检查
String city = Optional.ofNullable(getUser())
.map(User::getAddress)
.map(Address::getCity)
.orElse("未知城市");
}
record User(Address address) {}
record Address(String city) {}
static User getUser() { return null; }
static Optional<String> extractDomain(String email) {
int idx = email.indexOf('@');
return idx > 0 ? Optional.of(email.substring(idx + 1)) : Optional.empty();
}
static String computeDefault() {
System.out.println("计算默认值...");
return "computed";
}
}
Optional 使用规范
| 场景 | 正确做法 | 错误做法 |
|---|---|---|
| 返回值可能为空 | Optional<User> find() | User find() 返回 null |
| 判断存在后消费 | opt.ifPresent(...) | if (opt.isPresent()) opt.get() |
| 提供默认值 | opt.orElse(...) | opt.isPresent() ? opt.get() : ... |
| 字段类型 | 不要用 Optional 做字段 | Optional<String> name ❌ |
并行流
public class ParallelStreamDemo {
public static void main(String[] args) {
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000).boxed().toList();
// 顺序流
long t1 = System.nanoTime();
long sum1 = numbers.stream().mapToLong(Long::valueOf).sum();
long t2 = System.nanoTime();
// 并行流
long sum2 = numbers.parallelStream().mapToLong(Long::valueOf).sum();
long t3 = System.nanoTime();
System.out.println("顺序: " + sum1 + " 耗时: " + (t2 - t1) / 1_000_000 + "ms");
System.out.println("并行: " + sum2 + " 耗时: " + (t3 - t2) / 1_000_000 + "ms");
// 自定义并行度
var pool = java.util.concurrent.ForkJoinPool.commonPool();
System.out.println("并行度: " + pool.getParallelism());
}
}
| 场景 | 适合并行流 | 不适合并行流 |
|---|---|---|
| 大数据量 + 简单操作 | ✅ | |
| 小数据量 | ❌ 线程开销更大 | |
| 有状态操作(sorted) | ❌ 需要同步 | |
| 涉及共享可变状态 | ❌ 数据竞争 | |
| I/O 密集型 | ❌ 阻塞线程池 |
⚠️ 注意事项
- Stream 只能消费一次 — 重复使用会抛
IllegalStateException。 - 惰性求值 — 中间操作不执行,直到遇到终端操作。
- 避免副作用 — 不要在 Stream 操作中修改外部变量。
Optional不要get()前不检查 — 用orElse/ifPresent。
💡 技巧
Stream 转基本类型数组:
int[] arr = list.stream().mapToInt(Integer::intValue).toArray();多条件排序:
list.sort(Comparator.comparing(Student::getScore).reversed() .thenComparing(Student::getName));去重(按某字段):
list.stream().collect(Collectors.toMap(User::getId, u -> u, (a, b) -> a)) .values();
🏢 业务场景
- 数据转换: 从数据库查询结果转换为 DTO 列表。
- 统计报表: 按部门分组统计薪资总额、平均值。
- 数据清洗: 过滤无效数据、去重、格式转换。
- 配置处理: 从 properties 文件读取并分组配置项。