Appearance
Lambda 与 Stream
一、为什么需要 Lambda 表达式?
在 Java 8 之前,传递"行为"需要写大量样板代码——定义一个匿名内部类,即使只为了传递一行逻辑。Lambda 表达式让 Java 真正支持了函数式编程风格,核心价值在于:
- 减少样板代码:一行 Lambda 替代 5+ 行匿名内部类
- 行为参数化:将方法逻辑作为参数传递,代码更灵活、可复用
- 可读性提升:意图更清晰,代码即文档
- 并行处理的基础:Stream API 依赖 Lambda,使得并行编程变得简单
对比示例:匿名内部类 vs Lambda
java
// JDK 7 及之前:匿名内部类 —— 5 行代码只为传一个比较逻辑
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
// JDK 8 Lambda:一行搞定
Collections.sort(list, (a, b) -> a.compareTo(b));
// 更进一步:方法引用
list.sort(String::compareTo);java
// 线程创建对比
// 旧方式:6 行
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
}).start();
// Lambda:1 行
new Thread(() -> System.out.println("Hello")).start();二、Lambda 语法
2.1 基本语法
(参数列表) -> { 方法体 }| 形式 | 示例 | 说明 |
|---|---|---|
| 无参 | () -> System.out.println("Hi") | 空括号必须保留 |
| 单参无类型 | x -> x * 2 | 可省略括号和类型 |
| 单参有类型 | (String s) -> s.length() | 显式声明类型 |
| 多参 | (a, b) -> a + b | 括号必须保留 |
| 多行方法体 | (a, b) -> { int c = a + b; return c; } | 花括号和 return 必须 |
2.2 Lambda 的本质
Lambda 表达式不是匿名内部类的语法糖,它是通过 invokedynamic 指令在运行时动态生成实现类的。与匿名内部类关键区别:
| 维度 | 匿名内部类 | Lambda |
|---|---|---|
| 实现方式 | 编译时生成 $1.class 文件 | 运行时 invokedynamic 动态生成 |
this 指向 | 指向匿名内部类自身 | 指向外层类实例 |
| 作用域 | 可定义新变量,会遮蔽外层 | 不能定义与外部同名变量 |
| 性能 | 每次创建新对象 | JVM 优化后可复用 |
三、函数式接口
3.1 什么是函数式接口
函数式接口:有且仅有一个抽象方法的接口。可以用 @FunctionalInterface 注解标记,编译器会检查是否满足条件。
java
@FunctionalInterface
public interface MyFunction {
int apply(int a, int b); // 只有一个抽象方法
// 默认方法和静态方法不影响
default void log() {
System.out.println("Processing...");
}
static MyFunction identity() {
return (a, b) -> a;
}
}3.2 四大核心函数式接口
Java 8 在 java.util.function 包中内置了 43 个函数式接口,其中四个最核心:
| 接口 | 方法签名 | 用途 | 形象比喻 |
|---|---|---|---|
Function<T, R> | R apply(T t) | 输入 T,输出 R,转换 | 加工厂:原料进,产品出 |
Consumer<T> | void accept(T t) | 消费一个值,无返回 | 黑洞:吃进去,不吐出来 |
Supplier<T> | T get() | 无输入,提供值 | 供应商:要就给,从不问为什么 |
Predicate<T> | boolean test(T t) | 判断条件 | 法官:判定有罪/无罪 |
java
// Function:转换
Function<String, Integer> strLen = s -> s.length();
System.out.println(strLen.apply("Hello")); // 5
// 函数组合:andThen(先执行自己,再执行参数)和 compose(先执行参数,再执行自己)
Function<Integer, String> intToStr = i -> "Result: " + i;
Function<String, Integer> composed = strLen.andThen(intToStr); // 不合适,类型不匹配
// 正确示例:
Function<Integer, Integer> multiply10 = x -> x * 10;
Function<Integer, Integer> add2 = x -> x + 2;
Function<Integer, Integer> pipeline = multiply10.andThen(add2);
System.out.println(pipeline.apply(3)); // 3 * 10 + 2 = 32
// Consumer:消费
Consumer<String> printer = s -> System.out.println("打印: " + s);
Consumer<String> logger = s -> System.out.println("日志: " + s);
printer.andThen(logger).accept("Hello"); // 依次执行两个 Consumer
// Supplier:提供
Supplier<Double> random = Math::random;
System.out.println(random.get()); // 每次获取一个随机数
// Predicate:判断
Predicate<String> isEmpty = s -> s == null || s.isEmpty();
Predicate<String> isLong = s -> s.length() > 10;
System.out.println(isEmpty.negate().and(isLong).test("Hello World")); // true
// negate() 取反, and() 与, or() 或3.3 其他常用函数式接口
| 接口 | 方法签名 | 说明 |
|---|---|---|
BiFunction<T, U, R> | R apply(T t, U u) | 双参数 Function |
UnaryOperator<T> | T apply(T t) | 一元运算,继承 Function,输入输出同类型 |
BinaryOperator<T> | T apply(T t1, T t2) | 二元运算,继承 BiFunction |
BiConsumer<T, U> | void accept(T t, U u) | 双参数 Consumer |
BiPredicate<T, U> | boolean test(T t, U u) | 双参数 Predicate |
四、方法引用(Method Reference)
方法引用是 Lambda 的简化写法,当 Lambda 体只是调用一个已有方法时使用。语法:类名/对象名::方法名。
| 类型 | 语法 | 等价 Lambda | 示例 |
|---|---|---|---|
| 静态方法引用 | 类名::静态方法 | (a) -> 类名.静态方法(a) | Integer::parseInt |
| 实例方法引用(特定对象) | 对象::实例方法 | () -> 对象.实例方法() | System.out::println |
| 实例方法引用(任意对象) | 类名::实例方法 | (a, b) -> a.方法(b) | String::compareTo |
| 构造方法引用 | 类名::new | () -> new 类名() | ArrayList::new |
java
// 静态方法引用
Function<String, Integer> parser = Integer::parseInt;
// 等价于: Function<String, Integer> parser = s -> Integer.parseInt(s);
// 特定对象的实例方法引用
Consumer<String> printer = System.out::println;
// 等价于: Consumer<String> printer = s -> System.out.println(s);
// 任意对象的实例方法引用(第一个参数成为方法调用者)
Comparator<String> comp = String::compareTo;
// 等价于: Comparator<String> comp = (a, b) -> a.compareTo(b);
// 构造方法引用
Supplier<List<String>> listFactory = ArrayList::new;
// 等价于: Supplier<List<String>> listFactory = () -> new ArrayList<>();
// 带参数的构造方法引用
Function<Integer, ArrayList<String>> sizedFactory = ArrayList::new;
// 等价于: Function<Integer, ArrayList<String>> f = n -> new ArrayList<>(n);五、Stream API
Stream 是 Java 8 引入的数据处理抽象,不是数据结构,不存储数据。它像一个流水线,数据从源头流过,经过一系列中间操作,最终由终端操作收集结果。
5.1 Stream 的特性
[数据源] → [中间操作1] → [中间操作2] → [中间操作3] → [终端操作] → [结果]
集合/数组 filter() map() sorted() collect()
(惰性求值) (惰性求值) (惰性求值) (惰性求值) (触发计算)核心特性:
- 惰性求值:中间操作不会立即执行,只有在终端操作触发时才计算
- 链式调用:中间操作返回 Stream,可以链式组合
- 不可复用:Stream 只能消费一次,终端操作后 Stream 关闭
- 不修改数据源:所有操作返回新 Stream,原始数据不变
- 内部迭代:由 Stream API 管理迭代,而非显式 for 循环
5.2 创建 Stream
java
// 1. 从集合创建
Stream<String> stream1 = list.stream(); // 顺序流
Stream<String> stream2 = list.parallelStream(); // 并行流
// 2. 从数组创建
Stream<Integer> stream3 = Arrays.stream(new Integer[]{1, 2, 3});
Stream<String> stream4 = Stream.of("a", "b", "c");
// 3. 无限流(配合 limit 使用)
Stream<Integer> stream5 = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
Stream<Double> stream6 = Stream.generate(Math::random); // 随机数流
// 4. 基本类型流
IntStream intStream = IntStream.range(1, 10); // 1 到 9(不包含10)
IntStream intStream2 = IntStream.rangeClosed(1, 10); // 1 到 10(包含10)
// 5. 从文件创建
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
lines.forEach(System.out::println);
}5.3 中间操作(Intermediate Operations)
中间操作返回 Stream,惰性求值,不触发实际计算。
filter —— 过滤
java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Amy");
// 过滤出长度大于3的名字
List<String> result = names.stream()
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
// 结果: [Alice, Charlie, David]map —— 转换(一对一)
java
// 提取所有名字的长度
List<Integer> lengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
// 结果: [5, 3, 7, 5, 3]
// 提取对象属性
List<User> users = ...;
List<String> userNames = users.stream()
.map(User::getName)
.collect(Collectors.toList());flatMap —— 扁平化(一对多)
java
// 将嵌套列表拍平
List<List<String>> nested = Arrays.asList(
Arrays.asList("a", "b"),
Arrays.asList("c", "d", "e")
);
List<String> flat = nested.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
// 结果: [a, b, c, d, e]
// 经典场景:拆分单词
List<String> words = Arrays.asList("Hello World", "Java Stream");
List<String> letters = words.stream()
.flatMap(s -> Arrays.stream(s.split(""))) // 每个单词拆成字符
.distinct()
.collect(Collectors.toList());
// 结果: [H, e, l, o, W, r, d, J, a, v, S, t, m]distinct —— 去重
java
List<Integer> nums = Arrays.asList(1, 2, 2, 3, 3, 3);
List<Integer> unique = nums.stream()
.distinct()
.collect(Collectors.toList());
// 结果: [1, 2, 3]sorted —— 排序
java
// 自然排序
List<String> sorted = names.stream().sorted().collect(Collectors.toList());
// 自定义比较器
List<String> sortedByLen = names.stream()
.sorted((a, b) -> Integer.compare(a.length(), b.length()))
.collect(Collectors.toList());
// 结果: [Bob, Amy, Alice, David, Charlie]peek —— 调试(不改变元素)
java
// 查看流经每个元素的中间状态,常用于调试
List<String> result = names.stream()
.peek(s -> System.out.println("处理前: " + s))
.filter(s -> s.length() > 3)
.peek(s -> System.out.println("过滤后: " + s))
.collect(Collectors.toList());limit / skip —— 截取
java
Stream<Integer> ints = Stream.iterate(0, n -> n + 1);
// limit:取前 n 个
ints.limit(5).forEach(System.out::print); // 01234
// skip:跳过前 n 个
Stream.iterate(0, n -> n + 1)
.skip(5).limit(3)
.forEach(System.out::print); // 567
// 分页场景
int pageSize = 10;
int pageNumber = 2;
List<User> page = users.stream()
.skip((long) (pageNumber - 1) * pageSize)
.limit(pageSize)
.collect(Collectors.toList());5.4 终端操作(Terminal Operations)
终端操作触发实际计算,消费 Stream 并返回结果。
collect —— 收集(最常用)
java
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
Map<String, Integer> map = stream.collect(
Collectors.toMap(Function.identity(), String::length)
);forEach / forEachOrdered
java
// forEach:遍历,不保证顺序(并行流时)
stream.forEach(System.out::println);
// forEachOrdered:保证顺序(即使并行流)
stream.forEachOrdered(System.out::println);reduce —— 归约
java
// 求和
int sum = IntStream.range(1, 101).reduce(0, (a, b) -> a + b);
// 等价于
int sum2 = IntStream.range(1, 101).sum();
// 求最大值
Optional<Integer> max = Stream.of(1, 3, 5, 2, 4)
.reduce((a, b) -> a > b ? a : b);
// 拼接字符串
String concat = Stream.of("Hello", " ", "World")
.reduce("", (a, b) -> a + b);
// 结果: "Hello World"count
java
long count = stream.filter(s -> s.length() > 3).count();匹配操作
java
// anyMatch:至少有一个匹配
boolean hasLong = names.stream().anyMatch(s -> s.length() > 5); // true
// allMatch:全部匹配
boolean allLong = names.stream().allMatch(s -> s.length() > 3); // false
// noneMatch:一个都不匹配
boolean noneMatch = names.stream().noneMatch(s -> s.isEmpty()); // truefindFirst / findAny
java
// findFirst:返回第一个元素
Optional<String> first = names.stream()
.filter(s -> s.startsWith("A"))
.findFirst(); // Optional[Alice]
// findAny:返回任意一个元素(并行流更高效)
Optional<String> any = names.parallelStream()
.filter(s -> s.startsWith("A"))
.findAny(); // Optional[Alice] 或 Optional[Amy]5.5 Stream 完整示例:传统 vs Stream
java
// 需求:从用户列表中筛选出成年用户,提取姓名,按字母排序,取前3个
// 传统方式(JDK 7)
List<String> result = new ArrayList<>();
for (User user : users) {
if (user.getAge() >= 18) {
result.add(user.getName());
}
}
Collections.sort(result);
if (result.size() > 3) {
result = result.subList(0, 3);
}
for (String name : result) {
System.out.println(name);
}
// Stream 方式
users.stream()
.filter(u -> u.getAge() >= 18) // 筛选成年用户
.map(User::getName) // 提取姓名
.sorted() // 排序
.limit(3) // 取前3
.forEach(System.out::println); // 打印六、Collectors 详解
6.1 基础收集器
java
// toList
List<String> list = stream.collect(Collectors.toList());
// toSet
Set<String> set = stream.collect(Collectors.toSet());
// toCollection:指定集合类型
TreeSet<String> treeSet = stream.collect(Collectors.toCollection(TreeSet::new));
// joining:字符串拼接
String joined = stream.collect(Collectors.joining(", "));
// 带前缀后缀
String joined2 = stream.collect(Collectors.joining(", ", "[", "]"));
// 结果: "[Alice, Bob, Charlie]"6.2 toMap —— 收集为 Map
java
// 基本用法:key 是名字,value 是年龄
Map<String, Integer> map = users.stream()
.collect(Collectors.toMap(User::getName, User::getAge));
// 处理 key 冲突:保留后者
Map<String, Integer> map2 = users.stream()
.collect(Collectors.toMap(
User::getName,
User::getAge,
(existing, replacement) -> replacement // 冲突时用新值
));
// 指定 Map 类型
Map<String, Integer> map3 = users.stream()
.collect(Collectors.toMap(
User::getName,
User::getAge,
(e, r) -> e,
TreeMap::new // 指定 TreeMap
));6.3 groupingBy —— 分组
java
// 按年龄分组
Map<Integer, List<User>> byAge = users.stream()
.collect(Collectors.groupingBy(User::getAge));
// 分组后计数
Map<Integer, Long> countByAge = users.stream()
.collect(Collectors.groupingBy(User::getAge, Collectors.counting()));
// 分组后提取属性
Map<Integer, List<String>> namesByAge = users.stream()
.collect(Collectors.groupingBy(
User::getAge,
Collectors.mapping(User::getName, Collectors.toList())
));
// 多级分组:先按城市,再按性别
Map<String, Map<String, List<User>>> multiGroup = users.stream()
.collect(Collectors.groupingBy(
User::getCity,
Collectors.groupingBy(User::getGender)
));
// 分组统计
Map<String, DoubleSummaryStatistics> statsByCity = users.stream()
.collect(Collectors.groupingBy(
User::getCity,
Collectors.summarizingDouble(User::getSalary)
));6.4 partitioningBy —— 分区
分区是分组的一种特例,partition 只有 true 和 false 两组。
java
// 按是否成年分两组
Map<Boolean, List<User>> partitioned = users.stream()
.collect(Collectors.partitioningBy(u -> u.getAge() >= 18));
// 分区后统计
Map<Boolean, Long> count = users.stream()
.collect(Collectors.partitioningBy(
u -> u.getAge() >= 18,
Collectors.counting()
));
// 结果: {false=15, true=85}6.5 其他实用收集器
java
// summarizingInt:汇总统计(count, sum, min, average, max)
IntSummaryStatistics stats = users.stream()
.collect(Collectors.summarizingInt(User::getAge));
System.out.println("平均年龄: " + stats.getAverage());
System.out.println("最大年龄: " + stats.getMax());
// maxBy / minBy:按比较器取最大/最小
Optional<User> eldest = users.stream()
.collect(Collectors.maxBy(Comparator.comparingInt(User::getAge)));
// reducing:通用归约
Integer totalAge = users.stream()
.collect(Collectors.reducing(0, User::getAge, Integer::sum));七、并行流(Parallel Stream)
7.1 创建并行流
java
// 方式一:parallelStream()
list.parallelStream().filter(...).collect(...);
// 方式二:parallel()
list.stream().parallel().filter(...).collect(...);
// 方式三:并行转串行
list.parallelStream().sequential().filter(...).collect(...);7.2 底层原理
并行流底层使用 ForkJoinPool 的公共线程池 ForkJoinPool.commonPool(),默认线程数为 CPU 核心数 - 1。通过 RecursiveTask 将大任务不断二分(fork),直到子任务足够小,然后合并(join)结果。
7.3 何时使用并行流
| 适合并行流 | 不适合并行流 |
|---|---|
| 数据量大(10万+) | 数据量小(几千) |
| 计算密集(CPU 密集型) | IO 密集型 |
| 无状态、无副作用的操作 | 有状态操作(如 sorted、distinct) |
| 元素独立、无共享可变状态 | 依赖顺序的操作(如 findFirst) |
| 数据结构可高效拆分(ArrayList、数组) | 难以拆分的数据结构(LinkedList) |
7.4 常见陷阱
java
// 陷阱1:在并行流中修改共享变量(线程不安全)
int[] sum = {0};
IntStream.range(0, 10000).parallel().forEach(i -> sum[0] += i);
// sum[0] 的值不确定,每次运行结果不同!
// 正确做法:使用 reduce
int correctSum = IntStream.range(0, 10000).parallel().sum();
// 陷阱2:并行流中使用有状态的操作
// 以下代码在并行流中行为不可预测
List<Integer> list = new ArrayList<>();
IntStream.range(0, 100).parallel().forEach(list::add);
// list 的大小可能不是 100,元素顺序不确定
// 陷阱3:阻塞操作耗尽公共线程池
// 不要在 parallelStream 中执行 IO 或网络请求
// 这会阻塞公共线程池,影响整个 JVM 的其他并行流八、Optional
8.1 为什么需要 Optional?
null 是 Java 最臭名昭著的错误来源。Tony Hoare(null 的发明者)称其为"十亿美元的错误"。Optional 是一个容器类,它可能包含也可能不包含非 null 值,强制开发者显式处理"值不存在"的情况。
java
// 传统 null 检查 —— 防御式编程,层层嵌套
public String getCity(User user) {
if (user != null) {
Address address = user.getAddress();
if (address != null) {
City city = address.getCity();
if (city != null) {
return city.getName();
}
}
}
return "Unknown";
}
// Optional 链式调用 —— 扁平化,可读性高
public String getCity(User user) {
return Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.map(City::getName)
.orElse("Unknown");
}8.2 创建 Optional
java
// 创建非空 Optional —— 值为 null 时抛 NullPointerException
Optional<String> opt1 = Optional.of("Hello");
// 创建可空 Optional —— 值为 null 时返回 Optional.empty()
Optional<String> opt2 = Optional.ofNullable(maybeNull);
// 创建空 Optional
Optional<String> opt3 = Optional.empty();8.3 获取值
java
Optional<String> opt = Optional.of("Hello");
// get():获取值,若为空抛 NoSuchElementException(不推荐直接使用)
String val = opt.get(); // "Hello"
// orElse:有值返回值,无值返回默认值(无论有无值,默认值都会执行)
String val2 = opt.orElse(expensiveOperation()); // expensiveOperation 总会执行
// orElseGet:有值返回值,无值执行 Supplier(惰性求值,推荐)
String val3 = opt.orElseGet(() -> expensiveOperation()); // 仅在需要时执行
// orElseThrow:有值返回值,无值抛异常
String val4 = opt.orElseThrow(() -> new IllegalArgumentException("值不存在"));
// ifPresent:有值时执行操作
opt.ifPresent(v -> System.out.println("Value: " + v));8.4 转换与过滤
java
// map:转换值
Optional<Integer> length = Optional.of("Hello")
.map(String::length); // Optional[5]
// flatMap:防止嵌套 Optional
public class User {
public Optional<Address> getAddress() { ... }
}
Optional<User> user = Optional.of(new User());
Optional<String> city = user
.flatMap(User::getAddress) // 如果用 map 会得到 Optional<Optional<Address>>
.map(Address::getCity);
// filter:条件过滤
Optional<String> filtered = Optional.of("Hello")
.filter(s -> s.length() > 10); // 不满足条件,返回 Optional.empty()8.5 Optional 使用原则
| 推荐 | 不推荐 |
|---|---|
| 作为方法返回值,表示"可能没有结果" | 作为字段类型 |
| 用于链式调用避免 NPE | 作为方法参数 |
使用 orElseGet 获取默认值 | 在集合中使用(List<Optional<T>>) |
使用 ifPresent 执行副作用 | 直接调用 get() 而不检查 |
九、基本类型流(Primitive Streams)
Java 8 为三种基本类型提供了专门的 Stream:IntStream、LongStream、DoubleStream,避免装箱/拆箱开销。
java
// 创建基本类型流
IntStream intStream = IntStream.range(1, 100); // 1-99
IntStream intStream2 = IntStream.rangeClosed(1, 100); // 1-100
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);
// 基本类型特有方法
int sum = IntStream.range(1, 101).sum(); // 求和
OptionalDouble avg = IntStream.range(1, 101).average(); // 平均值
OptionalInt max = IntStream.range(1, 101).max(); // 最大值
IntSummaryStatistics stats = IntStream.range(1, 101).summaryStatistics();
// Stream 与基本类型流的转换
Stream<Integer> boxed = IntStream.range(1, 10).boxed(); // 装箱
IntStream unboxed = Stream.of(1, 2, 3).mapToInt(Integer::intValue); // 拆箱
LongStream longStream = Stream.of("1", "2").mapToLong(Long::parseLong);
// 数值范围转数组
int[] array = IntStream.range(1, 10).toArray();十、Stream 性能注意事项
java
// 1. 避免多次遍历 —— 一次终端操作触发一次遍历
// 反例:多次遍历
List<String> list = ...;
List<String> filtered = list.stream().filter(...).collect(toList()); // 遍历1
List<Integer> mapped = list.stream().map(...).collect(toList()); // 遍历2
// 3. 正确选择中间操作顺序
// 反例:先 map 后 filter —— map 对所有元素执行
list.stream().map(expensiveOp).filter(condition).collect(toList());
// 正例:先 filter 后 map —— 减少 map 执行次数
list.stream().filter(condition).map(expensiveOp).collect(toList());
// 4. 短路操作放在前面
// limit 放在前面可以减少后续操作量
list.stream().limit(10).filter(...).collect(toList());
// 5. 避免在 Stream 中抛受检异常
// 反例:编译错误
list.stream().map(s -> {
return new String(s.getBytes(), "UTF-8"); // UnsupportedEncodingException
});
// 正例:封装为运行时异常或使用工具方法
list.stream().map(s -> {
try {
return new String(s.getBytes(), "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
});十一、面试要点
Q1: Lambda 表达式和匿名内部类的区别?
Lambda 底层通过 invokedynamic 动态生成实现类,匿名内部类在编译时生成独立的 .class 文件。Lambda 中 this 指向外层类,匿名内部类中 this 指向内部类自身。Lambda 不能定义与外部同名的局部变量。Lambda 不能创建新的作用域,匿名内部类可以。
Q2: 什么是函数式接口?@FunctionalInterface 注解的作用?
函数式接口是有且仅有一个抽象方法的接口(可以有多个 default 或 static 方法)。@FunctionalInterface 是可选注解,作用是在编译期检查接口是否符合函数式接口定义,如果接口有多个抽象方法,编译器会报错。
Q3: Stream 的中间操作和终端操作有什么区别?
中间操作(filter, map, sorted 等)返回 Stream,惰性求值,不触发实际计算。终端操作(collect, forEach, reduce 等)触发计算,消费 Stream 并返回非 Stream 结果。Stream 在终端操作后关闭,不可复用。
Q4: map 和 flatMap 的区别?
| 维度 | map | flatMap |
|---|---|---|
| 映射关系 | 一对一 | 一对多 |
| 返回值 | Stream<R> | Stream<R>(将多个 Stream 拍平为一个) |
| 典型场景 | 提取对象属性 | 拆分字符串、展开嵌套集合 |
java
// map 返回 Stream<Stream<String>>(嵌套)
Stream<Stream<String>> nested = words.stream().map(w -> Arrays.stream(w.split("")));
// flatMap 返回 Stream<String>(拍平)
Stream<String> flat = words.stream().flatMap(w -> Arrays.stream(w.split("")));Q5: 并行流什么时候用?有什么坑?
适合 CPU 密集型、数据量大(10万+)、元素独立无共享状态、数据结构可高效拆分(ArrayList、数组)的场景。常见坑:并行流中修改共享变量导致线程不安全;有状态操作(sorted、distinct)性能下降;阻塞操作耗尽 ForkJoinPool 公共线程池;LinkedList 等难以拆分的数据结构性能差。
Q6: Optional 的 orElse 和 orElseGet 有什么区别?
orElse 的参数无论 Optional 是否有值都会执行,orElseGet 的 Supplier 参数仅在 Optional 为空时才执行。因此当默认值计算成本高时,应使用 orElseGet 实现惰性求值。
java
// orElse:expensiveOp() 始终执行
String result = opt.orElse(expensiveOp());
// orElseGet:expensiveOp() 仅在需要时执行
String result = opt.orElseGet(() -> expensiveOp());Q7: Collectors.groupingBy 和 partitioningBy 的区别?
groupingBy 按任意分类函数分组,可以产生多个组(Map 的 key 是分组键)。partitioningBy 是 groupingBy 的特例,只能按 boolean 条件分为 true 和 false 两组,返回 Map<Boolean, List<T>>。partitioningBy 效率更高,因为只需要两个桶。
Q8: Stream 是如何实现惰性求值的?
Stream 的中间操作(如 filter、map)不会立即执行,而是将操作记录在内部的一个双向链表(Sink 链)中。当终端操作触发时,数据从源头开始,依次经过每个中间操作的 Sink 节点,最终到达终端操作。这种设计使得短路操作(如 limit、findFirst)可以在满足条件时提前终止遍历,节省计算资源。
Q9: 为什么 Stream 中的 sorted 操作可能会影响性能?
sorted() 是无状态操作转为有状态操作的分界线。它需要先收集所有元素,然后排序,再继续流式处理。这意味着在无限流上不能使用 sorted,并且在大数据量并行流中,sorted 的排序合并开销很大。如果不需要全局排序,可以考虑使用 unordered() 解除排序约束以提升并行流性能。
