baeldung 2023-10-13
1. 概述
Java 8 引入了全新的日期和时间 API,以解决旧版 java.util.Date 和 java.util.Calendar 存在的问题。
在本教程中,我们将首先探讨现有 Date 和 Calendar API 中存在的问题,并讨论 Java 8 新的日期/时间 API 是如何解决这些问题的。
我们还将介绍 Java 8 中属于 java.time 包的一些核心类,例如 LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Period、Duration 以及它们所支持的 API。
2. 现有日期/时间 API 的问题
线程安全性:
Date和Calendar类不是线程安全的,这使得开发者不得不处理难以调试的并发问题,并编写额外代码来保证线程安全。相比之下,Java 8 引入的新日期/时间 API 是不可变且线程安全的,从而消除了这一并发难题。API 设计与易用性:旧的 Date 和 Calendar API 设计不佳,缺乏执行日常操作所需的足够方法。而新的日期/时间 API 以 ISO 标准为中心,采用一致的领域模型来表示日期、时间、持续时间和周期,并提供了大量实用方法来支持最常见的操作。
时区处理:使用旧 API 时,开发者需要编写额外逻辑来处理时区;而新 API 则通过
LocalDateTime和ZonedDateTime等类直接内置了对时区的支持。
3. 使用 LocalDate、LocalTime 和 LocalDateTime
最常用的类是 LocalDate、LocalTime 和 LocalDateTime。顾名思义,它们从观察者的上下文中表示本地日期/时间。
当上下文中不需要显式指定时区时,我们通常会使用这些类。本节将介绍其中一些最常用的 API。
3.1 使用 LocalDate
LocalDate 表示 ISO 格式(yyyy-MM-dd)的日期,不含时间部分。可用于存储生日、发薪日等日期信息。
可通过系统时钟创建当前日期的实例:
LocalDate localDate = LocalDate.now();
也可以使用 of() 方法或 parse() 方法创建特定年月日的 LocalDate 实例。
例如,以下代码片段都表示 2015 年 2 月 20 日:
LocalDate.of(2015, 02, 20);
LocalDate.parse("2015-02-20");
LocalDate 提供了多种实用方法来获取各种信息。下面快速浏览一些常用 API:
获取当前日期并加一天:
LocalDate tomorrow = LocalDate.now().plusDays(1);获取当前日期并减去一个月(注意这里使用了枚举作为时间单位):
LocalDate previousMonthSameDay = LocalDate.now().minus(1, ChronoUnit.MONTHS);解析日期 “2016-06-12” 并分别获取星期几和该月的第几天(前者返回
DayOfWeek对象,后者返回int值):DayOfWeek sunday = LocalDate.parse("2016-06-12").getDayOfWeek(); int twelve = LocalDate.parse("2016-06-12").getDayOfMonth();判断某日期是否处于闰年(例如当前日期):
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"));获取某日期对应的时间边界:
获取当天的开始时刻(2016-06-12T00:00):
LocalDateTime beginningOfDay = LocalDate.parse("2016-06-12").atStartOfDay();获取当月的第一天(2016-06-01):
LocalDate firstDayOfMonth = LocalDate.parse("2016-06-12") .with(TemporalAdjusters.firstDayOfMonth());
接下来,我们看看如何处理本地时间。
3.2 使用 LocalTime
LocalTime 表示不带日期的时间。
与 LocalDate 类似,我们可以从系统时钟创建 LocalTime 实例,也可以使用 parse() 和 of() 方法。
以下是一些常用 API 示例:
从系统时钟创建当前
LocalTime实例:LocalTime now = LocalTime.now();通过解析字符串创建表示早上 6:30 的
LocalTime:LocalTime sixThirty = LocalTime.parse("06:30");使用工厂方法
of()创建同样的时间:LocalTime sixThirty = LocalTime.of(6, 30);解析字符串后加一小时(结果为早上 7:30):
LocalTime sevenThirty = LocalTime.parse("06:30").plus(1, ChronoUnit.HOURS);使用 getter 方法获取具体的时间单位(如小时、分钟、秒):
int six = LocalTime.parse("06:30").getHour();比较两个时间的先后(以下代码返回
true):boolean isBefore = LocalTime.parse("06:30").isBefore(LocalTime.parse("07:30"));获取一天中的最大、最小和正午时间(常用于数据库查询):
LocalTime maxTime = LocalTime.MAX; // 23:59:59.999999999
现在我们深入了解一下 LocalDateTime。
3.3 使用 LocalDateTime
LocalDateTime 用于表示日期和时间的组合,是我们需要同时处理日期和时间时最常用的类。
该类提供了丰富的 API。以下是一些最常用的示例:
从系统时钟获取当前
LocalDateTime:LocalDateTime.now();使用
of()和parse()工厂方法创建表示 2015 年 2 月 20 日早上 6:30 的实例:LocalDateTime.of(2015, Month.FEBRUARY, 20, 06, 30); LocalDateTime.parse("2015-02-20T06:30:00");支持对时间单位(如天、月、年、分钟)进行加减操作:
localDateTime.plusDays(1); localDateTime.minusHours(2);提供 getter 方法提取特定时间单位(如下例返回
Month.FEBRUARY):localDateTime.getMonth();
4. 使用 ZonedDateTime API
当我们需要处理带时区的日期和时间时,Java 8 提供了 ZonedDateTime。ZoneId 是用于表示不同时区的标识符。目前大约有 40 个不同的时区,ZoneId 按如下方式表示它们。
例如,创建巴黎的时区:
ZoneId zoneId = ZoneId.of("Europe/Paris");
获取所有可用时区 ID:
Set<String> allZoneIds = ZoneId.getAvailableZoneIds();
将 LocalDateTime 转换为特定时区:
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, zoneId);
ZonedDateTime 还提供 parse() 方法来解析带时区的日期时间字符串:
ZonedDateTime.parse("2015-05-03T10:15:30+01:00[Europe/Paris]");
另一种处理时区的方式是使用 OffsetDateTime。OffsetDateTime 是一个带偏移量的不可变日期时间表示,它以纳秒精度存储所有日期和时间字段,以及相对于 UTC/Greenwich 的偏移量。
可以通过 ZoneOffset 创建 OffsetDateTime 实例。例如:
LocalDateTime localDateTime = LocalDateTime.of(2015, Month.FEBRUARY, 20, 06, 30);
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
Period 常用于修改给定日期或计算两个日期之间的差值:
LocalDate initialDate = LocalDate.parse("2007-05-10");
LocalDate finalDate = initialDate.plus(Period.ofDays(5));
Period 类提供了 getYears()、getMonths() 和 getDays() 等 getter 方法。
例如,以下代码返回 5(天数差):
int five = Period.between(initialDate, finalDate).getDays();
也可以使用 ChronoUnit.between() 按特定单位(如天、月、年)获取两个日期之间的差值:
long five = ChronoUnit.DAYS.between(initialDate, finalDate); // 返回 5
5.2 使用 Duration
与 Period 类似,Duration 用于处理时间。
例如,创建早上 6:30 的 LocalTime,然后加上 30 秒:
LocalTime initialTime = LocalTime.of(6, 30, 0);
LocalTime finalTime = initialTime.plus(Duration.ofSeconds(30));
可以使用 Duration.between() 获取两个时间点之间的差值(以秒为单位):
long thirty = Duration.between(initialTime, finalTime).getSeconds();
也可以使用 ChronoUnit.SECONDS.between() 达到相同效果:
long thirty = ChronoUnit.SECONDS.between(initialTime, finalTime);
6. 与 Date 和 Calendar 的兼容性
Java 8 添加了 toInstant() 方法,可将现有的 Date 和 Calendar 实例转换为新的日期/时间 API:
LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
LocalDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault());
还可以通过纪元秒(epoch second)构造 LocalDateTime。以下代码将生成 2016-06-13T11:34:50:
LocalDateTime.ofEpochSecond(1465817690, 0, ZoneOffset.UTC);
7. 日期和时间格式化
Java 8 提供了便捷的日期/时间格式化 API:
LocalDateTime localDateTime = LocalDateTime.of(2015, Month.JANUARY, 25, 6, 30);
使用 ISO 日期格式进行格式化(结果为 2015-01-25):
String localDateString = localDateTime.format(DateTimeFormatter.ISO_DATE);
DateTimeFormatter 提供了多种标准格式选项。
也可以使用自定义模式(如下返回 2015/01/25):
localDateTime.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
还可以传入格式样式(如 SHORT、LONG 或 MEDIUM):
localDateTime
.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
.withLocale(Locale.UK));
// 输出示例:25-Jan-2015, 06:30:00
8. Java 8 核心日期/时间 API 的替代方案
8.1 使用 ThreeTen 项目
对于正在从 Java 7 或 Java 6 升级到 Java 8 的组织,若希望提前使用新的日期/时间 API,ThreeTen 项目提供了向后移植(backport)功能。
开发者可以使用该项目中的类实现与 Java 8 日期/时间 API 相同的功能。一旦升级到 Java 8,只需切换包即可。
Maven 依赖如下:
<dependency>
<groupId>org.threeten</groupId>
<artifactId>threetenbp</artifactId>
<version>1.3.1</version>
</dependency>
8.2 Joda-Time 库
另一个 Java 8 日期/时间库的替代方案是 Joda-Time。事实上,Java 8 的日期/时间 API 正是由 Joda-Time 的作者 Stephen Colebourne 与 Oracle 共同主导开发的。该库几乎提供了 Java 8 日期/时间项目所支持的所有功能。
Maven 依赖如下:
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.4</version>
</dependency>
9. 结论
Java 8 提供了一套功能丰富、设计一致的日期/时间 API,极大简化了开发工作。