JDK 8 新增特性、使用方法与面试要点
JDK 8是企业Java开发最重要的分水岭。它不只是增加Lambda语法,还同时改变了集合处理、接口演进、日期时间、异步任务、并发统计、注解和JVM内存实现。
一、特性全景
| 类别 | 新增或重要变化 | 典型用途 |
|---|---|---|
| 语言 | Lambda、方法引用、类型注解、重复注解 | 行为参数化、注解增强 |
| 接口 | 默认方法、静态方法 | 接口兼容演进 |
| 集合 | Stream、Spliterator、Map新方法 | 过滤、聚合、并行拆分 |
| 空值 | Optional | 返回值表达缺失 |
| 时间 | java.time | 日期、时间点、时区和周期 |
| 异步 | CompletableFuture | 异步编排和并行聚合 |
| 并发 | LongAdder、StampedLock | 热点计数、读多写少 |
| 工具 | Base64、Arrays并行操作 | 编解码、批量处理 |
| JVM | Metaspace替代PermGen | 类元数据改用本地内存 |
| 集合实现 | HashMap树化、ConcurrentHashMap重构 | 极端冲突和并发性能 |
二、Lambda与函数式接口
Lambda把“行为”作为参数传递:
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class LambdaDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("Tom", "Amy", "Jerry");
names.sort(Comparator.comparingInt(String::length)
.thenComparing(String::compareTo));
System.out.println(names);
}
}函数式接口只有一个抽象方法:
@FunctionalInterface
interface PriceRule {
long calculate(long originalCent);
}
PriceRule discount = originalCent -> originalCent * 90 / 100;常用标准接口:
| 接口 | 输入 | 输出 | 场景 |
|---|---|---|---|
Predicate<T> | T | boolean | 过滤、校验 |
Function<T,R> | T | R | 映射转换 |
Consumer<T> | T | void | 消费、回调 |
Supplier<T> | 无 | T | 延迟创建 |
UnaryOperator<T> | T | T | 同类型转换 |
BinaryOperator<T> | T、T | T | 归并 |
Lambda通常通过invokedynamic和LambdaMetafactory链接,不应简单回答为“编译成一个匿名内部类”。被捕获的局部变量必须是final或effectively final,因为Lambda可能在方法返回后才执行,不能直接共享可变栈变量。
三、方法引用
| 形式 | 示例 | 等价Lambda |
|---|---|---|
| 静态方法 | Integer::parseInt | s -> Integer.parseInt(s) |
| 特定对象实例方法 | logger::info | s -> logger.info(s) |
| 任意对象实例方法 | String::length | s -> s.length() |
| 构造器 | ArrayList::new | () -> new ArrayList<>() |
方法引用适合目标明确的简单转发;如果需要多步业务逻辑,普通Lambda或命名方法更易读。
四、Stream
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
List<Order> orders = loadOrders();
Map<String, Long> amountByCity = orders.stream()
.filter(order -> order.getStatus() == OrderStatus.PAID)
.collect(Collectors.groupingBy(
Order::getCity,
Collectors.summingLong(Order::getAmountCent)));Stream执行链:
flowchart LR
A["数据源"] --> B["中间操作filter/map/sorted"]
B --> C["终止操作collect/reduce/count"]
C --> D["触发遍历并产生结果"]重要规则:
- Stream不是数据结构,不保存数据。
- 中间操作通常是惰性的,没有终止操作就不执行。
- 一个Stream只能消费一次。
- 不要在
map、filter里修改外部共享状态。 - JDK 8没有
Stream.toList(),使用collect(Collectors.toList())。 parallelStream()不保证更快,默认使用公共ForkJoinPool。- Stream中逐条调用数据库或远程接口会制造N+1问题。
常用Collector
Map<Long, Order> byId = orders.stream()
.collect(Collectors.toMap(
Order::getId,
Function.identity(),
(oldValue, newValue) -> newValue));toMap遇到重复Key会抛异常,生产代码必须明确合并规则。groupingBy适合一对多分组,partitioningBy只分true/false两组。
五、Optional
Optional<User> user = repository.findById(userId);
String city = user
.map(User::getAddress)
.map(Address::getCity)
.orElse("UNKNOWN");正确边界:
| 推荐 | 不推荐 |
|---|---|
| 方法返回值表达可能不存在 | Entity/DTO每个字段都包Optional |
map、flatMap组合 | 无判断直接get() |
orElseGet延迟创建默认值 | 把Optional变量本身设为null |
orElse会提前计算参数,orElseGet只在缺值时调用Supplier:
User user = optional.orElseGet(this::loadDefaultUser);六、接口默认方法和静态方法
interface PaymentService {
PaymentResult pay(PaymentCommand command);
default void validate(PaymentCommand command) {
if (command == null || command.getAmountCent() <= 0) {
throw new IllegalArgumentException("支付金额必须大于0");
}
}
static String normalizeChannel(String channel) {
return channel.trim().toUpperCase();
}
}默认方法让已经有大量实现类的接口增加方法而不立即破坏二进制兼容。冲突规则:类方法优先;更具体接口优先;两个无继承关系接口提供同签名默认方法时,实现类必须显式覆盖。
七、java.time日期时间API
| 类型 | 表达什么 | 示例 |
|---|---|---|
LocalDate | 无时区日期 | 生日、账单日 |
LocalTime | 无时区时间 | 每日营业时间 |
LocalDateTime | 无时区日期时间 | 本地业务展示时间 |
Instant | UTC时间线上的瞬间 | 事件时间戳 |
ZonedDateTime | 带地区时区和夏令时规则 | 跨地区会议 |
OffsetDateTime | 带固定UTC偏移 | API协议时间 |
Duration | 时间线上的秒/纳秒间隔 | 接口耗时 |
Period | 年/月/日日期间隔 | 账期、年龄 |
Instant createdAt = Instant.now();
ZonedDateTime shanghai = createdAt.atZone(ZoneId.of("Asia/Shanghai"));
String text = shanghai.format(DateTimeFormatter.ISO_ZONED_DATE_TIME);不要用LocalDateTime表达全球唯一时间点,因为它不包含时区或偏移。DateTimeFormatter不可变且线程安全,可以安全复用;SimpleDateFormat则不是线程安全的。
八、CompletableFuture
ExecutorService executor = Executors.newFixedThreadPool(20);
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
() -> userClient.get(userId), executor);
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(
() -> orderClient.list(userId), executor);
CompletableFuture<UserView> result = userFuture.thenCombine(
orderFuture, UserView::new);
UserView view = result
.exceptionally(ex -> fallback(userId, ex))
.join();| 方法 | 作用 |
|---|---|
thenApply | 同步转换上一步结果 |
thenCompose | 串联返回Future的异步操作,避免嵌套 |
thenCombine | 合并两个独立Future |
allOf | 等待多个任务全部完成 |
anyOf | 任一个完成后继续 |
handle | 同时处理结果和异常 |
JDK 8没有orTimeout和completeOnTimeout,常需借助调度器、HTTP客户端超时或框架能力实现总时限。生产必须指定业务线程池,不要把阻塞IO无界地提交给公共ForkJoinPool。
九、Map和集合API增强
Map<String, Long> counter = new HashMap<>();
counter.merge("PAID", 1L, Long::sum);
User user = users.computeIfAbsent(userId, this::loadUser);
orders.removeIf(order -> order.isExpired());
orders.sort(Comparator.comparing(Order::getCreatedAt));| 方法 | 用途 | 注意事项 |
|---|---|---|
getOrDefault | 缺失时给默认值 | 不写入Map |
putIfAbsent | 缺失时放入 | HashMap本身仍非线程安全 |
computeIfAbsent | 按Key延迟创建 | 映射函数不要做慢调用或递归更新同Key |
merge | 累计和合并 | 合并函数返回null会删除映射 |
forEach | 遍历 | 不适合复杂控制流 |
removeIf | 按条件删除 | 修改原集合 |
十、并发增强
LongAdder
LongAdder success = new LongAdder();
success.increment();
long current = success.sum();高竞争计数下,LongAdder通过分散热点降低对单个CAS变量的竞争;sum()不是与并发更新线性一致的原子快照,因此适合监控计数,不适合要求精确读改写语义的账户余额。
StampedLock
long stamp = lock.tryOptimisticRead();
double currentX = x;
double currentY = y;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}StampedLock支持乐观读,但不可重入,必须使用正确stamp解锁,使用复杂度高。普通业务优先选择不可变数据、ConcurrentHashMap或ReentrantReadWriteLock,确认读竞争瓶颈后再评估。
十一、Base64和其他工具API
String encoded = Base64.getUrlEncoder().withoutPadding()
.encodeToString("order:1001".getBytes(StandardCharsets.UTF_8));
byte[] decoded = Base64.getUrlDecoder().decode(encoded);Base64是编码而不是加密,不能用于保护密码或Token秘密。
Arrays.parallelSort、Arrays.parallelPrefix等并行数组操作适合足够大的纯内存计算;小数组或共享机器上不一定优于串行处理。
十二、注解增强
重复注解:
@Role("ADMIN")
@Role("AUDITOR")
public class AuditService {}类型注解可以出现在类型使用位置,主要为静态分析框架提供信息。-parameters编译参数可以在Class文件中保留方法参数名,便于反射和框架参数绑定,但会改变元数据可见性和产物大小。
十三、集合底层变化
HashMap
JDK 8使用数组+链表+红黑树。桶中节点达到树化阈值且数组容量达到要求时才树化;容量过小时优先扩容。HashMap仍然不是线程安全容器,红黑树不能解决并发put的数据竞争。
ConcurrentHashMap
JDK 8移除JDK 7的Segment结构,采用Node数组、CAS插入空桶、冲突时synchronized锁桶头、链表/红黑树和多线程协助扩容。面试不能把Segment和桶级锁说成同一版本实现。
十四、JVM变化:PermGen到Metaspace
| JDK 7常见HotSpot | JDK 8常见HotSpot |
|---|---|
| 类元数据主要在PermGen | 类元数据主要在本地内存Metaspace |
常见PermGen space | 常见Metaspace |
-XX:MaxPermSize | -XX:MaxMetaspaceSize |
移除永久代不表示不会发生类元数据OOM。动态代理、CGLIB、脚本、热部署和ClassLoader泄漏仍可能撑满Metaspace。
十五、Nashorn和JavaScript
JDK 8提供Nashorn JavaScript引擎,但它后来被废弃并从后续JDK移除。不要在新业务中把它当成长期稳定的平台能力;从JDK 8升级时应扫描javax.script和Nashorn专有API依赖。
十六、常见误区
| 误区 | 正确理解 |
|---|---|
| Lambda一定比匿名类快 | 语义和实现不同,性能必须测量 |
| Stream一定比for快 | Stream重在表达能力,不承诺更快 |
| parallelStream适合所有大集合 | 受任务粒度、公共池、阻塞和CPU影响 |
| Optional解决所有NPE | 它只显式表达缺失,错误使用仍会NPE |
| CompletableFuture会自动创建线程 | 实际由指定Executor或公共池执行 |
| HashMap树化后线程安全 | 数据结构优化与并发安全无关 |
| Metaspace使用本地内存所以无限 | 仍受机器、容器和配置限制 |
十七、面试回答
JDK 8主要新增了什么
JDK 8引入Lambda、方法引用、函数式接口和Stream,改变集合处理方式;引入Optional和新的java.time API;接口支持默认方法和静态方法;CompletableFuture支持异步任务组合;并发库增加LongAdder、StampedLock;HashMap增加红黑树,ConcurrentHashMap移除Segment并改为CAS加桶级锁;HotSpot移除PermGen并使用Metaspace承载主要类元数据。
Stream和集合有什么区别
集合负责保存数据,Stream描述一次数据处理流水线;中间操作通常惰性,终止操作才触发遍历。Stream只能消费一次,不应在操作中依赖共享可变状态,也不保证比for循环快。
CompletableFuture生产怎么用
明确业务线程池和队列,区分串行组合、并行聚合,设置下游与整体超时,处理异常和取消,避免在公共ForkJoinPool执行阻塞IO,并用指标监控active、queue、reject和任务耗时。
十八、关联学习
| 专题 | 内容 |
|---|---|
| LTS版本演进总览 | JDK 8、11、17、21对比和选型 |
| JDK 7与8差异 | 老项目写法和底层实现对比 |
| Lambda与Stream | 函数式编程深入 |
| 日期时间API | java.time类型和时区 |
| CompletableFuture全过程 | 异步执行链、线程池和异常 |
| ConcurrentHashMap全过程 | CAS、桶锁和协助扩容 |
本章小结
JDK 8的价值不在于“代码可以少写几行”,而在于让行为参数化、声明式数据处理、类型安全时间、异步组合和并发统计进入标准库。生产使用必须同时理解惰性执行、线程池、共享状态、时间语义和集合线程安全边界。
