Skip to content

Java 8 核心特性总览 ​

S1-029必学入门约 60 分钟JDK 21编程语言基础 · Java 版本特性

1. 本节定位 ​

Java 8 是 Java 历史上变化最大的一次:它把"函数式"这套写法带进了语言,也重写了日期时间 API。

《Lambda 表达式》、《Stream API》、《Optional》 已经分散讲过,本节做两件事:把它们放回"版本"这条线上串起来,以及补上还没讲的日期时间 API——它是日常开发用得最多、旧代码里最容易出错的部分。

2. 学习目标 ​

完成本节后,你应该能够:

  • 说出 Java 8 的六大核心变化,并指出对应章节;
  • 用接口默认方法给已有接口扩展能力而不破坏实现类;
  • 用 LocalDate / LocalDateTime 做日期计算,替代手工加减毫秒;
  • 用 Duration、ChronoUnit 表达时间差;
  • 用 DateTimeFormatter 做格式化,并说清它为什么比 SimpleDateFormat 安全;
  • 说出旧 Date / Calendar 的三个坑。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

Java 8 是"分水岭":Java 8 之前和之后写出来的代码,看起来像两种语言。

java
// 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 开发,读到的存量项目代码、面试题、框架源码,绝大多数都建立在这套范式上。这一节是把它们串成体系的地方。

5. 核心概念 ​

5.1 Java 8 六大变化与对应章节 ​

变化一句话说明对应章节
Lambda 表达式把函数当参数传递《Lambda 表达式》
Stream API用声明式写法处理集合《Stream API》
Optional在类型上表达"可能为空"《Optional》
接口默认方法接口可以带实现,能演进本节 5.2
新日期时间 API不可变、线程安全、语义清晰本节 5.3
方法引用Lambda 的进一步简化《Lambda 表达式》

5.2 接口默认方法:让接口能演进 ​

问题场景:Collection 接口里想加一个 stream() 方法,但所有实现类都得改——这在库开发中不可接受。

Java 8 的解法:默认方法。

java
interface Greeter {
    String name();                                    // 抽象方法:实现类必须实现

    default String greet() {                          // 默认方法:自带实现
        return "你好," + name() + "!";
    }

    static Greeter of(String name) {                  // 静态方法:接口自己的工具方法
        return () -> name;
    }
}

三条规则:

  1. 实现类可以不重写默认方法,直接用;
  2. 实现类可以重写默认方法;
  3. 一个类实现两个接口、且两个接口有同签名的默认方法时,必须显式指定用哪个(接口名.super.方法名())。

这条特性的意义:它让接口"能长大"。List 在 Java 8 加了 sort、replaceAll,Java 9 加了 of——没有破坏任何一个已有的实现类。

5.3 新日期时间 API:本节重点 ​

旧 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]

常用操作:

java
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,否则跨时区会算错。

6. 递进示例 ​

6.1 最小示例:日期计算 ​

java
LocalDate today = LocalDate.of(2026, 9, 21);
LocalDate nextMonth = today.plusMonths(1);
System.out.println(ChronoUnit.DAYS.between(today, nextMonth));     // 30

6.2 业务示例:"3 天后的上午 9 点提醒" ​

java
LocalDateTime remindAt = LocalDateTime.now()
        .plusDays(3)
        .withHour(9)
        .withMinute(0)
        .withSecond(0);

对比旧 API 的写法:Calendar 要 new、要 set、要 add、要取回 Date——四步操作、五次可变修改。

6.3 反例:共享 SimpleDateFormat ​

java
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");

String format(Date date) {
    return FORMAT.format(date);        // 多线程下会返回错乱结果,甚至抛异常
}

修复:换成 DateTimeFormatter(不可变、线程安全),或者每次新建、用 ThreadLocal(后者是历史代码的补救方案)。

7. 完整可运行代码 ​

文件:labs/version-features/src/main/java/cn/kb/learn/version/Java8OverviewLesson.java

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();
    }
}

运行命令:

powershell
cd labs
./mvnw -pl version-features compile
java -cp version-features/target/classes cn.kb.learn.version.Java8OverviewLesson

预期输出:

text
默认方法: 你好,知识库!(来自接口的默认方法)
今天: 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同一时刻的两个时区表示,时刻没变、显示在变
结果种类: 14 个线程共享一个 DateTimeFormatter,4000 次格式化无一次错乱

最后一行是对比实验的关键:把它换成 SimpleDateFormat 会得到多种互不相同的结果(甚至抛 NumberFormatException)——这就是"线程不安全"的实际表现。

8. Debug 观察点 ​

  1. 在 today.plusMonths(1) 处按 F7 进入,观察它内部创建了新对象而不是修改 today——这是不可变设计的实现方式;
  2. 在 ChronoUnit.DAYS.between(...) 处 F7 进入,观察它如何逐月计算天数(而不是简单地"天数相减");
  3. 在 beijing.withZoneSameInstant(...) 处断点,对比两个对象的 toEpochSecond()——你会发现它们相等:时区不同,时刻相同;
  4. 在 shared.format(date) 处断点,观察 DateTimeFormatter 对象的字段都是 final 的——不可变对象天然线程安全。

9. 常见错误 ​

错误 1:以为 LocalDateTime 带时区 ​

java
LocalDateTime.now();                     // 服务器在哪个时区,就按哪个时区解释

修复:跨时区场景用 ZonedDateTime 或 Instant;存储到数据库时统一用 UTC 时间戳。

错误 2:在旧代码里继续共享 SimpleDateFormat ​

见 6.3 节。修复:改成 DateTimeFormatter 或每次新建。

错误 3:混用 Date 与新 API ​

java
new Date().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();    // 转换长链

建议:新代码统一用 java.time,只在必须与旧框架交互时做转换,并把转换集中在一处。

错误 4:月份从 0 开始(旧 API 的坑) ​

java
Calendar calendar = Calendar.getInstance();
calendar.set(2026, 9, 21);        // 实际是 10 月 21 日!月份从 0 开始

修复:用 LocalDate.of(2026, 9, 21)——新 API 的月份就是正常人理解的月份。

错误 5:用 == 比较 LocalDate ​

java
if (date1 == date2) { }           // 比较引用,通常为 false
if (date1.equals(date2)) { }      // 正确

新 API 重写了 equals,语义是"值相等"。

10. 动手实验 ​

目标:用新 API 重写一段旧代码。

步骤:

  1. 运行第 7 节代码,核对输出;把 formatterThreadSafety 里换成共享的 SimpleDateFormat,观察输出结果不再唯一(实验后改回来);
  2. 用 LocalDate 计算:你的下一个生日距今天有多少天、是星期几;
  3. 写方法 String humanize(Duration duration):把时长转成"2 小时 15 分钟"这样的中文描述,要求正确处理不足 1 小时、恰好整点等情况;
  4. 用 LocalDate 写方法 List<LocalDate> nextPaydays(LocalDate start, int count):返回从 start 开始的 count 个"每月 15 日"(提示:withDayOfMonth);
  5. 说明第 4 步如果遇到 2 月,withDayOfMonth(31) 会发生什么,以及你会怎么处理。

验收方式:能解释第 5 步的异常(DateTimeException)与你的兜底方案。

扩展任务:写一个"工作日计算"方法 LocalDate plusWorkingDays(LocalDate start, int days):跳过周六周日(不考虑法定节假日),加 N 个工作日。

11. 练习题 ​

基础题

  1. 判断:LocalDate.plusDays(1) 会修改原对象。(对 / 错)
  2. 填空:只有日期用 ______;日期 + 时间用 ______;需要时区用 ______。
  3. 简答:DateTimeFormatter 为什么比 SimpleDateFormat 安全?

代码阅读题

java
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) + " 分钟");
  1. 两行输出分别是什么?2026-01-31 加一个月为什么会得到这个结果?

编程题

  1. 写一个"借阅到期"工具:
    • 方法 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 日归还,逾期多少天。

综合任务

  1. 实现一个"课程排期"工具:
    • 输入:开始日期、课程节数、每周几上课(如周一、周三、周五);
    • 输出:每一节课的日期列表;
    • 要求:跳过法定节假日(用一个 Set<LocalDate> 传入);
    • 说明:为什么用 LocalDate 而不是 Date 让这个任务变简单了?
分级提示

提示 1(方向):第 4 题的关键是"月末加月"的对齐规则。

提示 2(关键点):第 6 题用 date.getDayOfWeek() 判断,配合 plusDays(1) 逐日推进即可(简单直接,不用复杂算法)。

提示 3(接近答案):第 6 题的循环条件是"已排课程数 < 节数",每轮 plusDays(1);命中星期几且不在节假日集合中就记一节。

12. 参考答案 ​

基础题
  1. 错。LocalDate 不可变,plusDays 返回新对象,原对象不变。
  2. LocalDate;LocalDateTime;ZonedDateTime(或 Instant)。
  3. 因为 DateTimeFormatter 不可变(所有字段 final,没有任何可修改状态)。而 SimpleDateFormat 内部有可变的日历对象,多个线程共享时会互相覆盖,导致结果错乱。"共享可变状态"正是线程不安全的根源——这条原则与 《线程与同步基础》 完全一致。
代码阅读题(第 4 题)
text
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。

编程题(第 5 题)
java
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) 归零——这是一个值得单测覆盖的边界。

预期输出:

text
借阅日: 2026 年 09 月 01 日 星期二
到期日: 2026 年 10 月 01 日 星期四
归还日: 2026 年 09 月 25 日 星期五
逾期天数: 0
10 月 5 日归还的逾期天数: 4
综合任务(第 6 题)
java
package 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 让这个任务变简单:

  1. 日期运算是一等公民:plusDays、getDayOfWeek 直接可用,不需要 Calendar 的 add + get 两步操作;
  2. 不可变:current = current.plusDays(1) 的写法不会意外修改其他引用持有的日期;
  3. 值语义清晰:可以直接放进 Set 做节假日判断(equals / hashCode 正确实现),旧 Date 用起来要处处小心;
  4. 与星期、月份等概念直接对应:DayOfWeek.MONDAY 比 Calendar.MONDAY(值是 2)可读得多。

预期输出:

text
第 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 日之后的周末——这正是"逐日推进 + 过滤"的直观效果。

13. 掌握标准 ​

  • [ ] 能说出 Java 8 的六大变化及其对应章节;
  • [ ] 能写出带默认方法与静态方法的接口,并解释它为什么让接口"可演进";
  • [ ] 能熟练使用 LocalDate / LocalDateTime / ZonedDateTime 做日期计算;
  • [ ] 知道 LocalDateTime 不带时区,以及这意味着什么;
  • [ ] 知道为什么该用 DateTimeFormatter 而不是 SimpleDateFormat。

14. 扩展资源 ​

15. 下一步学习 ​

《Java 9-21 新特性与 Record》:从 Java 8 走到 Java 21,看语言变得有多不一样。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引