主题
界面
主题
Java 8 是 Java 历史上变化最大的一次:它把"函数式"这套写法带进了语言,也重写了日期时间 API。
《Lambda 表达式》、《Stream API》、《Optional》 已经分散讲过,本节做两件事:把它们放回"版本"这条线上串起来,以及补上还没讲的日期时间 API——它是日常开发用得最多、旧代码里最容易出错的部分。
完成本节后,你应该能够:
LocalDate / LocalDateTime 做日期计算,替代手工加减毫秒;Duration、ChronoUnit 表达时间差;DateTimeFormatter 做格式化,并说清它为什么比 SimpleDateFormat 安全;Date / Calendar 的三个坑。Java 8 是"分水岭":Java 8 之前和之后写出来的代码,看起来像两种语言。
// Java 7 风格
List<String> result = new ArrayList<>();
for (Order order : orders) {
if (order.isPaid()) {
result.add(order.getName().toUpperCase());
}
}
// Java 8 风格
List<String> result = orders.stream()
.filter(Order::isPaid)
.map(order -> order.getName().toUpperCase())
.toList();即使你现在用 JDK 21 开发,读到的存量项目代码、面试题、框架源码,绝大多数都建立在这套范式上。这一节是把它们串成体系的地方。
| 变化 | 一句话说明 | 对应章节 |
|---|---|---|
| Lambda 表达式 | 把函数当参数传递 | 《Lambda 表达式》 |
| Stream API | 用声明式写法处理集合 | 《Stream API》 |
| Optional | 在类型上表达"可能为空" | 《Optional》 |
| 接口默认方法 | 接口可以带实现,能演进 | 本节 5.2 |
| 新日期时间 API | 不可变、线程安全、语义清晰 | 本节 5.3 |
| 方法引用 | Lambda 的进一步简化 | 《Lambda 表达式》 |
问题场景:Collection 接口里想加一个 stream() 方法,但所有实现类都得改——这在库开发中不可接受。
Java 8 的解法:默认方法。
interface Greeter {
String name(); // 抽象方法:实现类必须实现
default String greet() { // 默认方法:自带实现
return "你好," + name() + "!";
}
static Greeter of(String name) { // 静态方法:接口自己的工具方法
return () -> name;
}
}三条规则:
接口名.super.方法名())。这条特性的意义:它让接口"能长大"。List 在 Java 8 加了 sort、replaceAll,Java 9 加了 of——没有破坏任何一个已有的实现类。
旧 API(Date、Calendar、SimpleDateFormat)的三个经典问题:
| 问题 | 旧 API | 新 API |
|---|---|---|
| 可变 | Date 能被修改(值传出去就被改) | LocalDate 等不可变,所有"修改"都返回新对象 |
| 非线程安全 | SimpleDateFormat 共享会出错 | DateTimeFormatter 线程安全 |
| 语义混乱 | 月份从 0 开始、Date 既表示日期又表示时间戳 | 日期、时间、时刻、时区分开建模 |
四个核心类型:
| 类型 | 表示 | 例子 |
|---|---|---|
LocalDate | 只有日期 | 2026-09-21 |
LocalTime | 只有时间 | 14:30 |
LocalDateTime | 日期 + 时间(无时区) | 2026-09-21T14:30 |
ZonedDateTime | 日期 + 时间 + 时区 | 2026-09-21T14:30+08:00[Asia/Shanghai] |
常用操作:
LocalDate today = LocalDate.of(2026, 9, 21);
today.plusMonths(1); // 返回新对象,today 不变
ChronoUnit.DAYS.between(today, nextMonth); // 相差天数
LocalDateTime meeting = LocalDateTime.of(2026, 9, 21, 14, 30);
meeting.plusMinutes(90);
Duration.ofMinutes(95).toHours(); // 时长换算
DateTimeFormatter.ofPattern("yyyy 年 MM 月 dd 日 EEEE");
today.format(formatter);选择建议:
| 场景 | 用哪个 |
|---|---|
| 生日、账单日(只有日期) | LocalDate |
| 会议时间(本地时间,不含时区) | LocalDateTime |
| 跨时区协作、日志时间戳 | ZonedDateTime / Instant |
| 计算两个时刻的间隔 | Duration(时间)/ Period(日期) |
一个易混点:LocalDateTime 不带时区——它表示"墙上时钟的时间"。如果你的系统有多个时区,就必须用 ZonedDateTime 或 Instant,否则跨时区会算错。
LocalDate today = LocalDate.of(2026, 9, 21);
LocalDate nextMonth = today.plusMonths(1);
System.out.println(ChronoUnit.DAYS.between(today, nextMonth)); // 30LocalDateTime remindAt = LocalDateTime.now()
.plusDays(3)
.withHour(9)
.withMinute(0)
.withSecond(0);对比旧 API 的写法:Calendar 要 new、要 set、要 add、要取回 Date——四步操作、五次可变修改。
SimpleDateFormat private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");
String format(Date date) {
return FORMAT.format(date); // 多线程下会返回错乱结果,甚至抛异常
}修复:换成 DateTimeFormatter(不可变、线程安全),或者每次新建、用 ThreadLocal(后者是历史代码的补救方案)。
文件:labs/version-features/src/main/java/cn/kb/learn/version/Java8OverviewLesson.java
package cn.kb.learn.version;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
public class Java8OverviewLesson {
/** 接口的默认方法与静态方法:接口终于可以带实现了 */
interface Greeter {
String name();
default String greet() {
return "你好," + name() + "!(来自接口的默认方法)";
}
static Greeter of(String name) {
return () -> name;
}
}
/** 共享一个 formatter 做并发格式化,验证它的线程安全性 */
static void formatterThreadSafety() throws InterruptedException {
DateTimeFormatter shared = DateTimeFormatter.ofPattern("yyyy 年 MM 月 dd 日");
LocalDate date = LocalDate.of(2026, 9, 21);
List<String> results = Collections.synchronizedList(new ArrayList<>());
Thread[] threads = new Thread[4];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1_000; j++) {
results.add(shared.format(date));
}
});
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
Set<String> distinct = new HashSet<>(results);
System.out.println("共享 DateTimeFormatter 格式化 " + results.size() + " 次,"
+ "结果种类: " + distinct.size() + "(" + distinct.iterator().next() + ")");
}
public static void main(String[] args) throws InterruptedException {
// 1. 接口的默认方法与静态方法
Greeter greeter = Greeter.of("知识库");
System.out.println("默认方法: " + greeter.greet());
// 2. 新日期时间 API:不可变对象
LocalDate today = LocalDate.of(2026, 9, 21);
LocalDate nextMonth = today.plusMonths(1);
System.out.println("今天: " + today + "(" + today.getDayOfWeek() + ")");
System.out.println("一个月后: " + nextMonth);
System.out.println("相差: " + ChronoUnit.DAYS.between(today, nextMonth) + " 天");
LocalDate modified = today.plusDays(1);
System.out.println("原对象不变: " + today + ",返回的是新对象: " + modified);
// 3. 时间计算:直接用方法而不是手工加减毫秒
LocalDateTime meeting = LocalDateTime.of(2026, 9, 21, 14, 30);
System.out.println("会议时间: " + meeting + ",加 90 分钟后: " + meeting.plusMinutes(90));
Duration duration = Duration.ofMinutes(95);
System.out.println("时长换算: " + duration.toHours() + " 小时 " + (duration.toMinutes() % 60) + " 分钟");
// 4. 格式化与本地化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy 年 MM 月 dd 日 EEEE", Locale.CHINA);
System.out.println("格式化: " + today.format(formatter));
// 5. 时区:显式携带时区信息,不做隐式转换
ZonedDateTime beijing = ZonedDateTime.of(meeting, ZoneId.of("Asia/Shanghai"));
ZonedDateTime london = beijing.withZoneSameInstant(ZoneId.of("Europe/London"));
System.out.println("北京: " + beijing);
System.out.println("伦敦(同一时刻): " + london);
// 6. 线程安全:唯一需要小心的不是 format,而是"共享可变状态"
formatterThreadSafety();
}
}运行命令:
cd labs
./mvnw -pl version-features compile
java -cp version-features/target/classes cn.kb.learn.version.Java8OverviewLesson预期输出:
默认方法: 你好,知识库!(来自接口的默认方法)
今天: 2026-09-21(MONDAY)
一个月后: 2026-10-21
相差: 30 天
原对象不变: 2026-09-21,返回的是新对象: 2026-09-22
会议时间: 2026-09-21T14:30,加 90 分钟后: 2026-09-21T16:00
时长换算: 1 小时 35 分钟
格式化: 2026 年 09 月 21 日 星期一
北京: 2026-09-21T14:30+08:00[Asia/Shanghai]
伦敦(同一时刻): 2026-09-21T07:30+01:00[Europe/London]
共享 DateTimeFormatter 格式化 4000 次,结果种类: 1(2026 年 09 月 21 日)三处值得对照:
| 输出 | 含义 |
|---|---|
原对象不变: 2026-09-21,返回的是新对象: 2026-09-22 | 不可变性——这是新 API 能线程安全的根本原因 |
北京: ...+08:00 → 伦敦: ...+01:00 | 同一时刻的两个时区表示,时刻没变、显示在变 |
结果种类: 1 | 4 个线程共享一个 DateTimeFormatter,4000 次格式化无一次错乱 |
最后一行是对比实验的关键:把它换成 SimpleDateFormat 会得到多种互不相同的结果(甚至抛 NumberFormatException)——这就是"线程不安全"的实际表现。
today.plusMonths(1) 处按 F7 进入,观察它内部创建了新对象而不是修改 today——这是不可变设计的实现方式;ChronoUnit.DAYS.between(...) 处 F7 进入,观察它如何逐月计算天数(而不是简单地"天数相减");beijing.withZoneSameInstant(...) 处断点,对比两个对象的 toEpochSecond()——你会发现它们相等:时区不同,时刻相同;shared.format(date) 处断点,观察 DateTimeFormatter 对象的字段都是 final 的——不可变对象天然线程安全。LocalDateTime 带时区 LocalDateTime.now(); // 服务器在哪个时区,就按哪个时区解释修复:跨时区场景用 ZonedDateTime 或 Instant;存储到数据库时统一用 UTC 时间戳。
SimpleDateFormat 见 6.3 节。修复:改成 DateTimeFormatter 或每次新建。
Date 与新 API new Date().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); // 转换长链建议:新代码统一用 java.time,只在必须与旧框架交互时做转换,并把转换集中在一处。
Calendar calendar = Calendar.getInstance();
calendar.set(2026, 9, 21); // 实际是 10 月 21 日!月份从 0 开始修复:用 LocalDate.of(2026, 9, 21)——新 API 的月份就是正常人理解的月份。
== 比较 LocalDate if (date1 == date2) { } // 比较引用,通常为 false
if (date1.equals(date2)) { } // 正确新 API 重写了 equals,语义是"值相等"。
目标:用新 API 重写一段旧代码。
步骤:
formatterThreadSafety 里换成共享的 SimpleDateFormat,观察输出结果不再唯一(实验后改回来);LocalDate 计算:你的下一个生日距今天有多少天、是星期几;String humanize(Duration duration):把时长转成"2 小时 15 分钟"这样的中文描述,要求正确处理不足 1 小时、恰好整点等情况;LocalDate 写方法 List<LocalDate> nextPaydays(LocalDate start, int count):返回从 start 开始的 count 个"每月 15 日"(提示:withDayOfMonth);withDayOfMonth(31) 会发生什么,以及你会怎么处理。验收方式:能解释第 5 步的异常(DateTimeException)与你的兜底方案。
扩展任务:写一个"工作日计算"方法 LocalDate plusWorkingDays(LocalDate start, int days):跳过周六周日(不考虑法定节假日),加 N 个工作日。
基础题
LocalDate.plusDays(1) 会修改原对象。(对 / 错)DateTimeFormatter 为什么比 SimpleDateFormat 安全?代码阅读题
LocalDate date = LocalDate.of(2026, 1, 31);
LocalDate next = date.plusMonths(1);
System.out.println(date + " → " + next);
Duration duration = Duration.ofSeconds(3725);
System.out.println(duration.toHours() + " 小时 " + (duration.toMinutes() % 60) + " 分钟");2026-01-31 加一个月为什么会得到这个结果?编程题
LocalDate dueDate(LocalDate borrowDate, int days):借阅 days 天后的到期日;long overdueDays(LocalDate dueDate, LocalDate returnDate):逾期天数(未逾期返回 0);String format(LocalDate date):输出"2026 年 09 月 21 日 星期一";main 演示:9 月 1 日借 30 天,9 月 25 日归还,逾期多少天。综合任务
Set<LocalDate> 传入);LocalDate 而不是 Date 让这个任务变简单了?提示 1(方向):第 4 题的关键是"月末加月"的对齐规则。
提示 2(关键点):第 6 题用 date.getDayOfWeek() 判断,配合 plusDays(1) 逐日推进即可(简单直接,不用复杂算法)。
提示 3(接近答案):第 6 题的循环条件是"已排课程数 < 节数",每轮 plusDays(1);命中星期几且不在节假日集合中就记一节。
LocalDate 不可变,plusDays 返回新对象,原对象不变。LocalDate;LocalDateTime;ZonedDateTime(或 Instant)。DateTimeFormatter 不可变(所有字段 final,没有任何可修改状态)。而 SimpleDateFormat 内部有可变的日历对象,多个线程共享时会互相覆盖,导致结果错乱。"共享可变状态"正是线程不安全的根源——这条原则与 《线程与同步基础》 完全一致。2026-01-31 → 2026-02-28
1 小时 2 分钟关于"月末加月":2026-01-31 加一个月,2 月没有 31 日,Java 的处理规则是对齐到该月最后一天(2026-02-28)。这不是 bug,而是规范行为——但需要你知道,否则遇到"月末对账"这类需求会算错。
第二个输出:3725 秒 = 62 分钟 5 秒 = 1 小时 2 分钟 5 秒,toHours() 得 1、toMinutes() % 60 得 2。
package cn.kb.learn.version;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Locale;
public class BorrowTool {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy 年 MM 月 dd 日 EEEE", Locale.CHINA);
static LocalDate dueDate(LocalDate borrowDate, int days) {
return borrowDate.plusDays(days);
}
static long overdueDays(LocalDate dueDate, LocalDate returnDate) {
long days = ChronoUnit.DAYS.between(dueDate, returnDate);
return Math.max(days, 0);
}
static String format(LocalDate date) {
return date.format(FORMATTER);
}
public static void main(String[] args) {
LocalDate borrow = LocalDate.of(2026, 9, 1);
LocalDate due = dueDate(borrow, 30);
LocalDate returned = LocalDate.of(2026, 9, 25);
System.out.println("借阅日: " + format(borrow));
System.out.println("到期日: " + format(due));
System.out.println("归还日: " + format(returned));
System.out.println("逾期天数: " + overdueDays(due, returned));
LocalDate late = LocalDate.of(2026, 10, 5);
System.out.println("10 月 5 日归还的逾期天数: " + overdueDays(due, late));
}
}注意 overdueDays 的返回值:ChronoUnit.DAYS.between(due, returned) 在 returned 早于 due 时是负数,用 Math.max(days, 0) 归零——这是一个值得单测覆盖的边界。
预期输出:
借阅日: 2026 年 09 月 01 日 星期二
到期日: 2026 年 10 月 01 日 星期四
归还日: 2026 年 09 月 25 日 星期五
逾期天数: 0
10 月 5 日归还的逾期天数: 4package cn.kb.learn.version;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class CourseScheduler {
static List<LocalDate> schedule(LocalDate start, int lessons,
Set<DayOfWeek> weekdays, Set<LocalDate> holidays) {
List<LocalDate> dates = new ArrayList<>();
LocalDate current = start;
while (dates.size() < lessons) {
if (weekdays.contains(current.getDayOfWeek()) && !holidays.contains(current)) {
dates.add(current);
}
current = current.plusDays(1);
}
return dates;
}
public static void main(String[] args) {
LocalDate start = LocalDate.of(2026, 9, 21);
Set<DayOfWeek> weekdays = Set.of(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY);
Set<LocalDate> holidays = Set.of(
LocalDate.of(2026, 10, 1), LocalDate.of(2026, 10, 2));
List<LocalDate> dates = schedule(start, 8, weekdays, holidays);
for (int i = 0; i < dates.size(); i++) {
System.out.println("第 " + (i + 1) + " 节: " + dates.get(i)
+ "(" + dates.get(i).getDayOfWeek() + ")");
}
}
}为什么 LocalDate 让这个任务变简单:
plusDays、getDayOfWeek 直接可用,不需要 Calendar 的 add + get 两步操作;current = current.plusDays(1) 的写法不会意外修改其他引用持有的日期;Set 做节假日判断(equals / hashCode 正确实现),旧 Date 用起来要处处小心;DayOfWeek.MONDAY 比 Calendar.MONDAY(值是 2)可读得多。预期输出:
第 1 节: 2026-09-21(MONDAY)
第 2 节: 2026-09-23(WEDNESDAY)
第 3 节: 2026-09-25(FRIDAY)
第 4 节: 2026-09-28(MONDAY)
第 5 节: 2026-09-30(WEDNESDAY)
第 6 节: 2026-10-05(MONDAY)
第 7 节: 2026-10-07(WEDNESDAY)
第 8 节: 2026-10-09(FRIDAY)注意第 5 节到第 6 节跳过了 10 月 1、2 日(节假日),也跳过了 10 月 2 日之后的周末——这正是"逐日推进 + 过滤"的直观效果。
LocalDate / LocalDateTime / ZonedDateTime 做日期计算;LocalDateTime 不带时区,以及这意味着什么;DateTimeFormatter 而不是 SimpleDateFormat。《Java 9-21 新特性与 Record》:从 Java 8 走到 Java 21,看语言变得有多不一样。