Java新特性(一):Java 8 核心特性
Java新特性(一):Java 8 核心特性
导语:新特性面试有一个公认的优先级:Java 8 > Java 21 > Java 17 > Java 11 > Java 25 > 其余版本。Java 8 是唯一一个「必须逐条掌握」的版本——Lambda、函数式接口、Stream、Optional、默认方法、新日期时间 API、CompletableFuture,几乎每场面试都会问。本篇把这些考点连同高频追问(为什么捕获 final、并行流是否更快、
orElse与orElseGet的区别)一次讲透。
一、先建立版本地图
Java 自 JDK 9 起改为每 6 个月一个大版本(每年 3 月、9 月各一次),并引入「长期支持版(LTS)」概念。截至 2026 年 9 月的版本节奏:
| 版本 | 发布时间 | 是否 LTS | 一句话定位 |
|---|---|---|---|
| Java 8 | 2014-03 | ✅ LTS | Lambda + Stream,史上最重要的版本 |
| Java 9 | 2017-09 | ❌ | 模块系统 JPMS、JShell |
| Java 10 | 2018-03 | ❌ | var 局部变量类型推断 |
| Java 11 | 2018-09 | ✅ LTS | 标准 HTTP Client、String 增强 |
| Java 12~15 | 2019-03 ~ 2020-09 | ❌ | switch 表达式、文本块、record 依次预览转正 |
| Java 17 | 2021-09 | ✅ LTS | record、密封类、模式匹配 instanceof 转正 |
| Java 18~20 | 2022-03 ~ 2023-03 | ❌ | 虚拟线程、FFM、结构化并发逐步预览 |
| Java 21 | 2023-09 | ✅ LTS | 虚拟线程转正、模式匹配 switch、Sequenced Collections |
| Java 22~24 | 2024-03 ~ 2025-03 | ❌ | FFM 转正、Stream Gatherers、无 pinning 虚拟线程 |
| Java 25 | 2025-09 | ✅ LTS | Scoped Values、紧凑源文件、紧凑对象头 |
| Java 26 | 2026-03 | ❌ | HTTP/3、AOT 对象缓存 |
| Java 27 | 2026-09 | ❌ | G1 全局默认、紧凑对象头默认开启、后量子 TLS |
记忆要点:
- LTS 序列:8 → 11 → 17 → 21 → 25(每两年一个 LTS);下一个 LTS 是预计 2027 年 9 月的 JDK 29。
- 面试权重:Java 8 是地基,Java 17/21 是当前主流生产版本,Java 25 是新晋 LTS,其余非 LTS 版本「知道有这回事」即可。
千万不要按版本逐条背诵新特性,面试官真正想听的是:这个特性解决了什么问题、怎么用、有没有坑。
二、Lambda 表达式
1. Lambda 表达式是什么?解决什么问题?
答: Lambda 是 Java 8 引入的函数式编程语法,本质是「一段可以传递的代码」,用于把行为作为参数传给方法。
它解决的问题:以前只能靠匿名内部类传递行为,代码冗长且噪音大(new Runnable(){...} 五行只干了一行的事)。
// Java 8 之前:匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
}).start();
// Java 8:Lambda
new Thread(() -> System.out.println("hello")).start();2. Lambda 的语法形式有哪几种?
答: 常见写法:
// 无参
() -> System.out.println("no args");
// 单参(可省略括号)
x -> x + 1;
// 多参 + 返回语句块
(int a, int b) -> { return a + b; }
// 单表达式,return 与花括号都可省略
(a, b) -> a + b;
// 带类型声明的写法(参数类型可省略,由编译器推断)
Comparator<Integer> c = (Integer a, Integer b) -> a - b;3. 为什么 Lambda 只能捕获 final 或「事实上的 final」的局部变量?
答: 这是面试高频追问,答案有两层:
- 语义层(不变性):Lambda 捕获的是局部变量的值副本(不是引用本身),如果允许该变量在 Lambda 之外被修改,就会出现「看见的是旧值还是新值」的歧义。要求 effectively final(即赋值后从未再改),从根本上杜绝了这种歧义。
- 实现层(无状态副本):Lambda 并不像匿名内部类那样在编译期通过构造器把变量「拷贝进对象字段」——它捕获的变量值随 invokedynamic 生成的实例一起传递。既然只是快照,就不该让人误以为还能读到后续修改。
int count = 10;
Runnable r = () -> System.out.println(count); // OK:count 未被再赋值
// count = 20; // 一旦放开这行,上面的 Lambda 直接编译报错对比:成员变量(实例/静态字段)不受此限制,因为它们存在堆里、通过 this 访问,天然可见最新值。
public class Demo {
private int count = 0; // 成员变量
void test() {
Runnable r = () -> count++; // 合法,可修改
}
}4. Lambda 和匿名内部类有什么区别?(高频)
答:
| 维度 | Lambda | 匿名内部类 |
|---|---|---|
| 适用接口 | 只能用于函数式接口(唯一抽象方法) | 任何接口/抽象类 |
| 编译产物 | invokedynamic + 动态生成的类(不是内部类文件) | 生成独立的 Xxx$1.class |
this 指向 | 指向外层实例 | 指向匿名内部类自身 |
| 变量捕获 | 只能捕获 effectively final 局部变量 | 同样只能捕获 effectively final 局部变量 |
| 能否定义同名成员 | 不可以 | 可以自定义字段遮蔽外层变量 |
public class ThisDemo {
Runnable lambda = () -> System.out.println(this); // 打印 ThisDemo 实例
Runnable anon = new Runnable() {
public void run() { System.out.println(this); } // 打印 匿名内部类实例
};
}三、函数式接口
5. 什么是函数式接口?Java 8 内置了哪些?
答: 有且仅有一个抽象方法的接口(可以有多个 default/static 方法,也可以有 Object 的公共方法签名)。@FunctionalInterface 只是编译期校验,加不加都能当函数式接口用,但建议加上。
四大核心内置函数式接口:
| 接口 | 方法签名 | 含义 | 典型场景 |
|---|---|---|---|
Function<T,R> | R apply(T t) | 转换 | map |
Consumer<T> | void accept(T t) | 消费 | forEach |
Supplier<T> | T get() | 生产 | orElseGet、CompletableFuture.supplyAsync |
Predicate<T> | boolean test(T t) | 判断 | filter |
派生接口:BiFunction、UnaryOperator、BinaryOperator、BiConsumer、IntFunction、ToIntFunction 等(基本类型特化版本用于避免装箱)。
@FunctionalInterface
interface Calculator {
int calc(int a, int b);
// default 方法不影响函数式接口的性质
default Calculator andThen(Calculator after) {
return (a, b) -> after.calc(this.calc(a, b), b);
}
}
Calculator add = (a, b) -> a + b;
System.out.println(add.calc(1, 2)); // 3四、方法引用
6. 方法引用有哪几种形式?
答: 方法引用是 Lambda 的语法糖,当 Lambda 体只是「调用一个已存在的方法」时可用 :: 简化。
| 类型 | 语法 | 示例 |
|---|---|---|
| 静态方法引用 | 类名::静态方法 | Integer::parseInt |
| 实例方法引用(特定对象) | 对象::实例方法 | System.out::println |
| 实例方法引用(任意对象) | 类名::实例方法 | String::length |
| 构造器引用 | 类名::new | ArrayList::new |
| 数组构造器引用 | 类型[]::new | int[]::new |
List<String> list = Arrays.asList("b", "a", "c");
list.sort(String::compareTo); // 任意对象实例方法引用
list.forEach(System.out::println); // 特定对象实例方法引用
list.stream().map(String::toUpperCase); // 类名::实例方法(等价 s -> s.toUpperCase())
Supplier<List<String>> s = ArrayList::new; // 构造器引用易错点:
类名::实例方法中,第一个参数会作为方法接收者,因此String::length等价于s -> s.length(),而String::compareTo等价于(a, b) -> a.compareTo(b)。
五、Stream API
7. Stream 和 Collection 的区别是什么?(必问)
答:
- Collection 是「数据结构」,关注存储:元素在内存中的组织方式、增删改查、可以反复遍历。
- Stream 是「计算视图」,关注运算:对数据源做一系列声明式的流水线操作,本身不存储元素。
- 关键差异:
- Stream 不修改数据源,返回的是新的 Stream;
- Stream 是惰性的,中间操作不会立即执行,直到遇到终止操作;
- Stream 只能消费一次,再次使用会抛
IllegalStateException: stream has already been operated upon or closed; - Stream 可以是无限的(
generate/iterate),Collection 不行。
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println); // ❌ IllegalStateException8. 中间操作和终止操作有什么区别?
答:
| 类型 | 特点 | 常见 API |
|---|---|---|
| 中间操作(Intermediate) | 返回新 Stream,惰性,不触发计算 | filter、map、flatMap、distinct、sorted、limit、skip、peek |
| 终止操作(Terminal) | 触发流水线执行,返回非 Stream 结果 | forEach、collect、reduce、count、anyMatch、findFirst、toArray |
Stream.of("a", "b", "c")
.filter(s -> { System.out.println("filter: " + s); return true; }) // 不输出
.map(String::toUpperCase); // 不输出,因为无终止操作
Stream.of("a", "b", "c")
.filter(s -> { System.out.println("filter: " + s); return true; })
.count(); // 此时才逐个执行短路操作:anyMatch、findFirst、limit 等可以在满足条件后提前结束流水线,是性能优化的重要手段。
9. Stream 常用操作速查(附 demo)
答: 一段代码覆盖高频 API:
public class StreamDemo {
// Java 8 还没有 record,用普通类演示
static class User {
private final String name;
private final int age;
private final String city;
User(String name, int age, String city) {
this.name = name;
this.age = age;
this.city = city;
}
String getName() { return name; }
int getAge() { return age; }
String getCity() { return city; }
}
public static void main(String[] args) {
// Java 8 还没有 List.of,用 Arrays.asList
List<User> users = Arrays.asList(
new User("张三", 18, "北京"),
new User("李四", 25, "上海"),
new User("王五", 30, "北京"),
new User("赵六", 25, "深圳")
);
// 1. filter + map + collect
List<String> names = users.stream()
.filter(u -> u.getAge() >= 25)
.map(User::getName)
.collect(Collectors.toList());
// 2. 分组 groupingBy
Map<String, List<User>> byCity = users.stream()
.collect(Collectors.groupingBy(User::getCity));
// 3. 分组 + 聚合:每个城市的平均年龄
Map<String, Double> avgAge = users.stream()
.collect(Collectors.groupingBy(User::getCity, Collectors.averagingInt(User::getAge)));
// 4. 分区 partitioningBy(条件分组,key 只有 true/false)
Map<Boolean, List<User>> adult = users.stream()
.collect(Collectors.partitioningBy(u -> u.getAge() >= 18));
// 5. 排序 + 取前 N
List<User> top2 = users.stream()
.sorted(Comparator.comparingInt(User::getAge).reversed())
.limit(2)
.collect(Collectors.toList());
// 6. flatMap:把嵌套集合「拍平」
List<List<Integer>> nested = Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4));
List<Integer> flat = nested.stream()
.flatMap(List::stream)
.collect(Collectors.toList()); // [1, 2, 3, 4]
// 7. reduce:归约成一个值
int totalAge = users.stream().map(User::getAge).reduce(0, Integer::sum);
// 8. 转 Map(注意 key 冲突要用合并函数,否则抛 IllegalStateException)
Map<String, Integer> nameToAge = users.stream()
.collect(Collectors.toMap(User::getName, User::getAge, (a, b) -> a));
// 9. 拼接字符串
String joined = users.stream().map(User::getName).collect(Collectors.joining(", ", "[", "]"));
System.out.println(joined); // [张三, 李四, 王五, 赵六]
}
}高频坑:
Collectors.toMap遇到重复 key 会抛IllegalStateException: Duplicate key,第四个参数必须给合并函数;value 为null会直接抛 NPE(toMap内部用了HashMap.merge)。
10. 并行流(ParallelStream)一定比普通流快吗?(必问)
答: 不一定,甚至常常更慢。 原因:
- 共用线程池:并行流默认使用
ForkJoinPool.commonPool(),其并行度默认为CPU 核数 - 1,且全局共享——你和别人的代码、其他并行流会互相争抢线程,一个慢任务会拖垮所有使用者。 - 任务拆分与合并有开销:数据量小、单元素计算轻时,拆分开销远大于收益。
- 不适用于阻塞 I/O:commonPool 线程被阻塞会耗尽整个池,严重影响系统其他部分。
- 顺序陷阱:
forEach不保证顺序,需顺序时用forEachOrdered;有状态操作(如sorted、distinct)在并行下开销更大。 - 线程安全问题:parallel 下往普通
ArrayList里add()会出错,要用collect或并发容器。
// 适用场景:CPU 密集型、数据量大、无共享可变状态
long count = IntStream.rangeClosed(1, 10_000_000)
.parallel()
.filter(i -> isPrime(i))
.count();
// 想用自己的线程池隔离(不建议在生产滥用)
ForkJoinPool pool = new ForkJoinPool(8);
long result = pool.submit(() -> list.parallelStream().map(this::heavy).count()).join();面试标准回答:并行流是一个「可能提升性能的工具」,不是「性能开关」。判断依据是「CPU 密集 + 数据量大 + 无共享状态 + 我能接受 commonPool 被占用」,否则老老实实串行或用自定义线程池。
11. Stream 的常见坑有哪些?
答:
- 流只能消费一次:重复使用抛
IllegalStateException。 - 不要在流中修改外部状态:
peek/forEach里改外部集合,在并行流下是线程不安全的,也违背了「无副作用」原则。 peek不是给业务用的:它只为调试设计,在「没有终止操作」或「可被优化掉」时根本不执行。- 惰性求值导致语义变化:
filter与map的顺序会影响执行次数,把filter放前面通常更高效。
六、Optional
12. Optional 解决什么问题?orElse 和 orElseGet 有什么区别?(必问)
答: Optional 用类型系统显式表达「可能为空」,强制调用者处理空值,从而减少 NPE 与层层判空。
orElse vs orElseGet:
orElse(T other):无论 Optional 是否为空,other 都会被求值(急切求值)。orElseGet(Supplier<? extends T> supplier):只有为空时才调用 supplier(惰性求值)。
当兜底值是「创建对象 / 查库 / 远程调用」等昂贵操作时,用 orElseGet,否则会造成无谓开销。
User u = findUser();
// 即使 u 不为空,new User() 也已经创建了(浪费)
User a = Optional.ofNullable(u).orElse(new User());
// 只有为空才创建(推荐)
User b = Optional.ofNullable(u).orElseGet(User::new);
// 为空则抛异常
User c = Optional.ofNullable(u).orElseThrow(() -> new IllegalStateException("user not found"));13. Optional 的正确用法与常见误用?
答:
Optional<User> opt = findUser();
opt.isPresent();
opt.ifPresent(u -> System.out.println(u.getName())); // 有值才消费
opt.filter(u -> u.getAge() > 18).map(User::getName); // 链式转换
opt.map(User::getAddress).map(Address::getCity); // 嵌套取值,自动处理 null
opt.or(() -> Optional.of(defaultUser)); // Java 9+:为空时给另一个 Optional
opt.ifPresentOrElse(u -> log(u), () -> log("empty")); // Java 9+三条纪律:
- 不要把 Optional 用作字段、方法参数或集合元素(它是为返回值设计的,且不可序列化);
- 不要用
opt.get()直接取值,应使用orElseGet/orElseThrow/ifPresent; - 不要写成
if (opt.isPresent()) { ... }的变相判空,那样就退化成!= null了。
七、接口的默认方法与静态方法
14. 为什么引入默认方法?冲突时怎么解决?
答: 目的很明确:在不破坏已有实现类的前提下给接口新增方法。典型例子是 Java 8 给 Collection 加了 stream()、forEach()——如果直接加抽象方法,全世界的实现类都会编译失败。
菱形冲突的三条解决规则(依次判断):
- 类优先:类中的具体方法 > 接口的默认方法。
- 更具体的接口优先:如果多个接口冲突,且其中一个接口继承自另一个,则子接口的默认方法胜出。
- 必须显式重写:以上都不满足时,编译器报错,必须重写方法并可用
InterfaceName.super.method()指定调用哪个。
interface A { default String hello() { return "A"; } }
interface B { default String hello() { return "B"; } }
class C implements A, B {
@Override
public String hello() {
return A.super.hello() + B.super.hello(); // 必须显式指定
}
}interface Greeter {
default void hi() { System.out.println("default"); }
static Greeter of() { return new Greeter() {}; } // 接口静态方法,不会被继承
}注意:接口的
static方法不被实现类继承,只能通过接口名调用;default方法不能重写Object的方法(如equals/hashCode)。
八、新日期时间 API(java.time)
15. 新日期时间 API 相比 Date/Calendar 好在哪?(必问)
答:
| 对比项 | 旧 API(Date/Calendar/SimpleDateFormat) | 新 API(java.time,JSR-310) |
|---|---|---|
| 可变性 | 可变(setTime 能改),易被误改 | 不可变,线程安全 |
| 线程安全 | SimpleDateFormat 非线程安全,共享会踩坑 | DateTimeFormatter 线程安全 |
| 设计与命名 | 月份从 0 开始、Date 既表示日期又表示时间,混乱 | 职责清晰:LocalDate/LocalTime/LocalDateTime/Instant/ZonedDateTime |
| 时区 | 支持弱,易出错 | 显式 ZoneId,Instant 表示时间戳 |
| 计算能力 | 加减日期要写一堆 Calendar 代码 | 链式 plusDays/minusMonths/with |
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.*;
public class DateDemo {
public static void main(String[] args) {
LocalDate today = LocalDate.now(); // 2026-09-17
LocalDate birthday = LocalDate.of(1995, Month.MAY, 20);
// 计算
long days = ChronoUnit.DAYS.between(birthday, today);
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastDayOfMonth = today.with(TemporalAdjusters.lastDayOfMonth());
// 判断
boolean before = birthday.isBefore(today);
boolean leap = today.isLeapYear();
// 格式化与解析(线程安全,可作为常量复用)
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String text = LocalDateTime.now().format(fmt);
LocalDateTime parsed = LocalDateTime.parse("2026-09-17 10:30:00", fmt);
// 时间戳 <-> 新 API
Instant instant = Instant.now(); // UTC 时间戳
long epochMilli = instant.toEpochMilli();
LocalDateTime local = LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), ZoneId.systemDefault());
// 与旧 Date 互转
java.util.Date oldDate = java.util.Date.from(instant);
Instant back = oldDate.toInstant();
// 时区换算
ZonedDateTime beijing = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime tokyo = beijing.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println(tokyo);
}
}补充考点:
Instant是新旧 API 之间的「桥梁类型」,内部就是long epochSecond + int nano;Period用「年月日」度量,Duration用「时分秒纳秒」度量,别混用。
九、CompletableFuture
16. CompletableFuture 相比 Future 的改进?
答: Future 的两大痛点:
- 只能被动阻塞获取:
get()会阻塞,isDone()轮询很丑,没有回调机制。 - 无法组合编排:多个异步任务无法优雅地串行、并行、聚合、竞速。
CompletableFuture 实现了 Future + CompletionStage,提供回调式与函数式编排能力。
import java.util.concurrent.*;
import java.util.function.*;
public class CFDemo {
public static void main(String[] args) throws Exception {
// 1. 异步执行 + 回调(不回传值)
CompletableFuture.runAsync(() -> System.out.println("task run"));
// 2. 异步执行 + 有返回值(默认使用 ForkJoinPool.commonPool)
CompletableFuture<String> f = CompletableFuture.supplyAsync(() -> "hello");
// 3. 串行编排:thenApply(转换)/ thenAccept(消费)/ thenRun(不关心结果)
String result = f.thenApply(String::toUpperCase)
.thenApply(s -> s + "!")
.join(); // join 不抛受检异常
System.out.println(result); // HELLO!
// 4. 并行聚合:thenCombine(两个结果合并)
CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> 20);
int sum = a.thenCombine(b, Integer::sum).join(); // 30
// 5. 全量等待:allOf(注意返回 CompletableFuture<Void>,需自行取值)
CompletableFuture<Void> all = CompletableFuture.allOf(a, b);
all.join();
// 6. 竞速:任意一个完成即返回
Object any = CompletableFuture.anyOf(a, b).join();
// 7. 异常处理:handle(能拿到异常并回传值)/ exceptionally(异常兜底)
String safe = CompletableFuture.<String>supplyAsync(() -> {
throw new RuntimeException("boom");
})
.exceptionally(ex -> "fallback: " + ex.getMessage())
.join();
// 8. 自定义线程池(强烈建议:避免污染 commonPool)
ExecutorService pool = Executors.newFixedThreadPool(4);
CompletableFuture<String> withPool =
CompletableFuture.supplyAsync(() -> "pool task", pool);
System.out.println(withPool.join());
pool.shutdown();
}
}高频追问:
thenApplyvsthenCompose:前者是「同步转换」(T -> R),后者是「扁平化」(T -> CompletableFuture<R>),类比map与flatMap。thenApplyvsthenApplyAsync:不带Async的会在完成它的那个线程上执行;带Async的会提交到线程池(默认 commonPool)。- 生产环境必须传自定义线程池,否则 commonPool 被阻塞任务占满,会拖慢整个 JVM 的并行流与其它
CompletableFuture。
十、其他高频加分项
17. Java 8 还有哪些「顺手能提」的变化?
答:
- JVM 层面:永久代(PermGen)被元空间(Metaspace)取代。永久代在堆内、由
-XX:MaxPermSize限制;元空间使用本地内存、由-XX:MaxMetaspaceSize限制(默认不限),类元数据放元空间后「OOM: PermGen space」变成了「OOM: Metaspace」。相关:-XX:MetaspaceSize是首次触发 GC 的阈值而非初始大小。 HashMap优化:链表长度超过 8 且数组容量 ≥ 64 时转为红黑树,查询从 O(n) 降到 O(log n);扩容时用高低位拆分替代重新计算 hash。- 并发包增强:
LongAdder/DoubleAdder(分段 CAS,高竞争计数比AtomicLong快)、StampedLock(读写锁升级版,支持乐观读)。 String工具方法:String.join、chars、codePoints。- Base64:
java.util.Base64标准化,支持 Basic/URL/MIME 三种编码。 - 重复注解与类型注解:
@Repeatable支持同一位置重复标注;@Target(TYPE_USE)让注解可用于泛型、类型转换等任意类型使用点,为静态分析工具铺路。 Arrays.parallelSort:大数组并行排序。-parameters:编译时保留方法参数名(MyBatis 的@Param依赖它,否则参数名变成arg0)。- Nashorn JS 引擎(JDK 15 已移除,属于历史考点)。
一句话总结 Java 8:它让 Java 从「纯面向对象」变成了「面向对象 + 函数式」的混合范式语言,也是之后所有版本的语法地基。
