JDK1.8 新特性 · 复习手册
一、接口 & 函数式接口
卡片 1|default 方法 —— 为什么接口可以有方法体了?
Q:JDK8 为什么要给接口加 default 方法?它和抽象类有什么区别?
A:
JDK8 之前,接口只能声明抽象方法。这意味着:只要在接口中新增一个方法,所有实现类全部编译报错,必须逐一修改。这在大型项目(比如 JDK 自身的 Collection 接口)中完全不可接受。
default 方法的本质是在接口中提供一个带方法体的默认实现,实现类可以选择:
- 不重写 → 自动继承默认实现
- 重写 → 用自己的逻辑覆盖
典型场景:
1// JDK 给 Collection 加了 stream(),如果不加 default,
2// 所有 List/Set/Queue 的子类全部要改,灾难级别。
3interface Collection<E> {
4 default Stream<E> stream() {
5 return StreamSupport.stream(spliterator(), false);
6 }
7}和抽象类的区别:
| 接口 + default | 抽象类 | |
|---|---|---|
| 继承数量 | 可实现多个 | 只能继承一个 |
| 成员变量 | 只能 public static final | 可以有实例变量 |
| 构造函数 | 不能有 | 可以有 |
this 含义 | 指向实现类实例 | 指向自身实例 |
🧠 钩子: default = "给你默认实现,你不用必须改"。接口轻量,抽象类重量。
❌ 常见反例
1// ❌ 错误:接口的 default 方法不能调用 Object 的方法(编译报错)
2interface Foo {
3 default String toString() { // ❌ 编译错误!
4 return "Foo";
5 }
6}
7// 原因:Object 的 toString/hashCode/equals 是"最终仲裁者",
8// 接口 default 不允许覆盖它们,否则继承链混乱。1// ❌ 错误:两个接口有同名 default 方法,实现类必须手动解决冲突
2interface A { default void say() { System.out.println("A"); } }
3interface B { default void say() { System.out.println("B"); } }
4
5class C implements A, B {
6 // ❌ 编译错误!必须重写 say()
7 // ✅ 正确做法:
8 @Override
9 public void say() {
10 A.super.say(); // 显式指定调用哪个
11 }
12}🔍 追问
Q1:接口的 static 方法能被实现类继承吗?
A: 不能。接口的静态方法只能通过 接口名.方法() 调用,实现类不会继承它。这和类的静态方法继承规则不同。
1interface MyInterface {
2 static void hello() { System.out.println("hello"); }
3}
4
5class MyClass implements MyInterface {
6 void test() {
7 // MyClass.hello(); // ❌ 编译错误
8 MyInterface.hello(); // ✅ 正确
9 }
10}Q2:为什么接口的 default 方法不能是 final 或 synchronized?
A: final 违背了"允许实现类重写"的初衷;synchronized 会引入状态依赖,但接口本身不持有状态(no instance fields),加锁没有意义且破坏接口的纯粹性。
卡片 2|函数式接口 —— 凭什么只准有一个抽象方法?
Q:什么是函数式接口?@FunctionalInterface 是不是必须的?
A:
函数式接口 = 有且仅有一个抽象方法的接口。这个限制为什么重要?因为 Lambda 表达式的语法本身就是"隐式实现那个唯一的抽象方法"——如果接口有两个抽象方法,编译器就不知道该实现哪个了。
@FunctionalInterface 不是必须的,它是一个编译期校验注解:
- 加上它 → 编译器帮你检查,违反规则直接编译报错
- 不加它 → 也能当函数式接口用,但容易手滑加方法而不自知
1// ✅ 合法(不加注解也能用于 Lambda)
2interface Converter {
3 String convert(Integer i);
4}
5
6// ✅ 推荐写法(加注解让编译器替你盯着)
7@FunctionalInterface
8interface Converter {
9 String convert(Integer i);
10 // 可以加 default 和 static 方法,不破坏"单一抽象方法"规则
11 default String defaultConvert(Integer i) { return String.valueOf(i); }
12}🧠 钩子: 只有一个抽象方法 = 函数式接口;+ 注解 = 编译器帮你盯着。
❌ 常见反例
1// ❌ 错误:两个抽象方法
2@FunctionalInterface
3interface Broken { // ❌ 编译报错!
4 void doA();
5 void doB();
6}
7
8// ❌ 错误:但很容易不经意犯错——比如继承了一个抽象方法
9interface Base {
10 void doBase();
11}
12
13@FunctionalInterface
14interface Child extends Base {
15 void doChild(); // ❌ 现在有两个抽象方法了!(Base 的 + 自己的)
16}1// ✅ 正确:继承的方法如果也是抽象方法,总数超过 1 就不行
2// ✅ 但如果继承的是 default 方法,那就不算抽象方法
3@FunctionalInterface
4interface Good extends Base2 {
5 // Base2 里有一个 default 方法 → 不算抽象方法
6 // 所以这里只有一个抽象方法 → 合法
7 void doSomething();
8}🔍 追问
Q1:Runnable 和 Callable 是函数式接口吗?
A: 都是。Runnable 只有 void run(),Callable 只有 V call()。JDK 已在源码中给它们加了 @FunctionalInterface。
Q2:函数式接口可以继承其他接口吗? A: 可以,但要保证继承后抽象方法总数 ≤ 1。如果父接口有 0 个抽象方法(全部是 default/static),子接口可以自己有 1 个;如果父接口有 1 个,子接口就不能再加了。
Q3:Comparator 有 compare 和 equals 两个抽象方法,为什么它还是函数式接口?
A: equals 签名和 Object.equals 一致,而 Object 的方法不计入抽象方法数量——因为每个实现类都会从 Object 继承实现。所以 Comparator 实际只有 compare 一个"有效抽象方法"。
卡片 3|@FunctionalInterface —— 加了有什么好处?
Q:加不加 @FunctionalInterface,运行时行为有区别吗?
A: 没有区别。它纯粹是编译期的"看门狗":
- 当你或同事在接口里无意加了第二个抽象方法 → 编译直接报错,而不是到运行时才发现 Lambda 绑定失败
- 它也是一种文档信号——告诉调用者"这个接口是给 Lambda 用的,别乱加方法"
不加注解的坏处:可能写了 Lambda 发现编译不过,排查半天才知道接口里多了个方法。
🧠 钩子: 它是"编译器看门狗" + "代码文档",不加也行但有心智负担。
❌ 常见反例
1// 看起来没问题,但编译通不过
2@FunctionalInterface
3interface Processor {
4 void process(int x);
5 // 三个月后,同事在这加了一行:
6 void process(String x); // ❌ 立马编译报错,帮你拦截
7 // 如果没有 @FunctionalInterface,这里悄无声息地多了一个抽象方法,
8 // 原来 Lambda 写法全部炸掉,排查到天荒地老
9}🔍 追问
Q1:能在一个非函数式接口上用 @FunctionalInterface 骗编译器吗?
A: 不能,编译器真实检查抽象方法数量,不是看注解。
Q2:@FunctionalInterface 能被继承吗?
A: 不会自动继承。子接口如果也只有一个抽象方法,可以加也可以不加。但建议加,保持一致性。
二、Lambda 表达式
卡片 4|Lambda 本质 —— 到底是不是匿名内部类的语法糖? ⭐ 高频
Q:Lambda 和匿名内部类是等价替换吗?底层实现有什么不同?
A:
不是等价替换。 虽然用起来像,底层完全不同:
| Lambda | 匿名内部类 | |
|---|---|---|
| 字节码 | 不生成独立的 .class 文件,运行时用 invokedynamic + LambdaMetafactory 动态生成 | 编译器生成 Outer$1.class |
| this 指向 | 外层类实例 | 匿名类自身 |
| 能用的接口 | 只能是函数式接口 | 任何接口/抽象类 |
| 变量遮蔽 | 不能定义与外层同名的局部变量 | 可以遮蔽 |
Lambda 的底层流程:
- 编译期生成一条
invokedynamic指令 - 运行时 JVM 调用
LambdaMetafactory.metafactory() - 通过
MethodHandle动态生成一个实现类(非.class文件,而是内存中的字节码)
这样做的好处:不污染磁盘空间、加载更快(不需要 ClassLoader 加载额外的类)、未来 JVM 可以持续优化而不需要重新编译源码。
1// 表面看差不多
2Runnable r1 = () -> System.out.println("Lambda");
3Runnable r2 = new Runnable() {
4 @Override
5 public void run() { System.out.println("匿名内部类"); }
6};
7
8// 但:
9// r1.getClass() → 类似 com.xxx.Main$$Lambda$1/0x000000...
10// r2.getClass() → 类似 com.xxx.Main$1
11// r1.getClass().getDeclaredFields() → 空
12// r2.getClass().getDeclaredFields() → 可能有外部类引用🧠 钩子: 匿名内部类 = 编译期生成 class 文件;Lambda = 运行时动态捏一个,更轻更快。
❌ 常见反例
1// ❌ 错误:用 Lambda 实现非函数式接口
2// 比如想用 Lambda 创建 Thread(其实可以,Runnable 是函数式接口)
3// 但如果自己写了个双方法接口:
4interface Worker {
5 void work();
6 void rest(); // 第二个抽象方法
7}
8
9// Worker w = () -> System.out.println("work"); // ❌ 编译错误!
10// 编译器不知道你实现的是 work 还是 rest1// ❌ 常见陷阱:Lambda 里 this 是外层类
2public class Outer {
3 void test() {
4 Runnable r = () -> System.out.println(this); // this = Outer 实例
5 r.run();
6 }
7}
8// 在匿名内部类里,this = 匿名类本身🔍 追问
Q1:Lambda 能序列化吗?
A: 可以,但需要把 Lambda 表达式赋值给一个同时实现 Serializable 的接口类型。比如 Runnable r = (Runnable & Serializable) () -> ...。
Q2:Lambda 表达式能访问外层方法的局部变量,那这个变量存哪了?
A: 如果 Lambda 在局部变量作用域外执行(比如被传到另一个线程),局部变量早已被栈回收。所以 Lambda 拷贝了一份值到自己的字段里,这就是为什么要求 final——拷贝后原变量改动了,Lambda 看不到,会导致不一致。
卡片 5|Lambda 语法省略规则 —— 什么时候能省,什么时候不能?
Q:详细的省略规则有哪些?哪些场景容易踩坑?
A:
可以省略的三种情况:
1// 1️⃣ 参数类型可以省略(编译器靠函数式接口推断)
2// 写完整版:
3Predicate<String> p1 = (String s) -> s.length() > 3;
4// 省略类型:
5Predicate<String> p2 = s -> s.length() > 3;
6
7// 2️⃣ 单参数可以省略括号
8Consumer<String> c = s -> System.out.println(s); // ✅
9// 但 0 参数或 2+ 参数必须加括号:
10Supplier<String> sup = () -> "hello"; // ✅ 必须括号
11BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; // ✅ 必须括号
12
13// 3️⃣ 单行函数体可以省略 {} 和 return
14Function<Integer, String> f1 = i -> { return String.valueOf(i); }; // 完整版
15Function<Integer, String> f2 = i -> String.valueOf(i); // 省略版 ✅
16
17// 多行必须写 {} 和 return:
18Function<Integer, String> f3 = i -> {
19 System.out.println("debug: " + i); // 多行
20 return String.valueOf(i);
21};🧠 钩子: 类型可省,单参可省括号,单行可省花括号 return。多行一个都不能省。
❌ 常见反例
1// ❌ 错误:多行却省略 return
2Function<Integer, String> f = i -> {
3 String s = "prefix_" + i;
4 s; // ❌ 这是表达式语句,不是 return!
5};
6// 编译器会报:not a statement 或 missing return statement
7
8// ❌ 错误:0 参数省略了括号
9// Supplier<String> s = -> "hello"; // ❌ 不行,必须 ()
10Supplier<String> s = () -> "hello"; // ✅
11
12// ❌ 错误:参数类型一部分省一部分不省
13// BiFunction<Integer, Integer, Integer> add = (int a, b) -> a + b; // ❌ 要么全写要么全省
14BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; // ✅1// ❌ 常见陷阱:省略花括号时无意中改变了语义
2// 你以为这样是 { print; return; }:
3Consumer<String> c = s -> System.out.println(s); // ✅ 正确
4
5// 但如果写成:
6// Runnable r = () -> System.out.println("hello"); // 这其实没有 return
7// 跟 Runnable 的 void run() 刚好匹配,所以可以
8// 但如果是 Function,必须有 return 值🔍 追问
Q1:为什么"类型全写"和"类型全省"不能混着来?
A: 这是 Java 语言规范的设计决定——类型推断是"全有或全无"的,要么根据函数式接口全部推断,要么你全部显式声明。部分推断会引入模糊性,比如 (Integer a, b) 里 b 的类型应该是什么?从上下文推断一个却不推断另一个,规则会更复杂。
Q2:Lambda 参数能用 var 吗?
A: JDK11+ 支持 (var a, var b) -> a + b,效果和省略类型一样,但 var 可以在参数上加注解(如 (@NonNull var s) -> ...)。
卡片 6|Lambda vs 匿名内部类 —— 什么时候该用哪个?
Q:什么场景下必须用匿名内部类而不能用 Lambda?
A:
以下场景 Lambda 做不到:
1// 1️⃣ 接口有多个抽象方法(非函数式接口)
2// 必须用匿名内部类:
3WindowListener listener = new WindowListener() {
4 @Override public void windowOpened(WindowEvent e) {}
5 @Override public void windowClosing(WindowEvent e) {}
6 // ... 其他方法
7};
8
9// 2️⃣ 需要自己的实例状态(字段)
10// Lambda 里不能声明实例字段:
11Runnable r = new Runnable() {
12 private int count = 0; // ✅ 匿名内部类可以
13 @Override
14 public void run() {
15 count++;
16 }
17};
18// Lambda 写法拿不到 this 的字段
19
20// 3️⃣ 需要 this 指向自己(比如用在递归或自引用场景)
21// 匿名内部类:this = 自己
22// Lambda:this = 外层类
23
24// 4️⃣ 创建抽象类的实例
25// 抽象类不能用于 Lambda(Lambda 只认接口):
26ActionListener l = new ActionListener() { // ✅ 匿名内部类
27 @Override
28 public void actionPerformed(ActionEvent e) {}
29};🧠 钩子: 多方法接口 / 需要自己的字段 / 需要自己的 this / 抽象类 → 只能用匿名内部类。
❌ 常见反例
1// ❌ 经典错误:在 Lambda 里用 this 引用自己
2// 场景:一个按钮点击后把自己 disable
3Button btn = new Button();
4btn.setOnAction(e -> {
5 // this 不是 btn!this 是外层类!
6 // this.setDisable(true); // ❌ 编译错误 or 错对象
7 btn.setDisable(true); // ✅ 直接引用外部变量
8});
9
10// 对比匿名内部类:
11btn.setOnAction(new EventHandler<ActionEvent>() {
12 @Override
13 public void handle(ActionEvent event) {
14 // this 指向匿名内部类,也不是 btn!
15 // 所以这个场景下两者 this 都不指向 btn,需要显式引用外部变量
16 }
17});🔍 追问
Q1:Lambda 和匿名内部类哪个性能更好?
A: Lambda 通常更好:不生成 .class 文件(减少类加载开销),invokedynamic 允许 JVM 未来优化。匿名内部类编译后就固定了。但差异在绝大多数场景微乎其微,更关键的是可读性。
Q2:为什么 Lambda 不能有自己独立的 this?
A: 设计目标就是让它感觉像"普通代码块"而不是"一个对象"。如果 this 指向 Lambda 自身,访问外层类的成员就要写 Outer.this.field,这和设计哲学"简洁"矛盾。
三、方法引用 ::
卡片 7|四种方法引用 —— 一招吃透 ::
Q::: 的四种形式分别怎么用?背后的规则是什么?
A:
方法引用的核心规则:参数列表和返回值类型必须与函数式接口的抽象方法匹配。
| # | 形式 | 语法 | Lambda 等价写法 | 参数怎么来的 |
|---|---|---|---|---|
| ① | 静态方法 | ClassName::staticMethod | (x) -> ClassName.staticMethod(x) | 全部参数透传 |
| ② | 特定实例 | instance::method | (x) -> instance.method(x) | 全部参数透传 |
| ③ | 类的实例方法 | ClassName::instanceMethod | (obj, x) -> obj.instanceMethod(x) | 第一个参数变成调用者 |
| ④ | 构造器 | ClassName::new | (x) -> new ClassName(x) | 全部参数透传 |
第③种最反直觉,多给一个例子:
1// ③ 类的实例方法引用:第一个参数成为方法调用者
2// 函数式接口:BiFunction<String, String, Integer>
3// 抽象方法:Integer apply(String t, String u)
4// 方法引用:String::indexOf
5// 效果:(t, u) -> t.indexOf(u) ← t 成了"调用者",u 是参数
6
7BiFunction<String, String, Integer> f = String::indexOf;
8int result = f.apply("Hello", "el"); // = "Hello".indexOf("el") = 11// 另一个经典例子
2// Stream 中:names.stream().map(String::toUpperCase)
3// map 的参数是 Function<String, String>,即 String apply(String s)
4// String::toUpperCase 是"类的实例方法引用",
5// 接收一个 String(即 s),调用 s.toUpperCase()
6List<String> upper = names.stream().map(String::toUpperCase).collect(...);🧠 钩子: ③最特殊——"类的实例方法引用",第一个参数变调用者,后面参数传入方法。
❌ 常见反例
1// ❌ 错误:参数不匹配
2// Predicate<String> 的 test 方法:boolean test(String s)
3// String::isEmpty 是 boolean isEmpty()(无参数)
4// 签名匹配 → 可以用!因为 String::isEmpty 的隐含参数就是调用者本身
5Predicate<String> p = String::isEmpty; // ✅ 其实合法!等价于 s -> s.isEmpty()
6
7// ❌ 错误:返回类型不匹配
8// Consumer<String> 的 accept 方法:void accept(String s)
9// String::toUpperCase 返回 String,不是 void
10// Consumer<String> c = String::toUpperCase; // ❌ 编译错误!
11// 应该用 Function<String, String> f = String::toUpperCase;1// ❌ 错误:构造器引用匹配错了
2// Supplier<Person> 的 get():无参数,返回 Person
3Supplier<Person> s = Person::new; // ✅ 调用 Person() 无参构造
4
5// Function<String, Person> 的 apply(String):一个参数,返回 Person
6Function<String, Person> f = Person::new; // ✅ 调用 Person(String name)
7
8// BiFunction<String, Integer, Person>:两个参数
9BiFunction<String, Integer, Person> bf = Person::new; // ✅ 调用 Person(String, Integer)
10// 构造函数的重载选择完全由函数式接口的参数列表决定!🔍 追问
Q1:为什么 System.out::println 能匹配 Consumer<String>?
A: Consumer<String> 的抽象方法是 void accept(String s)。而 System.out 是 PrintStream 实例,println(String) 接收 String 返回 void。完美匹配。"特定实例"形式自动把 accept 的参数传给 println。
Q2:方法引用可以串联吗?
A: 不能直接 A::b::c。但可以组合函数式接口来实现:Function<String, String> f = ((Function<String, Integer>) Integer::parseInt).andThen(String::valueOf)。
Q3:如果构造器有两个版本(有参/无参),ClassName::new 怎么选?
A: 由函数式接口决定。Supplier<T> → 无参构造;Function<A, T> → 单参构造;BiFunction<A, B, T> → 双参构造。编译器自动推断。
四、Lambda 作用域
卡片 9|final 限制 —— 为什么不能改局部变量? ⭐ 高频
Q:详细的 final 限制是什么?"effectively final" 怎么判断?
A:
规则: Lambda 体内引用外部局部变量时,该变量必须是 final 或 effectively final(虽然没有显式声明 final,但赋值后从未改变)。
"effectively final" 的判断标准很简单:
- 变量只被赋值一次(包括声明时赋值)→ 符合
- 声明后又被修改 → 不符合,即使从没在 Lambda 中使用也会报错(但 Lambda 不用它时不会报,编译器只在引用处检查)
1// ✅ effectively final
2int x = 10;
3Runnable r = () -> System.out.println(x);
4
5// ❌ 不是 effectively final
6int y = 10;
7y = 20; // 重新赋值了
8// Runnable r2 = () -> System.out.println(y); // ❌ 编译错误为什么有这个限制?
Lambda 可能比它所处的栈帧活得久(比如被提交到线程池)。当 Lambda 执行时,原来的局部变量早已被栈回收。所以 Java 在 Lambda 对象创建时拷贝一份值到 Lambda 的字段里。如果原变量后续被改动,拷贝的值和原变量就不一致了——这就是"必须 final"的根本原因:拷贝后原变量不改,一致性才能保证。
🧠 钩子: 局部变量会在 Lambda 创建时被拷贝走。拷贝的值和原来的变量必须保持一致 → 原变量不能改 → final。
❌ 常见反例
1// ❌ 经典错误:在循环里用 Lambda
2for (int i = 0; i < 3; i++) {
3 // Runnable r = () -> System.out.println(i); // ❌ 编译错误!
4 // i 不是 effectively final(i++ 了两次)
5}
6
7// ✅ 正确做法:拷贝一份
8for (int i = 0; i < 3; i++) {
9 int copy = i; // copy 是 effectively final
10 Runnable r = () -> System.out.println(copy); // ✅
11}1// ❌ 错误:数组元素的引用问题(初学者最常踩的坑)
2int[] sum = {0};
3Arrays.asList(1, 2, 3).forEach(n -> sum[0] += n); // ✅ 合法!
4// 等等,不是说不能改吗?注意:sum 这个引用没有变(仍是同一个数组),
5// 变的只是数组的内容。final 限制的是"变量本身",不是"变量指向的对象内部状态"。
6// 这其实是个坏实践——如果你在并行流里这么做,就会出现竞态条件!1// ❌ 并行流中的共享可变状态(实际开发中出 bug 的常见原因)
2int[] total = {0};
3list.parallelStream().forEach(n -> total[0] += n);
4// ⚠️ 竞态条件!结果不确定。正确做法:用 reduce 或 AtomicInteger🧠 钩子: final 限制的是变量引用不能变,不是指向的对象不能变。但改对象内部状态在并行流里是危险的。
🔍 追问
Q1:为什么 Lambda 能改成员变量却没有这个限制?
A: 成员变量和对象的生命周期绑定,Lambda 只要持有 this 引用就能通过它读/写堆上的成员变量。不存在"栈被回收"的问题。
Q2:如果 Lambda 不在当前线程执行,改了成员变量为什么没问题? A: 有可能有问题(竞态条件),但这是并发安全问题,不是语言学上的限制。局部变量的 final 限制是语言规范层面的强制安全;成员变量的并发安全由开发者自己负责(加锁、volatile、AtomicXXX)。
卡片 10|成员变量 vs 局部变量 —— 为什么区别对待?
Q:详细对比 Lambda 中访问成员变量和局部变量的不同?
A:
1public class ScopeDemo {
2 private int member = 10; // 成员变量
3 private static int staticVar = 20; // 静态变量
4
5 public void test() {
6 int local = 30; // 局部变量
7
8 Runnable r = () -> {
9 System.out.println(member); // ✅ 可读
10 member++; // ✅ 可写(但要处理并发问题)
11 System.out.println(staticVar); // ✅ 可读
12 staticVar++; // ✅ 可写
13 System.out.println(local); // ✅ 可读
14 // local++; // ❌ 不可写!不是 effectively final
15 };
16 }
17}根本原因: 变量存在哪决定了规则。
- 局部变量 = 栈上 → Lambda 拷贝值到自己字段,原变量必须 final 保证一致性
- 成员变量 = 堆上 → Lambda 通过 this 引用直接访问,不拷贝,没有一致性问题
🧠 钩子: 位置决定命运——栈上的(局部)final,堆上的(成员)随便。
❌ 常见反例
1// ❌ 混淆:以为"静态变量也在方法区,为啥能改"
2// 静态变量不在栈上,在方法区(或元空间),Lambda 通过 Class 直接访问,
3// 不涉及拷贝,不违反一致性逻辑。五、内置四大函数式接口
卡片 12|四大核心接口 —— 别再背参数名了 ⭐ 高频
Q:四大接口各自是什么?方法名和签名?
A:
| 接口 | 抽象方法 | 签名 | 中文记法 | 使用场景 |
|---|---|---|---|---|
Predicate<T> | boolean test(T t) | T → boolean | 断言 | filter, 条件判断 |
Function<T,R> | R apply(T t) | T → R | 转换 | map, 类型转换 |
Supplier<T> | T get() | () → T | 生产 | 工厂, 懒加载 |
Consumer<T> | void accept(T t) | T → void | 消费 | forEach, 副作用 |
1// 每个接口一行实战代码
2Predicate<String> isEmpty = String::isEmpty; // 断言
3Function<String, Integer> len = String::length; // 转换
4Supplier<Double> random = Math::random; // 生产
5Consumer<String> printer = System.out::println; // 消费🧠 钩子: Predicate = test 断;Function = apply 转;Supplier = get 生;Consumer = accept 消。
❌ 常见反例
1// ❌ 错误:Function 用错了方向
2// Function<Integer, String> 意思是:接收 Integer,返回 String
3Function<Integer, String> f = String::valueOf; // ✅ int → String
4// 反过来就错了:
5// Function<String, Integer> f2 = String::valueOf; // ❌ String → String? 不对!valueOf 返回 String
6
7// ❌ 错误:把 Consumer 当 Function 用
8// Consumer<String> c = String::toUpperCase; // ❌ toUpperCase 返回 String,不是 void
9Consumer<String> c = System.out::println; // ✅ println 返回 void🔍 追问
Q1:为什么 Supplier 不叫 Producer?
A: 命名来自函数式编程传统。"Supplier" is a common name for a function that supplies values without taking any input. Java 采纳了这个命名传统。Guava 里类似接口叫 Supplier,JDK8 沿用了。
Q2:四大接口各自的变体有哪些?
A: 非常多在 java.util.function 包里:
| 变体类型 | 例子 | 说明 |
|---|---|---|
| 双参数版本 | BiFunction<T,U,R> BiPredicate<T,U> BiConsumer<T,U> | 接收两个参数 |
| 同类型算子 | UnaryOperator<T>(= Function<T,T>)BinaryOperator<T>(= BiFunction<T,T,T>) | 输入输出同类型 |
| 原始类型特化 | IntPredicate LongFunction<R> DoubleConsumer | 避免自动装箱 |
| 双原始类型 | ToIntBiFunction<T,U> ObjIntConsumer<T> | 混合 |
面试高频:UnaryOperator<T> 和 BinaryOperator<T>,Stream 的 reduce 就用了 BinaryOperator。
卡片 14|compose 和 andThen —— 方向别反了
Q:compose 和 andThen 到底谁先谁后?有没有直观的记忆法?
A:
1Function<Integer, Integer> f = x -> x + 1; // f(x) = x + 1
2Function<Integer, Integer> g = x -> x * 2; // g(x) = x * 2
3
4// compose:参数函数先执行,自己的函数后执行
5Function<Integer, Integer> h1 = g.compose(f);
6// h1(x) = g(f(x)) = g(x+1) = (x+1)*2
7// compose 里 f 在"前"(先跑)
8
9// andThen:自己的函数先执行,参数函数后执行
10Function<Integer, Integer> h2 = g.andThen(f);
11// h2(x) = f(g(x)) = f(x*2) = x*2 + 1
12// andThen 里 f 在"后"(后跑)
13
14// 数值验证:
15h1.apply(3); // (3+1)*2 = 8
16h2.apply(3); // 3*2+1 = 7记忆法:
- compose = "组合到前面" → 参数函数先跑(前 → 后:compose参数 → this)
- andThen = "然后" → 参数函数后跑(前 → 后:this → andThen参数)
一句话:compose 把参数塞前面,andThen 把参数塞后面。
1// 链式模拟数学:h(x) = sqrt( abs(x) ) + 1
2// 即 h(x) = add1( sqrt( abs(x) ) )
3// 函数分解:abs → sqrt → add1
4Function<Double, Double> h =
5 ((Function<Double, Double>) Math::abs) // abs
6 .andThen(Math::sqrt) // 然后 sqrt
7 .andThen(x -> x + 1); // 然后 +1
8// andThen 可以一直链下去,compose 也可以但不太自然🧠 钩子: compose = 把参数函数塞前面先跑;andThen = 把参数函数塞后面后跑。
❌ 常见反例
1// ❌ 常见错误:方向搞反了
2// 需求:先 trim 再转大写
3Function<String, String> trim = String::trim;
4Function<String, String> upper = String::toUpperCase;
5
6Function<String, String> wrong = trim.andThen(upper); // ✅ 正确:先 trim 后 upper
7Function<String, String> also = upper.compose(trim); // ✅ 也正确:先 trim 后 upper
8// 两者结果一样:trim→upper
9
10// 混淆时刻:
11Function<String, String> bug = upper.andThen(trim); // 先 upper 后 trim(可能不是你想要的)
12// " hello " → upper → " HELLO " → trim → " HELLO"?不对 trim 只去首尾空格,先去空格再大写才有意义六、Optional
卡片 15|Optional 本质 —— 不止是 NPE 克星 ⭐ 高频
Q:Optional 到底解决了什么?和 @Nullable 注解有什么不同?
A:
Optional 解决的核心问题不是"NPE 不会再出现",而是把"可能为空"这个事实暴露在类型签名里,强迫调用者处理它。
1// 传统写法:返回值类型看不出是否可能为 null
2public User findUser(String name) {
3 // 返回 null 还是空 User?调用方不看文档就不知道
4 return null;
5}
6
7// Optional 写法:签名自己说清楚了 —— "结果有可能没有"
8public Optional<User> findUser(String name) {
9 return Optional.ofNullable(db.get(name));
10}
11// 调用方:必须显式处理空值,否则拿不到里面的 User
12User user = findUser("张三").orElse(User.GUEST);Optional vs @Nullable:
@Nullable只是注解/文档,不写检查代码照样 NPE- Optional 是编译器/运行时强制——你必须调用
orElse/ifPresent/get才能拿到值
🧠 钩子: Optional 不是免死金牌(
of(null)照样炸),而是"我可能为空,你看着办"的强制提醒。
❌ 常见反例
1// ❌ 最致命的反模式:把 Optional 本身当 null 用
2public Optional<User> findUser(String name) {
3 if (name == null) return null; // ❌❌❌ 绝对不要这样!
4 // Optional 方法返回 null 等于双重 null,调用方的 orElse 全白费
5 return Optional.empty(); // ✅ 这才是对的
6}
7
8// ❌ 反模式:用 isPresent() + get() 代替 orElse
9Optional<User> opt = findUser("张三");
10if (opt.isPresent()) { // ❌ 这是传统 null 检查的翻版,没发挥 Optional 优势
11 User user = opt.get(); // ❌ isPresent + get = 和 if(x != null) 没区别
12 // ...
13}
14// ✅ 正确:
15findUser("张三").ifPresent(user -> { /* ... */ });
16// 或
17User user = findUser("张三").orElse(User.GUEST);
18
19// ❌ 反模式:Optional 作为方法参数
20public void process(Optional<User> user) { // ❌ 别这样设计 API
21 // 调用方传来的 Optional 可能是 null → 你的代码就炸了
22}
23// ✅ Optional 只应该作为返回值类型,不推荐作为参数或字段1// ❌ 经典错误:of() vs ofNullable()
2Optional<String> opt1 = Optional.of(null); // ❌ NullPointerException!
3Optional<String> opt2 = Optional.ofNullable(null); // ✅ 返回 Optional.empty()
4// 规则:确定不为空用 of;不确定用 ofNullable🔍 追问
Q1:为什么 Optional.get() 被认为是不安全的?
A: 因为如果 Optional 是 empty,get() 会抛 NoSuchElementException,本质上把 NPE 换成了另一个异常。安全做法是永远搭配 orElse / orElseGet / orElseThrow。
Q2:Optional 的 map 和 flatMap 区别?
A: 和 Stream 一样:
1Optional<User> user = findUser("张三");
2// map:User → String → Optional<String>
3Optional<String> name = user.map(User::getName);
4// flatMap:当转换函数自身也返回 Optional 时,避免 Optional<Optional<T>>
5// flatMap:User → Optional<Address> → Optional<Address>(拍平)
6Optional<Address> addr = user.flatMap(User::getAddress);
7// 如果 User.getAddress() 返回 Optional<Address>,
8// 用 map 会得到 Optional<Optional<Address>>,用 flatMap 得到 Optional<Address>Q3:orElse 和 orElseGet 有什么区别?
A: orElse 是急切的——即使 Optional 有值,默认值也会被创建;orElseGet 是惰性的——只有缺失时才调用 Supplier。如果默认值创建很贵,用 orElseGet。
1// orElse:不管 Optional 有没有值,expensiveDefault() 都会执行
2String s1 = opt.orElse(expensiveDefault()); // ❌ 浪费
3
4// orElseGet:只有缺失时才执行 expensiveDefault()
5String s2 = opt.orElseGet(() -> expensiveDefault()); // ✅卡片 16|Optional API 全景图
Q:Optional 完整 API 梳理?
A:
1// ========== 创建 ==========
2Optional.empty() // 空 Optional
3Optional.of(value) // 非空值(null 则 NPE)
4Optional.ofNullable(value) // 允许 null,null 变成 empty
5
6// ========== 取值 ==========
7opt.get() // ⚠️ 不安全,empty 时抛异常
8opt.orElse(default) // 有值返回值,否则返回默认值
9opt.orElseGet(() -> ...) // 惰性默认值
10opt.orElseThrow() // 有值返回值,否则抛 NoSuchElementException
11opt.orElseThrow(() -> new X) // 自定义异常
12
13// ========== 判空执行 ==========
14opt.isPresent() // 有值返回 true
15opt.isEmpty() // JDK11+:空返回 true
16opt.ifPresent(v -> ...) // 有值则消费
17opt.ifPresentOrElse(v -> ..., () -> ...) // JDK9+:有值/无值双处理
18
19// ========== 链式转换 ==========
20opt.map(v -> ...) // 有值时转换,返回 Optional
21opt.flatMap(v -> ...) // 和 map 一样但自动拍平
22opt.filter(v -> condition) // 有值且满足条件才保留,否则 empty
23
24// ========== 流转换(JDK9+)==========
25opt.stream() // 变成 0 或 1 个元素的 Stream🧠 钩子: 创建三件套 + 取值三件套 + map/flatMap/filter 链 = 必备技能。
七、Stream 流
卡片 17|Stream 本质 —— 不是数据是管道 ⭐ 高频
Q:Stream 的本质是什么?三大特性为什么这么设计?
A:
Stream 不是数据结构,是对数据源的计算流程描述。本质是"声明式流水线":
1names.stream() // 获取数据源
2 .filter(n -> n != null) // 描述步骤1:去空
3 .map(String::toUpperCase) // 描述步骤2:转大写
4 .sorted() // 描述步骤3:排序
5 .collect(toList()); // 触发执行,输出结果三大核心特性及其设计原因:
① 不存数据 —— Stream 只是数据的"视图",不会复制一份。内存开销极小。
② 惰性求值 —— 中间操作(filter、map)只是"记账",不真执行。只有终端操作(collect、count)才触发全部计算。好处:
- 中间操作可以融合:
filter→map可以在一次遍历中完成,不用先过滤完再映射 - 可以短路:
findFirst找到第一个就停止,不用处理全部元素 - 无限流成为可能:
Stream.iterate(0, i->i+1).limit(10)— limit 之前不用真的生成无限个
③ 一次性消费 —— 流用完就没了,不能复用。像"读完的文件流"一样。需要复用就重新从数据源获取。
🧠 钩子: Stream = 不存数据的惰性流水线,用完即弃。
❌ 常见反例
1// ❌ 经典错误:复用已消费的 Stream
2Stream<String> stream = list.stream().filter(s -> s.length() > 3);
3long count = stream.count(); // ✅ 第一次消费
4// long count2 = stream.count(); // ❌ IllegalStateException: stream has already been operated upon or closed
5
6// ✅ 正确:需要多次用时重新获取
7long count1 = list.stream().filter(s -> s.length() > 3).count();
8long count2 = list.stream().filter(s -> s.length() > 3).count();
9
10// ❌ 错误:在 Stream 操作中修改源数据
11List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
12// list.stream().forEach(s -> list.add(s + "!")); // ⚠️ 可能 ConcurrentModificationException
13// Stream 不应该在操作过程中修改源数据,尤其在并行流中是灾难1// ❌ 常见误会:以为中间操作改了源集合
2List<String> list = new ArrayList<>(Arrays.asList("b", "a", "c"));
3list.stream().sorted().collect(toList()); // 返回新 List
4System.out.println(list); // [b, a, c] —— 原集合没变!sorted 只是视图排序🔍 追问
Q1:Stream 能不能无限大?
A: 可以,用 Stream.generate() 或 Stream.iterate() 创建无限流。但必须配合 limit() 或 findFirst() 等短路终端操作,否则会无限运行。
1// 斐波那契无限流(只取前10个)
2Stream.iterate(new int[]{0, 1}, f -> new int[]{f[1], f[0] + f[1]})
3 .limit(10)
4 .map(f -> f[0])
5 .forEach(System.out::println);Q2:Stream 和 IntStream / LongStream / DoubleStream 有什么区别?
A: 原始类型特化版本,避免自动装箱开销。IntStream 多了 sum()、average()、range()、summaryStatistics() 等数值专用方法。
卡片 18|中间 vs 终端操作 —— 记住"懒"和"点火" ⭐ 高频
Q:所有中间和终端操作的完整清单?
A:
| 中间操作(返回 Stream,惰性) | 特点 | 终端操作(触发,输出结果) | 输出类型 |
|---|---|---|---|
filter(Predicate) | 过滤 | forEach(Consumer) | void |
map(Function) | 转换 | forEachOrdered(Consumer) | void(保证顺序) |
flatMap(Function→Stream) | 转换+拍平 | collect(Collector) | R |
distinct() | 去重 | toList() (JDK16+) | List<T> |
sorted() | 排序 | count() | long |
sorted(Comparator) | 排序 | min(Comparator) | Optional<T> |
peek(Consumer) | 调试偷看 | max(Comparator) | Optional<T> |
limit(long) | 截断 | anyMatch(Predicate) | boolean |
skip(long) | 跳过 | allMatch(Predicate) | boolean |
takeWhile(Pred) (JDK9+) | 满足则取 | noneMatch(Predicate) | boolean |
dropWhile(Pred) (JDK9+) | 满足则丢 | findFirst() | Optional<T> |
mapToInt/mapToLong/mapToDouble | 转原始类型 | findAny() | Optional<T> |
reduce(BinaryOperator) | Optional<T> | ||
reduce(identity, BinaryOp) | T | ||
reduce(identity, BiFunc, BinaryOp) | U |
🧠 记忆法: 返回 Stream 的 = 中间;返回非 Stream 的 = 终端。peek 是个例外(返回 Stream 但通常有副作用,只用于调试)。
❌ 常见反例
1// ❌ 经典错误:没有终端操作,什么都不执行
2list.stream()
3 .filter(s -> { System.out.println("filter: " + s); return true; })
4 .map(s -> { System.out.println("map: " + s); return s; });
5// 控制台空空如也!因为没加终端操作,整个管道没被"点火"
6
7// ✅ 加个终端操作:
8list.stream()
9 .filter(s -> { System.out.println("filter: " + s); return true; })
10 .map(s -> { System.out.println("map: " + s); return s; })
11 .collect(toList()); // ← 点火!现在控制台有输出了1// ❌ 错误:终端操作后还想继续流操作
2Stream<String> s = list.stream();
3s.filter(...).collect(toList()); // ✅
4// s.map(...) // ❌ 流已经消费了🔍 追问
Q1:forEach 和 forEachOrdered 有什么区别?
A: 在顺序流中没区别。在并行流中,forEach 不保证顺序(为了效率),forEachOrdered 严格按原始顺序。但 forEachOrdered 会牺牲并行性能。
Q2:findFirst 和 findAny 有什么区别?
A: 顺序流中没区别。并行流中,findFirst 返回第一个(限制并行效率),findAny 返回任意一个(更高效)。如果只关心"有没有"而不关心"第几个",用 findAny。
卡片 19|sorted 的坑 —— 为什么它不是真正的流式操作?
Q:为什么 sorted 是"有状态"中间操作?还有哪些操作有状态?
A:
**"有状态"**意味着操作需要记住已经见过的所有元素才能继续。对于 sorted,你必须看完所有元素才知道最小的(或第一个排序后的)是什么。
1// 直观理解:排序必须全部看完才能确定第一个
2// 如果给你 1, 5, 3, 2 四个数
3// filter(x > 2):来一个判一个,x=1 直接丢 → 流式 ✅
4// sorted():看完 1,5,3,2 才知道排序后是 1,2,3,5 → 非流式 ❌(得全量缓存)有状态的中间操作清单:
sorted()/sorted(Comparator)— 全量排序distinct()— 记住所有见过的元素才能去重limit()— 无状态(直接数就行)skip()— 有状态(得数过去多少个)takeWhile/dropWhile— 无状态(条件一破坏就停止/开始)
🧠 钩子: sorted 和 distinct 都要"全量记忆",大数据量可能 OOM。
❌ 常见反例
1// ⚠️ 性能陷阱:大文件行排序
2// 如果文件有 10GB,stream 里 sorted() 会把所有行读到内存 → OOM
3Files.lines(Paths.get("huge.log")) // 惰性读取
4 .sorted() // ❌ 读进内存,可能 OOM
5 .forEach(System.out::println);
6
7// ✅ 正确:大文件排序用外部排序,或数据库排序,或先 limit 再 sorted1// ❌ 顺序误区:先 sorted 再 filter vs 先 filter 再 sorted
2list.stream()
3 .sorted() // 排序全部
4 .filter(...) // 过滤掉大部分
5 .collect(...);
6// 不如:
7list.stream()
8 .filter(...) // 先过滤掉大部分
9 .sorted() // 只排序剩下的 ← 更高效!
10// 原则:先 filter 缩小数据集,再做 sorted / distinct 等有状态操作八、并行流
卡片 21|并行流原理 —— ForkJoinPool 是怎么工作的? ⭐ 高频
Q:并行流底层用了什么?分片策略是怎样的?
A:
底层: parallelStream() 使用 ForkJoinPool.commonPool()(公共线程池)。线程数默认 = Runtime.getRuntime().availableProcessors() - 1(至少 1)。
流程:
1// 列表:[1, 2, 3, 4, 5, 6, 7, 8]
2list.parallelStream()
3 .map(x -> x * 2)
4 .reduce(0, Integer::sum);
5
6// 内部大致流程:
7// Step 1: Fork(拆分)
8// [1,2,3,4,5,6,7,8] → [1,2,3,4] + [5,6,7,8]
9// → [1,2] + [3,4] + [5,6] + [7,8]
10// Step 2: Process(并行处理)
11// 四个线程分别处理 [1,2] [3,4] [5,6] [7,8]
12// → (2,4) (6,8) (10,12) (14,16)
13// Step 3: Join(合并)
14// → (2+4+6+8) + (10+12+14+16) = 20 + 52 = 72分片策略: ArrayList 用数组索引分片(高效),LinkedList 或 HashSet 分片效率低(遍历才知道大小),不适合并行流。
🧠 钩子: ForkJoinPool 公共池,默认 CPU 核心数-1 线程。ArrayList 适合并行,LinkedList 不适合。
❌ 常见反例
1// ❌ 错误:在并行流内使用非线程安全的集合
2List<Integer> results = new ArrayList<>(); // 不是线程安全的!
3list.parallelStream()
4 .map(x -> x * 2)
5 .forEach(results::add); // ❌ 竞态条件!可能丢数据或抛异常
6// ✅ 用 collect:
7List<Integer> safe = list.parallelStream()
8 .map(x -> x * 2)
9 .collect(toList()); // ✅ 线程安全
10// ✅ 或用线程安全容器:
11List<Integer> results = Collections.synchronizedList(new ArrayList<>());
12
13// ❌ 错误:并行流里用了有状态的 Lambda
14int[] sum = {0}; // ❌ 竞态条件
15list.parallelStream().forEach(n -> sum[0] += n);1// ❌ 错误:在公共 ForkJoinPool 中执行阻塞操作
2list.parallelStream().forEach(item -> {
3 // Thread.sleep(1000); // ❌ 占用公共线程池线程,影响全局
4});
5// 公共线程池被阻塞 → JVM 中所有用到 parallelStream 的地方都受影响🔍 追问
Q1:怎么自定义并行流的线程数? A: 默认用公共池无法直接改。但可以这样:
1ForkJoinPool customPool = new ForkJoinPool(4);
2customPool.submit(() ->
3 list.parallelStream().forEach(...)
4).get();
5// 这会使用自定义池而不是公共池Q2:并行流的元素顺序有保证吗?
A: forEach 不保证,forEachOrdered 保证但影响性能。collect(toList()) 输出顺序与输入一致(Collector 保证顺序)。findAny 不保证,findFirst 保证。
Q3:哪些数据源适合并行流? A: ArrayList、数组、IntStream.range → 非常适合(O(1) 随机访问分片);HashSet、TreeSet → 一般(有点分片开销);LinkedList、Stream.iterate → 不适合(很难分片)。
九、Map 新特性
卡片 23|Map 新方法 —— 让你告别冗长的 if-else
Q:JDK8 Map 新增方法的使用场景和最佳实践?
A:
① putIfAbsent — "不存在时才放"
1// 旧写法(3行)
2if (!map.containsKey(key)) {
3 map.put(key, value);
4}
5// 新写法(1行)
6map.putIfAbsent(key, value);② computeIfAbsent — "不存在时计算并放入"(最高频!)
1// 经典场景:分组 + 计数 / 分组 + 列表
2Map<String, List<User>> byCity = new HashMap<>();
3for (User u : users) {
4 byCity.computeIfAbsent(u.getCity(), k -> new ArrayList<>())
5 .add(u);
6}
7// 内部逻辑:如果 city 不在 map 中,执行 Lambda 创建 ArrayList,放入 map,返回它
8// 如果 city 已存在,直接返回已有的 List
9
10// 旧写法(需要 if + put,至少 5 行):
11List<User> list = byCity.get(u.getCity());
12if (list == null) {
13 list = new ArrayList<>();
14 byCity.put(u.getCity(), list);
15}
16list.add(u);③ computeIfPresent — "存在时才计算更新"
1// 场景:对购物车里的商品数量翻倍
2map.computeIfPresent(key, (k, v) -> v * 2);
3// 如果 key 存在,用新值替换;如果不存在,什么都不做④ merge — "合并值"
1// 场景:计数累加
2wordCounts.merge(word, 1, Integer::sum);
3// key 不存在 → 放 1
4// key 存在 → sum(old, 1) = old+1
5
6// 场景:合并两个 Map
7map2.forEach((k, v) -> map1.merge(k, v, Integer::sum));⑤ 其他
1map.getOrDefault(key, defaultValue); // 不存在给默认值
2map.remove(key, value); // k 且 v 都匹配才删
3map.replace(key, value); // 存在则替换
4map.replace(key, oldValue, newValue); // k 且 v 都匹配才替换🧠 钩子:
computeIfAbsent= 不存就算,computeIfPresent= 存了再算,merge= 合二为一。
❌ 常见反例
1// ❌ 错误:computeIfAbsent 里又去改同一个 map(可能导致 ConcurrentModificationException)
2map.computeIfAbsent(key, k -> {
3 // map.put("other", "value"); // ❌ 别!在 compute 函数里修改 map 是不安全的
4 return "value";
5});
6
7// ❌ 错误:merge 的 remapping 函数返回 null
8map.merge(key, 1, (oldV, newV) -> null);
9// 如果 remapping 函数返回 null,会删除该 key!这是 merge 的特殊行为
10
11// ❌ 常见误解:putIfAbsent 返回什么?
12String old = map.putIfAbsent("key", "newValue");
13// 如果 key 已经存在 → 返回已有的 value
14// 如果 key 不存在 → 返回 null
15// 容易误以为返回"将要被 put 的值"🔍 追问
Q1:computeIfAbsent 和 putIfAbsent 核心区别?
A: putIfAbsent 的值是已计算好的传进去;computeIfAbsent 的值是惰性计算的(Lambda),只在 key 不存在时才执行。如果值的创建很昂贵(比如新建一个大对象),用 computeIfAbsent 更省。
Q2:为什么 merge 的 remapping 函数返回 null 会删除 key?
A: 这是刻意设计的:允许通过 merge 实现"如果计算结果为 null 则删除",省去额外的 remove 调用。如果不想删除,永远不要在 remapping 里返回 null。
卡片 24|Map 与 Stream —— Map 本身不流
Q:Map 为什么不直接支持 Stream?怎么在 Map 上用 Stream 操作?
A:
Map 不支持 .stream() 主要是语义问题——Stream 是"元素序列",而 Map 的"元素"是什么?key?value?entry?三者完全不同,没法用一个 stream() 同时表达。所以设计了三种视图:
1Map<String, Integer> map = ...;
2
3// 三种视图各自都可以用 Stream:
4map.keySet().stream() // Stream<String> → 对 key 操作
5map.values().stream() // Stream<Integer> → 对 value 操作
6map.entrySet().stream() // Stream<Map.Entry<String, Integer>> → 对 key+value 操作
7
8// 实用例子:
9// 过滤出 value > 10 的 key
10List<String> keys = map.entrySet().stream()
11 .filter(e -> e.getValue() > 10)
12 .map(Map.Entry::getKey)
13 .collect(toList());
14
15// 把 Map 转成另一个 Map
16Map<String, Integer> doubled = map.entrySet().stream()
17 .collect(toMap(Map.Entry::getKey, e -> e.getValue() * 2));🧠 钩子: Map 不直接流,但 keySet / values / entrySet 都能流。
十、Date API(java.time)
卡片 25|新日期 API —— 为什么老 Date 是"坏的"? ⭐ 高频
Q:java.util.Date 具体哪里不好?新 API 怎么解决了?
A:
老 Date 的罪状:
1// 1️⃣ 可变 → 线程不安全
2Date d = new Date();
3d.setTime(1000); // 原对象被改了!
4
5// 2️⃣ 偏移量诅咒:月份从 0 开始
6Date d = new Date(2024, 1, 1); // 你以为 2024-01-01?
7// 实际是 3924-02-01!(年份 = 1900+2024,月份 1=2月)
8
9// 3️⃣ SimpleDateFormat 线程不安全!
10SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
11// 多线程共享同一个 sdf 实例 → 解析结果错乱,甚至抛异常
12
13// 4️⃣ 命名混乱
14Date d = new Date(); // 包含日期+时间,但叫"Date"
15Date d2 = new Date(0); // 也是日期+时间新 API 设计哲学:一切不可变 + 线程安全:
1// 每个类只做一件事,名字说清楚
2LocalDate date = LocalDate.of(2024, 1, 1); // 只有日期,月份终于从 1 开始
3LocalTime time = LocalTime.of(10, 30); // 只有时间
4LocalDateTime dt = LocalDateTime.of(date, time); // 日期+时间
5ZonedDateTime zdt = ZonedDateTime.now(); // 带时区
6Instant instant = Instant.now(); // 机器时间戳
7
8// 所有修改都返回新对象
9LocalDate tomorrow = date.plusDays(1); // date 本身没变
10LocalDate nextMonth = date.plusMonths(1); // 又一个新对象🧠 钩子: 老API = 可变/月份0开头/线程不安全;新API = 不可变/1开头/线程安全。
❌ 常见反例
1// ❌ 老代码常见错误:共享 SimpleDateFormat
2public class DateUtils {
3 private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");
4 // 多线程共享 → ❌ 线程安全问题!!!
5
6 public static String format(Date d) {
7 return SDF.format(d); // ❌ 并发调用会挂
8 }
9}
10
11// ✅ 新写法:
12public class DateUtils {
13 private static final DateTimeFormatter FMT =
14 DateTimeFormatter.ofPattern("yyyy-MM-dd");
15 // DateTimeFormatter 不可变,线程安全 ✅
16
17 public static String format(LocalDate d) {
18 return FMT.format(d);
19 }
20}1// ❌ 常见错误:LocalDateTime 不带时区,跨国业务直接用会出问题
2LocalDateTime dt = LocalDateTime.now(); // 拿的是服务器本地时间
3// 如果服务器在上海,用户在美国,这个时间对用户来说无意义
4
5// ✅ 跨时区用 ZonedDateTime 或 Instant
6Instant instant = Instant.now(); // UTC 时间,全球通用
7ZonedDateTime userTime = instant.atZone(ZoneId.of("America/New_York"));🔍 追问
Q1:Instant 和 LocalDateTime 什么关系?
A: Instant = UTC 时间线上的一点(纯机器概念);LocalDateTime = 墙上时间(人类概念:2024-01-01 10:00),但不知道是哪个时区的。同一 Instant 在不同时区对应不同 LocalDateTime。
Q2:新 API 怎么和老的 Date/Calendar 互转? A:
1// Date → Instant → LocalDateTime
2Date oldDate = new Date();
3LocalDateTime dt = oldDate.toInstant()
4 .atZone(ZoneId.systemDefault())
5 .toLocalDateTime();
6
7// LocalDateTime → Instant → Date
8Date newDate = Date.from(
9 LocalDateTime.now()
10 .atZone(ZoneId.systemDefault())
11 .toInstant()
12);Q3:Period 和 Duration 区别?
A: Period = 基于日期的(年月日),比如"差 3 个月 5 天";Duration = 基于时间的(时分秒纳秒),比如"差 2 小时 30 分钟"。
1Period period = Period.between(date1, date2); // P3M5D
2Duration duration = Duration.between(time1, time2); // PT2H30M十一、注解
卡片 27|重复注解 —— 什么时候真的需要它?
Q:@Repeatable 实际解决了什么问题?容器注解是什么?
A:
JDK8 之前,同一个注解同一个位置只能用一次:
1// JDK7 及以前:想打多个 @Role → 必须用容器注解包起来
2@Roles({ // 容器注解
3 @Role("admin"),
4 @Role("user")
5})
6public class User {}
7
8// JDK8:可以直接写多个同名注解
9@Role("admin")
10@Role("user")
11public class User {}容器注解是什么? 是 @Repeatable 用到的幕后容器——仍然存在,但被隐藏了。编译器在背后自动把多个 @Role 打包成 @Roles。用反射获取时:
1// 反射获取时,直接用 getAnnotationsByType
2Role[] roles = User.class.getAnnotationsByType(Role.class);
3// 也能通过 getAnnotation(Roles.class) 获取容器,但不推荐定义方式:
1// 可重复的注解
2@Repeatable(Roles.class) // 指定容器
3@interface Role {
4 String value();
5}
6
7// 容器注解
8@interface Roles {
9 Role[] value(); // 必须是 value(),且类型是重复注解的数组
10}🧠 钩子: @Repeatable = 编译期去重,运行时靠反射
getAnnotationsByType。容器注解藏在幕后。
❌ 常见反例
1// ❌ 错误:容器注解的 value() 属性的类型写错
2@Repeatable(Roles.class)
3@interface Role { String value(); }
4
5@interface Roles {
6 // ❌ 错误:
7 // String[] value(); // 必须是 Role[],不是其他类型
8 Role[] value(); // ✅ 正确
9}
10
11// ❌ 错误:容器注解的成员名必须是 value(编译器约定)
12@interface Roles {
13 // Role[] roles(); // ❌ 不能用其他名字
14 Role[] value(); // ✅ 必须叫 value
15}1// ❌ 常见错误:只获取容器注解而忽略了重复注解
2// Roles roles = cls.getAnnotation(Roles.class); // JDK7 方式,能用但笨拙
3// ✅ 新 API:
4Role[] roles = cls.getAnnotationsByType(Role.class); // JDK8 方式🔍 追问
Q1:TYPE_USE 和 TYPE_PARAMETER 的区别?
A:
TYPE_PARAMETER:只能标在类型参数声明上(如<@NonNull T>)TYPE_USE:可以标在任何使用类型的地方(声明、new、cast、泛型参数等等),范围更广
1// TYPE_PARAMETER:只能标类型参数的声明
2class Container<@MyAnnotation T> {} // 标在 T 的声明上
3
4// TYPE_USE:标在类型使用的任何地方
5@MyAnnotation String name; // 字段声明
6String s = (@MyAnnotation String) obj; // 强制转换
7new @MyAnnotation ArrayList<>(); // 对象创建
8List<@MyAnnotation String> list; // 泛型参数Q2:有什么实际场景会用到重复注解?
A: 常见于权限/角色系统(一个方法需要多个角色)、校验框架(一个字段需要多种校验)、事件监听(一个方法处理多个事件类型)。Spring 的 @EventListener 用了 @Repeatable。
📊 复习优先级 + 自检清单
⭐⭐⭐ 最高频(面试 90% 会问)
| 卡片 | 自检问题 | 能否 30 秒讲清? |
|---|---|---|
| 4 | Lambda 底层不是语法糖,为什么?invokedynamic 怎么工作的? | ☐ |
| 9 | 为什么局部变量要 final?"effectively final"的临界线在哪? | ☐ |
| 12 | 四大函数式接口分别叫什么、方法名是什么、签名的箭头怎么写? | ☐ |
| 15 | Optional 解决的不是 NPE 消失,而是什么?of vs ofNullable 区别? | ☐ |
| 17 | Stream 三大特性?惰性求值为什么重要?中间操作什么时候才执行? | ☐ |
| 18 | 至少说出 5 个中间操作和 5 个终端操作 | ☐ |
| 21 | parallelStream 用的什么线程池?默认几个线程?怎么改? | ☐ |
| 25 | 老 Date 四个缺点 + 新 API 两个核心优势?DateTimeFormatter vs SimpleDateFormat? | ☐ |
⭐⭐ 常考
| 卡片 | 自检问题 |
|---|---|
| 1 | default 方法和抽象类有什么区别?什么时候用哪个? |
| 2 | 函数式接口定义?Comparator 为什么算函数式接口? |
| 7 | :: 四种形式,第③种"类的实例方法引用"怎么理解? |
| 16 | Optional 的 map vs flatMap,orElse vs orElseGet? |
| 20 | reduce 的三个重载版本分别怎么用? |
| 23 | computeIfAbsent 和 putIfAbsent 区别?merge 返回 null 会怎样? |
⭐ 加分项
| 卡片 | 自检问题 |
|---|---|
| 3 / 5 / 6 / 8 / 10 / 11 / 13 / 14 / 19 / 22 / 24 / 26 / 27 | 至少过一遍,能说出大概即可 |
🔧 使用建议
- 先自检 —— 对着上面的清单,☐ 里打勾,找出真正的盲区
- 每张卡先看 Q,默写 A —— 写不出来才看答案,写完对比
- 每张卡的 ❌ 反例自己写一遍 —— 知道"什么不该写"比知道"什么该写"记得更牢
- 追问部分用来模拟面试 —— 让朋友/自己追问,逼出知识深度
- 配合源码跑 —— clone itstack-demo-jdk8,跑不通的地方就是没理解透的地方