Day34 | Java中的日期时间API

59 阅读6分钟

在Java8之前,在代码里处理日期和时间是一件让人头疼的事情。

java.util.Date和java.util.Calendar这对搭档存在着很多的问题:

Date类的月份从0开始(一月是0),星期从1开始(周日是1),很容易出错。Calendar的操作方式(add(), set())也很笨重。

Date和Calendar对象是可变的,可以被任意修改。在多线程环境下很容易出问题,出BUG了很难找。

SimpleDateFormat作为主要的格式化工具,不是线程安全的,程序员只能用ThreadLocal或同步锁来配合使用。

Date类本身不包含时区信息,但在toString()的时候又会使用JVM的默认时区,造成很多混乱。

后来就在Java8引入了全新的java.time包,基于Joda-Time库构建,给日期时间操作提供了很好的解决方案。

一、java.time的核心设计思想

所有java.time包里的核心类都是不可变的,任何修改操作(比如增加一天)都会返回一个新的实例,原始实例保持不变。这使得它们天然线程安全。

API把"人类可读的日期时间"和"机器使用的精确时间戳"明确分开,并清晰地处理有时区和无时区的场景。

像LocalDate、LocalTime、LocalDateTime这些类表示的时间就是我们日常说的时间:

今天是2025年7月16日下午3点半。

Instant是对时间轴上的一个点的抽象,更适合做时间差计算、系统时钟同步、日志排序等。

API的设计采用了链式调用风格,代码读起来更像自然语言。

现在的时间,往后加一天,把小时设置成10点,把分钟设成30分。

二、核心类

2.1 人类日期时间

Local系列处理的是不带时区的日期和时间,它代表的是我们日常所见的日历和时钟上的时间。

LocalDate: 只表示日期,如2025-07-15。

LocalTime: 只表示时间,如16:36:37。

LocalDateTime: 表示日期和时间的组合,如2025-07-15T16:36:37。

下面是一个简短的案例,包含了当前日期时间获取、创建指定日期时间、操作日期时间等方法。

package com.lazy.snail.day34;

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

/**
 * @ClassName Day34Demo
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/7/16 15:21
 * @Version 1.0
 */
public class Day34Demo {
    public static void main(String[] args) {
        // --- 获取当前日期和时间 ---
        LocalDate today = LocalDate.now();
        LocalTime now = LocalTime.now();
        LocalDateTime currentDateTime = LocalDateTime.now();
        System.out.println("今天日期: " + today);
        System.out.println("当前时间: " + now);
        System.out.println("当前日期时间: " + currentDateTime);

        // --- 创建指定的日期和时间 ---
        LocalDate birthday = LocalDate.of(2000, Month.JANUARY, 1);
        LocalTime meetingTime = LocalTime.of(10, 30, 0);
        System.out.println("生日: " + birthday);

        // --- 操作日期和时间 ---
        LocalDate nextWeek = today.plusWeeks(1);
        System.out.println("一周后的今天: " + nextWeek);

        LocalDateTime meetingDateTime = LocalDateTime.of(today, meetingTime);
        LocalDateTime nextMeeting = meetingDateTime.plusDays(7).withHour(11);
        System.out.println("下次会议时间: " + nextMeeting);
    }
}

2.2 机器时间戳

Instant代表的是时间线上的一个精确的点,以UTC为基准,从1970年1月1日0时0分0秒开始计算的秒数和纳秒数。

适合用来记录事件发生的时间戳、进行时间戳计算等机器级的应用。

来看一个案例:

package com.lazy.snail.day34;

import java.time.Duration;
import java.time.Instant;

/**
 * @ClassName Day34Demo2
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/7/16 15:38
 * @Version 1.0
 */
public class Day34Demo2 {
    public static void main(String[] args) {
        Instant start = Instant.now();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Instant end = Instant.now();

        System.out.println("操作开始时间戳: " + start);
        System.out.println("操作结束时间戳: " + end);

        Duration duration = Duration.between(start, end);
        System.out.println("操作耗时: " + duration.toMillis() + " 毫秒");
    }
}

案例里面用Thread.sleep模拟了一个耗时操作,使用Duration.between计算出耗时操作耗费的时间差值。

Thread.sleep(1000);就是让线程暂停了1秒,后面讲到线程时会详细讲。

2.3 带时区的日期和时间

如果你想精确处理特定时区的日期和时间时,就要使用ZonedDateTime。

它是由一个LocalDateTime和一个ZoneId(时区ID)组成的。

直接看案例:

package com.lazy.snail.day34;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

/**
 * @ClassName Day34Demo3
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/7/16 15:42
 * @Version 1.0
 */
public class Day34Demo3 {
    public static void main(String[] args) {
        ZoneId beijingZone = ZoneId.of("Asia/Shanghai");
        ZoneId parisZone = ZoneId.of("Europe/Paris");

        ZonedDateTime beijingTime = ZonedDateTime.now(beijingZone);
        System.out.println("当前北京时间: " + beijingTime);

        LocalDateTime localMeeting = LocalDateTime.of(2025, 7, 20, 14, 0);
        ZonedDateTime parisMeeting = localMeeting.atZone(parisZone);
        System.out.println("巴黎会议时间: " + parisMeeting);

        ZonedDateTime beijingMeeting = parisMeeting.withZoneSameInstant(beijingZone);
        System.out.println("对应的北京会议时间: " + beijingMeeting);
    }
}

通过ZoneId指定时区,获取特定时区的当前时间。

把一个本地时间转换为特定时区的时间。

不同时区时间之间的转换。

为什么我们经常听到的是北京时间,但是代码里定义的时区是Asia/Shanghai? “北京时间”是官方规定的法定时间标准。 IANA时区数据库历史上使用的是Asia/Shanghai,定的都是东八区(UTC+8)时间。

2.4 时间段的度量

Duration: 用于度量基于秒和纳秒的时间差,适合机器时间(如Instant, LocalTime, LocalDateTime)。

Period: 用于度量基于年、月、日的时间差,适合人类日历时间(如LocalDate)。

package com.lazy.snail.day34;

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

/**
 * @ClassName Day34Demo4
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/7/16 16:13
 * @Version 1.0
 */
public class Day34Demo4 {
    public static void main(String[] args) {
        LocalDate startDate = LocalDate.of(2025, 1, 20);
        LocalDate endDate = LocalDate.of(2026, 3, 15);
        Period period = Period.between(startDate, endDate);
        System.out.printf("两个日期相差: %d年, %d月, %d天\n",
                period.getYears(), period.getMonths(), period.getDays());

        LocalTime startTime = LocalTime.of(9, 0, 0);
        LocalTime endTime = LocalTime.of(10, 30, 30);
        Duration duration = Duration.between(startTime, endTime);
        System.out.println("两个时间相差: " + duration.toMinutes() + " 分钟");
    }
}

上半段是Period示例,下半段是Duration示例。

2.5 格式化

DateTimeFormatter替代了以前的SimpleDateFormat。

package com.lazy.snail.day34;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @ClassName Day34Demo5
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/7/16 16:15
 * @Version 1.0
 */
public class Day34Demo5 {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();

        String isoDateTime = now.format(DateTimeFormatter.ISO_DATE_TIME);
        System.out.println("ISO格式: " + isoDateTime);

        DateTimeFormatter customFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
        String customFormatted = now.format(customFormatter);
        System.out.println("自定义格式: " + customFormatted);

        String dateTimeStr = "2025年08月10日 20:30:00";
        LocalDateTime parsedDateTime = LocalDateTime.parse(dateTimeStr, customFormatter);
        System.out.println("解析后的日期时间: " + parsedDateTime);
    }
}

可以使用预定义的格式化器,DateTimeFormatter里面预定义了很多的格式化器。

既然有预定义的,当然也可以自定义,按照自定义的格式进行格式化。

把字符串按照自定义的格式转换成日期时间对象也是常见的操作。

三、TemporalAdjusters

可以看成一个工具类,这个类里提供了一些静态方法,可以用他们进行复杂的日期调整。

package com.lazy.snail.day34;

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

/**
 * @ClassName Day34Demo6
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/7/16 16:31
 * @Version 1.0
 */
public class Day34Demo6 {
    public static void main(String[] args) {
        LocalDate today = LocalDate.now();

        LocalDate firstDayOfMonth = today.with(TemporalAdjusters.firstDayOfMonth());
        System.out.println("本月第一天: " + firstDayOfMonth);

        LocalDate nextSunday = today.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
        System.out.println("下一个周日: " + nextSunday);
    }
}

获取本月的第一天,获取下一个周日。

结语

为什么我要单开一篇文章来讲一下Java里的日期时间的操作?

一是日期时间处理不管在什么系统中,都是绕不开的,在实际开发中确实很常用。

二是Java版本的迭代,确实对日期时间API进行了很大的改变。

下一篇预告

Day35 | Java多线程入门

如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!

更多文章请关注我的公众号《懒惰蜗牛工坊》