1. 概述

Java 8引入了全新的日期时间API,彻底解决了旧版java.util.Datejava.util.Calendar的痛点。本文将先分析旧API的问题,再详解新API如何解决这些缺陷,并重点介绍java.time包中的核心类:LocalDateLocalTimeLocalDateTimeZonedDateTimePeriodDuration及其关键用法。

2. 旧版日期时间API的问题

旧版API存在三大硬伤:

  • 线程安全缺失
    DateCalendar类非线程安全,开发者需自行处理并发问题,极易踩坑。而Java 8新API的所有类都是不可变且线程安全的,彻底告别并发噩梦。
  • API设计反人类
    旧API设计混乱,日常操作(如日期加减)需要写大量冗余代码。新API采用ISO标准,提供直观的领域模型(日期/时间/时间段),并内置大量实用方法。
  • 时区处理复杂
    旧API处理时区需要额外逻辑,新API通过LocalZoned系列类(如ZonedDateTime)原生支持时区操作。

3. 使用LocalDate、LocalTime和LocalDateTime

这三个类是使用频率最高的核心类,表示不带时区的本地日期/时间,适用于无需显式指定时区的场景。

3.1 LocalDate操作指南

LocalDate表示ISO格式的日期(yyyy-MM-dd),不含时间信息,适合存储生日、发薪日等场景。

基础创建

// 获取当前日期
LocalDate localDate = LocalDate.now();

// 指定日期创建(两种方式)
LocalDate.of(2015, 02, 20);          // 方法1:直接传参
LocalDate.parse("2015-02-20");      // 方法2:解析字符串

常用操作

// 日期加减
LocalDate tomorrow = LocalDate.now().plusDays(1);  // 加1天
LocalDate previousMonthSameDay = LocalDate.now().minus(1, ChronoUnit.MONTHS);  // 减1个月

// 获取日期信息
DayOfWeek sunday = LocalDate.parse("2016-06-12").getDayOfWeek();  // 获取星期几(返回枚举)
int twelve = LocalDate.parse("2016-06-12").getDayOfMonth();       // 获取当月第几天(返回int)

// 日期判断
boolean leapYear = LocalDate.now().isLeapYear();  // 是否闰年
boolean notBefore = LocalDate.parse("2016-06-12").isBefore(LocalDate.parse("2016-06-11"));  // 是否早于指定日期
boolean isAfter = LocalDate.parse("2016-06-12").isAfter(LocalDate.parse("2016-06-11"));    // 是否晚于指定日期

// 边界时间获取
LocalDateTime beginningOfDay = LocalDate.parse("2016-06-12").atStartOfDay();  // 当天开始时间(00:00)
LocalDate firstDayOfMonth = LocalDate.parse("2016-06-12").with(TemporalAdjusters.firstDayOfMonth());  // 当月第一天

3.2 LocalTime操作指南

LocalTime表示不含日期的时间,适合处理每日重复的时间场景(如闹钟时间)。

基础创建

// 获取当前时间
LocalTime now = LocalTime.now();

// 指定时间创建
LocalTime sixThirty = LocalTime.parse("06:30");  // 解析字符串
LocalTime sixThirty = LocalTime.of(6, 30);       // 直接传参

常用操作

// 时间加减
LocalTime sevenThirty = LocalTime.parse("06:30").plus(1, ChronoUnit.HOURS);  // 加1小时

// 获取时间分量
int six = LocalTime.parse("06:30").getHour();  // 获取小时

// 时间比较
boolean isbefore = LocalTime.parse("06:30").isBefore(LocalTime.parse("07:30"));  // 是否早于指定时间

// 特殊时间常量
LocalTime maxTime = LocalTime.MAX;  // 23:59:59.999999999(数据库查询常用)

3.3 LocalDateTime操作指南

LocalDateTime日期+时间的组合,最常用的日期时间类。

基础创建

// 获取当前日期时间
LocalDateTime.now();

// 指定日期时间创建
LocalDateTime.of(2015, Month.FEBRUARY, 20, 06, 30);  // 直接传参
LocalDateTime.parse("2015-02-20T06:30:00");          // 解析字符串

常用操作

// 日期时间加减
localDateTime.plusDays(1);    // 加1天
localDateTime.minusHours(2);  // 减2小时

// 获取分量
localDateTime.getMonth();  // 获取月份(返回Month枚举)

4. 使用ZonedDateTime API

当需要处理带时区的日期时间时,使用ZonedDateTimeZoneId表示时区标识符(全球约40个时区)。

时区操作

// 创建时区ID
ZoneId zoneId = ZoneId.of("Europe/Paris");

// 获取所有可用时区ID
Set<String> allZoneIds = ZoneId.getAvailableZoneIds();

// LocalDateTime转ZonedDateTime
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, zoneId);

// 解析带时区的字符串
ZonedDateTime.parse("2015-05-03T10:15:30+01:00[Europe/Paris]");

替代方案:OffsetDateTime

OffsetDateTime表示带UTC偏移量的日期时间(精度到纳秒):

// 创建基础时间
LocalDateTime localDateTime = LocalDateTime.of(2015, Month.FEBRUARY, 20, 06, 30);

// 添加偏移量(+2小时)
ZoneOffset offset = ZoneOffset.of("+02:00");
OffsetDateTime offSetByTwo = OffsetDateTime.of(localDateTime, offset);  // 结果:2015-02-20T06:30+02:00

5. 使用Period和Duration

  • Period:基于年/月/日的时间段
  • Duration:基于秒/纳秒的时间段

5.1 Period操作

// 创建日期
LocalDate initialDate = LocalDate.parse("2007-05-10");

// 日期加减(Period)
LocalDate finalDate = initialDate.plus(Period.ofDays(5));  // 加5天

// 获取时间段差值
int five = Period.between(initialDate, finalDate).getDays();  // 获取天数差
long five = ChronoUnit.DAYS.between(initialDate, finalDate); // 直接按天计算差值

5.2 Duration操作

// 创建时间
LocalTime initialTime = LocalTime.of(6, 30, 0);

// 时间加减(Duration)
LocalTime finalTime = initialTime.plus(Duration.ofSeconds(30));  // 加30秒

// 获取时间段差值
long thirty = Duration.between(initialTime, finalTime).getSeconds();  // 获取秒数差
long thirty = ChronoUnit.SECONDS.between(initialTime, finalTime);   // 直接按秒计算差值

6. 与Date和Calendar的兼容性

通过toInstant()方法实现旧API到新API的转换:

// Date转LocalDateTime
LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());

// Calendar转LocalDateTime
LocalDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault());

// 从Unix时间戳创建
LocalDateTime.ofEpochSecond(1465817690, 0, ZoneOffset.UTC);  // 结果:2016-06-13T11:34:50

7. 日期时间格式化

使用DateTimeFormatter实现灵活格式化:

// 创建日期时间
LocalDateTime localDateTime = LocalDateTime.of(2015, Month.JANUARY, 25, 6, 30);

// ISO格式化
String localDateString = localDateTime.format(DateTimeFormatter.ISO_DATE);  // 2015-01-25

// 自定义格式
localDateTime.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));  // 2015/01/25

// 本地化格式(英国风格)
localDateTime.format(
  DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
    .withLocale(Locale.UK)
);  // 25-Jan-2015, 06:30:00

8. 向后移植和替代方案

8.1 ThreeTen项目(Java 6/7兼容)

为尚未升级Java 8的项目提供新API的向后移植:

<dependency>
    <groupId>org.threeten</groupId>
    <artifactId>threetenbp</artifactId>
    <version>1.3.1</version>
</dependency>

8.2 Joda-Time库(经典替代)

Java 8日期时间API的灵感来源,功能高度重合:

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.9.4</version>
</dependency>

9. 总结

Java 8日期时间API通过线程安全设计直观的API模型原生时区支持,彻底解决了旧版API的痛点。核心类LocalDate/LocalTime/LocalDateTime覆盖了80%的日常场景,ZonedDateTime处理复杂时区需求,Period/Duration提供精确的时间段计算。建议新项目直接采用新API,旧项目可通过ThreeTen或Joda-Time平滑过渡。

本文代码示例已上传至Java 8 Date/Time示例库


原始标题:Introduction to the Java 8 Date/Time API

» 下一篇: Java周报,131