V
Vel·ToolKit
简洁 · 高效 · 即开即用
ZH
第 17 章 / 共 20 章

日期时间 API

java.time:Local/Zoned/Instant、Duration vs Period、格式化与解析

为什么不用 Date / Calendar

Java 8 之前的 Date / Calendar 是历史包袱:可变(线程不安全)、月份从 0 开始、设计混乱。新项目一律用 java.time(JSR-310,借鉴 Joda-Time):不可变、线程安全、API 清晰。下文所有示例都基于 java.time。

  • 应用场景:用户注册时间、订单倒计时、定时任务调度、跨时区会议、计费周期
  • 核心类速记:LocalDate(日期)/ LocalTime(时间)/ LocalDateTime(无时区日期+时间)/ ZonedDateTime(带时区)/ Instant(epoch 时间戳)/ Duration(时间长度,秒/纳秒)/ Period(年月日长度)

LocalDate / LocalTime / LocalDateTime

三个"不带时区"的类:LocalDate 只有日期、LocalTime 只有时间、LocalDateTime 两者都有。适合表达"业务日期"(生日、营业时间),与具体时区无关。

// LocalDemo.java
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;

public class LocalDemo {
    public static void main(String[] args) {
        LocalDate today = LocalDate.now();
        LocalDate birthday = LocalDate.of(1990, Month.MAY, 12);
        System.out.println(today + " " + birthday);

        LocalTime noon = LocalTime.of(12, 0);
        LocalTime now  = LocalTime.now();
        System.out.println(noon + " " + now);

        LocalDateTime meeting = LocalDateTime.of(2026, 5, 12, 14, 30);
        System.out.println(meeting);

        // 加减:返回新对象
        System.out.println(today.plusDays(7));
        System.out.println(today.minusMonths(1));
        System.out.println(today.withDayOfMonth(1));  // 本月 1 号

        // 字段访问
        System.out.println("year=" + today.getYear() + " month=" + today.getMonthValue() + " day=" + today.getDayOfMonth());
        System.out.println("weekday=" + today.getDayOfWeek());
    }
}

ZonedDateTime 与时区

应用场景:要展示"上海现在几点"、"伦敦同事看到的时间"、跨时区会议。ZonedDateTime = LocalDateTime + ZoneId。同一个瞬间,在不同 ZoneId 下显示的时钟数字不同。

// ZonedDemo.java
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class ZonedDemo {
    public static void main(String[] args) {
        ZoneId sh = ZoneId.of("Asia/Shanghai");
        ZoneId ny = ZoneId.of("America/New_York");

        ZonedDateTime nowSh = ZonedDateTime.now(sh);
        ZonedDateTime nowNy = nowSh.withZoneSameInstant(ny);  // 同一瞬间,换时区
        System.out.println(nowSh);
        System.out.println(nowNy);

        // 它们的 Instant 相等
        System.out.println(nowSh.toInstant().equals(nowNy.toInstant())); // true
    }
}

Instant:epoch 时间戳

应用场景:和外部系统传值、写入数据库的时间戳列、日志排序、计算时间差。Instant 是"距 1970-01-01 UTC 的纳秒数"——无时区、绝对的瞬间。

// InstantDemo.java
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class InstantDemo {
    public static void main(String[] args) {
        Instant now = Instant.now();
        System.out.println(now);                  // 2026-05-12T08:30:00.123Z
        System.out.println(now.toEpochMilli());   // Unix 毫秒
        System.out.println(now.getEpochSecond()); // Unix 秒

        // Instant <-> ZonedDateTime
        ZonedDateTime sh = now.atZone(ZoneId.of("Asia/Shanghai"));
        System.out.println(sh);

        // 从 epoch 毫秒还原
        Instant fromMs = Instant.ofEpochMilli(1_715_000_000_000L);
        System.out.println(fromMs);
    }
}

Duration vs Period

Duration 表达"基于秒/纳秒的精确时间长度"(5 小时、200 毫秒);Period 表达"日历层面的年月日长度"(2 个月、3 年)。两者不可互换:"一个月"长度不固定(28~31 天),不能用 Duration 表达。

// DurationPeriod.java
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;

public class DurationPeriod {
    public static void main(String[] args) {
        // Duration:秒/纳秒
        Duration d = Duration.ofMinutes(90).plusSeconds(30);
        System.out.println(d);                 // PT1H30M30S
        System.out.println(d.toMinutes() + "m"); // 90m

        LocalTime t1 = LocalTime.of(9, 0);
        LocalTime t2 = LocalTime.of(17, 30);
        Duration worked = Duration.between(t1, t2);
        System.out.println(worked.toHours() + "h");

        // Period:年月日
        Period p = Period.of(2, 6, 0);            // 2 年 6 月
        LocalDate from = LocalDate.of(2020, 1, 1);
        LocalDate to   = LocalDate.of(2026, 5, 12);
        Period age = Period.between(from, to);
        System.out.printf("%d年%d月%d天%n", age.getYears(), age.getMonths(), age.getDays());
    }
}

DateTimeFormatter:格式化与解析

应用场景:前后端 API 时间字段、日志时间戳、CSV/Excel 导出。提供常用格式(ISO_DATE / ISO_DATE_TIME / RFC_1123)和自定义 pattern。**与 SimpleDateFormat 不同,DateTimeFormatter 是线程安全的,可以做 static final 字段全局共享**。

// FormatDate.java
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class FormatDate {
    static final DateTimeFormatter CN = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm");

    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.of(2026, 5, 12, 14, 30);

        // 标准 ISO
        System.out.println(now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        // 自定义
        System.out.println(now.format(CN));
        // 简写
        System.out.println(now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));

        // 解析
        LocalDate d = LocalDate.parse("2026-05-12");
        System.out.println(d);

        LocalDateTime dt = LocalDateTime.parse(
            "2026-05-12 14:30",
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
        );
        System.out.println(dt);
    }
}

ChronoUnit:精确单位计算

应用场景:算两个日期间的天数 / 小时数 / 月数。ChronoUnit.X.between(a, b) 比手算 Duration / Period 字段更清晰。

// ChronoBetween.java
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

public class ChronoBetween {
    public static void main(String[] args) {
        LocalDate signup  = LocalDate.of(2024, 3, 1);
        LocalDate today   = LocalDate.of(2026, 5, 12);
        System.out.println(ChronoUnit.DAYS.between(signup, today));   // 总天数
        System.out.println(ChronoUnit.MONTHS.between(signup, today)); // 26

        LocalDateTime start = LocalDateTime.of(2026, 5, 12, 9,  0);
        LocalDateTime end   = LocalDateTime.of(2026, 5, 12, 14, 30);
        System.out.println(ChronoUnit.MINUTES.between(start, end));   // 330
    }
}

常用日期计算

// DateMath.java
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;

public class DateMath {
    public static void main(String[] args) {
        LocalDate today = LocalDate.of(2026, 5, 12);

        System.out.println(today.with(TemporalAdjusters.firstDayOfMonth()));      // 月初
        System.out.println(today.with(TemporalAdjusters.lastDayOfMonth()));       // 月末
        System.out.println(today.with(TemporalAdjusters.next(DayOfWeek.MONDAY))); // 下周一
        System.out.println(today.with(TemporalAdjusters.firstDayOfNextMonth()));  // 下月 1 号

        // 闰年
        System.out.println(today.isLeapYear());
    }
}