前端视角 Java Web 入门手册 3.2:常用工具—— 时间与日期

206 阅读6分钟

Java Time API

在 Java 8 之前时间日期处理主要使用 Date、Calendar、SimpleDateFormat 对象,但这几个对象存在明显设计缺陷

  • 可变性:Date 和 Calendar 都是可变的,容易导致线程安全问题。
  • 混淆性:在 Calendar 中,月份是从 0 开始计数的,这容易引起混淆。
  • 设计缺陷:日期、时间和时区数据之间缺乏清晰的分离。

Java 8 引入了 Java Time API(java.time 包),受到 Joda-Time 库的启发,旨在解决前述类库的不足,提供:

  • 不可变性:所有类都是不可变的,线程安全。
  • 清晰的分离:不同的类分别处理日期、时间和时区。
  • 流畅的 API:更具可读性和直观性的开发接口。
  • 全面的功能:涵盖几乎所有与日期和时间相关的使用场景。

核心组件

java.time 包提供了几个核心组件:

  1. LocalDate:表示不含时间和时区的日期。
  2. LocalTime:表示不含日期和时区的时间。
  3. LocalDateTime:结合日期和时间,不含时区。
  4. ZonedDateTime:结合日期、时间和时区。
  5. Instant:表示时间线上的一个点(时间戳),以 UTC 为基准。
  6. DurationPeriod:分别表示基于时间和日期的时间段。

LocalDate

表示不含时间和时区的日期,如 2025-02-12,对象包含几个关键方法

  • now():获取当前日期。
  • of(int year, int month, int dayOfMonth):通过年、月、日创建日期。
  • plusDays(long days):日期加天数。
  • minusMonths(long months):日期减月份。
  • format(DateTimeFormatter formatter):格式化日期。
import java.time.LocalDate;

public class LocalDateExample {
    public static void main(String[] args) {
        LocalDate today = LocalDate.now();
        LocalDate birthday = LocalDate.of(1990, 5, 20);
        
        System.out.println("今天的日期: " + today);
        System.out.println("生日: " + birthday);
    }
}

LocalTime

表示不含日期和时区的时间,如 15:30:45,对象包含几个关键方法

  • now():获取当前时间。
  • of(int hour, int minute, int second):通过时、分、秒创建时间。
  • plusHours(long hours):时间加小时。
  • minusMinutes(long minutes):时间减分钟。
  • format(DateTimeFormatter formatter):格式化时间。
import java.time.LocalTime;

public class LocalTimeExample {
    public static void main(String[] args) {
        LocalTime now = LocalTime.now();
        LocalTime meetingTime = LocalTime.of(14, 30, 0);
        
        System.out.println("当前时间: " + now);
        System.out.println("会议时间: " + meetingTime);
    }
}

LocalDateTime

结合日期和时间,不含时区,如 2025-02-12T15:30:45,对象包含几个关键方法

  • now():获取当前日期和时间。
  • of(int year, int month, int day, int hour, int minute):创建日期时间
  • plusDays(long days):日期时间加天数。
  • minusHours(long hours):日期时间减小时。
  • format(DateTimeFormatter formatter):格式化日期时间。
import java.time.LocalDateTime;

public class LocalDateTimeExample {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime future = LocalDateTime.of(2026, 1, 1, 0, 0);
        
        System.out.println("当前日期和时间: " + now);
        System.out.println("未来日期和时间: " + future);
    }
}

ZonedDateTime

ZonedDateTime 结合日期、时间和时区,如 2025-02-12T15:30:45+08:00[Asia/Shanghai]对象包含几个关键方法

  • now(ZoneId zone):获取指定时区的当前日期和时间。
  • of(int year, int month, int day, int hour, int minute, int second, int nanoOfSecond, ZoneId zone):创建带时区的日期时间实例。
  • withZoneSameInstant(ZoneId zone):更改时区同时保持瞬间不变。
  • format(DateTimeFormatter formatter):格式化带时区的日期时间。

ZoneOffset:表示相对于 UTC 的固定偏移量,如 +02:00。

// 创建北京时区的 ZoneId 对象,时区标识符,如 Asia/Shanghai 或 Europe/London。
ZoneId bjZone = ZoneId.of("Asia/Beijing");

// 创建北京当前时间的 ZonedDateTime 对象
ZonedDateTime bjTime = ZonedDateTime.now(bjZone);

ZonedDateTime utcTime = bjTime.atZone(ZoneId.of("UTC"));
import java.time.ZonedDateTime;
import java.time.ZoneId;

public class ZonedDateTimeExample {
    public static void main(String[] args) {
        ZonedDateTime nowInUTC = ZonedDateTime.now(ZoneId.of("UTC"));
        ZonedDateTime nowInShanghai = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
        
        System.out.println("当前 UTC 时间: " + nowInUTC);
        System.out.println("当前上海时间: " + nowInShanghai);
    }
}

Instant

在 Java 里纪元秒是从 UTC 1970 年 1 月 1 日 00:00:00 开始到指定时刻所经过的秒数

表示时间线上的一个点(时间戳),以 UTC 为基准,对象包含的关键方法

  • now():获取当前时间戳。
  • ofEpochSecond(long epochSecond):根据纪元秒创建时间戳。
  • getEpochSecond():获取时间戳的纪元秒。
  • format(DateTimeFormatter formatter):格式化时间戳。
import java.time.Instant;

public class InstantExample {
    public static void main(String[] args) {
        Instant now = Instant.now();
        Instant epoch = Instant.ofEpochSecond(0);

        // 当前时间戳: 2025-02-12T05:43:18.959453Z
        System.out.println("当前时间戳: " + now);

        // 纪元时间戳: 1970-01-01T00:00:00Z
        System.out.println("纪元时间戳: " + epoch);
    }
}

Duration 与 Period

  • Period:表示基于日期的时间段,如“2 天”或“3 个月”。
  • Duration:表示基于时间的时间段,如“5 小时”或“30 分钟”。

Period 对象可以通过静态方法 Period.of() 创建

// 3年10个月20天的 Period 对象
Period period = Period.of(3, 10, 20);

Duration 类表示时间上的一个持续时间,可以用来表示小时、分钟、秒、毫秒这样的时间段

// 3小时20分钟的 Duration 对象
Duration duration = Duration.ofHours(3).plusMinutes(20);

可以利用 Period 和 Duration 对象计算日期和时间之间的差距

LocalDate date1 = LocalDate.of(2023, 1, 1);
LocalDate date2 = LocalDate.of(2023, 11, 22);

// 计算两个 LocalDate 对象的日期差
Period period = Period.between(date1, date2);

System.out.println(
    period.getYears() + " years, " + 
    period.getMonths() + " months, " + 
    period.getDays() + " days");

LocalDateTime time1 = LocalDateTime.of(2023, 01, 01, 10, 0);
LocalDateTime time2 = LocalDateTime.of(2023, 05, 01, 10, 0);

// 计算两个 LocalDateTime 对象之间的时间差
Duration duration = Duration.between(time1, time2);

System.out.println(
    duration.toHours() + " hours, " + 
    duration.toMinutes() % 60 + " minutes, " + 
    duration.getSeconds() % 60 + " seconds");

DataTimeFormatter

java.time.format.DateTimeFormatter 类提供了灵活且线程安全的方式来格式化和解析日期时间对象。

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

public class FormattingExample {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy HH:mm:ss");
        String formattedDate = now.format(formatter);

        // 格式化后的日期: Wednesday, February 12, 2025 13:55:49
        System.out.println("格式化后的日期: " + formattedDate);
    }
}

解析字符串为日期时间对象

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

public class ParsingExample {
    public static void main(String[] args) {
        String dateStr = "2025-02-12 13:30:45";
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime dateTime = LocalDateTime.parse(dateStr, formatter);

        // 解析后的日期时间: 2025-02-12T13:30:45
        System.out.println("解析后的日期时间: " + dateTime);
    }
}

常见的日期时间格式模式

  • yyyy:年份(如 2025)
  • MM:月份(01-12)
  • dd:月份中的日(01-31)
  • HH:24 小时制的小时(00-23)
  • hh:12 小时制的小时(01-12)
  • mm:分钟(00-59)
  • ss:秒(00-59)
  • a:上午/下午标记(AM/PM)
  • EEEE:星期几的全称(如 Wednesday)
  • MMM:月份的缩写(如 Oct)
  • MMMM:月份的全称(如 October)
  • z:时区缩写(如 EST)
  • Z:相对于 UTC 的偏移量(如 -05:00)
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class MultiplePatternsExample {
    public static void main(String[] args) {
        LocalDate date = LocalDate.now();
        LocalTime time = LocalTime.now();
        
        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("hh:mm a");
        
        String formattedDate = date.format(dateFormatter);
        String formattedTime = time.format(timeFormatter);

        // 格式化后的日期: 12/02/2025
        System.out.println("格式化后的日期: " + formattedDate);
        
        // 格式化后的时间: 03:30 PM
        System.out.println("格式化后的时间: " + formattedTime);
    }
}

夏令时

Java 在使用正确的 ZoneId 时会自动调整夏令时,java.time 包中的类与 IANA(Internet Assigned Numbers Authority)时区数据库紧密集成,该数据库包含了全球各个时区的详细信息,其中就包括夏令时和冬令时的规则。这些规则会根据不同国家和地区的法律规定进行更新。

import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

public class DSTExample {
    public static void main(String[] args) {
        ZonedDateTime beforeDST = ZonedDateTime.of(2023, 3, 12, 1, 30, 0, 0, ZoneId.of("America/New_York"));
        ZonedDateTime afterDST = beforeDST.plusHours(1);
        
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z");
        // 夏令时前: 2023-03-12 01:30 EST
        System.out.println("夏令时前: " + beforeDST.format(formatter));

        // 夏令时后: 2023-03-12 03:30 EDT
        System.out.println("夏令时后: " + afterDST.format(formatter));
    }
}