要访问完整的JAVA中文教程请点击这里:Java™教程
Java SE 8发布时引入的日期时间包java.time,提供了一个全面的日期和时间模型,并在JSR 310:日期和时间API下开发。尽管java.time基于国际标准化组织(ISO)日历系统,但也支持常用的全球日历。
本教程介绍了使用基于ISO的类来表示日期和时间以及操作日期和时间值的基本知识。
- 日期时间概述
- 标准日历
- 概述
- DayOfWeek和Month枚举
- 日期类
- 日期和时间类
- 时区和偏移类
- Instant类
- 解析和格式化
- Temporal包
- Period和Duration
- Clock
- 非ISO日期转换
- 遗留日期时间代码
- 总结
- 问题和练习:日期时间API
日期时间概述
时间似乎是一个简单的主题;即使是一只廉价的手表也可以提供相对准确的日期和时间。然而,仔细观察后,您会意识到微妙的复杂性和许多影响您对时间的理解的因素。例如,将一个月加到1月31日的结果对闰年和其他年份是不同的。时区也增加了复杂性。例如,一个国家可能会在短时间内或多次一年内进入和退出夏令时,或者对于某一年完全跳过夏令时。
日期时间API使用ISO-8601中定义的日历系统作为默认日历。此日历基于公历系统,在全球范围内被用作事实上的日期和时间表示标准。日期时间API的核心类名为LocalDateTime、ZonedDateTime和OffsetDateTime。所有这些都使用ISO日历系统。如果您想使用另一种日历系统,如伊斯兰历或泰国佛教历,java.time.chrono包允许您使用其中一个预定义的日历系统。或者您可以创建自己的日历系统。
日期时间API使用Unicode通用语言环境数据存储库(CLDR)。该存储库支持世界上的语言,并包含全球最大的可用区域设置数据集合。该存储库中的信息已本地化为数百种语言。日期时间API还使用时区数据库(TZDB)。该数据库提供有关自1970年以来全球每个时区更改的信息,并提供主要时区自该概念引入以来的历史。
日期时间设计原则
日期时间API是使用几个设计原则开发的。
清晰
API中的方法定义明确,行为清晰可预测。例如,使用null参数值调用日期时间方法通常会触发NullPointerException。
流畅
日期时间API提供流畅的接口,使代码易于阅读。由于大多数方法不允许使用null值参数,并且不返回null值,因此方法调用可以链接在一起,生成的代码可以快速理解。例如:
LocalDate today = LocalDate.now(); LocalDate payday = today.with(TemporalAdjusters.lastDayOfMonth()).minusDays(2);
不可变
日期时间API中的大多数类创建的对象是不可变的,意味着在对象创建后,它们不能被修改。要更改不可变对象的值,必须构建一个修改后的副本作为原始对象的副本。这也意味着日期时间API在定义上是线程安全的。这影响API的方式是,用于创建日期或时间对象的大多数方法以
of
、from
或with
为前缀,而不是构造函数,并且没有set
方法。例如:LocalDate dateOfBirth = LocalDate.of(2012, Month.MAY, 14); LocalDate firstBirthday = dateOfBirth.plusYears(1);
可扩展
日期时间API在尽可能的情况下是可扩展的。例如,您可以定义自己的时间调整器和查询,或构建自己的日历系统。
日期时间包
日期时间API由主要包java.time和四个子包组成:
java.time
该API的核心部分,用于表示日期和时间。它包括用于日期、时间、日期和时间组合、时区、瞬间、持续时间和时钟的类。这些类基于ISO-8601定义的日历系统,是不可变和线程安全的。
java.time.chrono
用于表示除默认ISO-8601以外的其他日历系统的API。您还可以定义自己的日历系统。本教程不会详细介绍此包。
java.time.format
用于格式化和解析日期和时间的类。
java.time.temporal
扩展API,主要用于框架和库编写者,允许日期和时间类之间的互操作、查询和调整。字段(TemporalField和ChronoField)和单位(TemporalUnit和ChronoUnit)在此包中定义。
java.time.zone
支持时区、偏移量和时区规则的类。如果使用时区,大多数开发人员只需使用ZonedDateTime、ZoneId或ZoneOffset。
方法命名约定
日期时间 API 提供了丰富的方法和类。方法名在尽可能的情况下保持一致。例如,许多类都提供了一个 now 方法,用于捕获与该类相关的当前时刻的日期或时间值。还有一些 from 方法,允许从一种类转换为另一种类。
方法名前缀也是标准化的。由于日期时间 API 中的大多数类都是不可变的,API 不包括 set 方法。(创建后,不可变对象的值无法更改。不可变对象的等价方法是 with。)下表列出了常用的前缀:
前缀 方法类型 用途 of 静态工厂 创建一个实例,其中工厂主要验证输入参数,而不是进行转换。 from 静态工厂 将输入参数转换为目标类的实例,可能会丢失输入的信息。 parse 静态工厂 解析输入的字符串以生成目标类的实例。 format 实例 使用指定的格式化程序将时间对象中的值格式化为字符串。 get 实例 返回目标对象的一部分状态。 is 实例 查询目标对象的状态。 with 实例 返回目标对象的副本,其中一个元素已更改;这是与 JavaBean 上的 set 方法等效的不可变方法。 plus 实例 返回目标对象的副本,增加了一定的时间量。 minus 实例 返回目标对象的副本,减去了一定的时间量。 to 实例 将该对象转换为另一种类型。 at 实例 将该对象与另一个对象结合。 标准日历
日期时间API的核心是java.time包。定义在java.time中的类基于ISO日历系统,这是表示日期和时间的世界标准。ISO日历遵循修正格里高利历的规则。格里高利历于1582年引入;在修正格里高利历中,日期向前扩展,以创建一条一致、统一的时间线,并简化日期计算。
本课程涵盖以下主题:
概述
本节比较了人类时间和机器时间的概念,并提供了java.time包中主要基于时间的类的表格。
DayOfWeek和Month枚举
本节讨论了定义星期几(DayOfWeek)和月份(Month)的枚举。
日期类
本节展示了仅处理日期而不涉及时间或时区的基于时间的类。这四个类是LocalDate、YearMonth、MonthDay和Year。
日期和时间类
本节介绍了处理时间的LocalTime类和处理日期和时间的LocalDateTime类,但不涉及时区。
时区和偏移类
本节讨论存储时区(或时区偏移)信息的基于时间的类ZonedDateTime、OffsetDateTime和OffsetTime。还讨论了支持的类ZoneId、ZoneRules和ZoneOffset。
Instant类
本节讨论了表示时间线上瞬时时刻的Instant类。
解析和格式化
本节概述了如何使用预定义的格式化器来格式化和解析日期和时间值。
Temporal包
本节概述了支持时间类、字段(TemporalField和ChronoField)和单位(TemporalUnit和ChronoUnit)的java.time.temporal包。本节还解释了如何使用时间调整器获取调整后的时间值,例如“4月11日之后的第一个星期二”,以及如何执行时间查询。
Period和Duration
本节介绍了如何使用Period和Duration类以及ChronoUnit.between方法来计算时间。
时钟
本节简要介绍了Clock类。您可以使用此类提供一个替代系统时钟的时钟。
非ISO日期转换
本节解释了如何将ISO日历系统中的日期转换为非ISO日历系统(如JapaneseDate或ThaiBuddhistDate)中的日期。
传统日期时间代码
本节提供了一些关于如何将旧的java.util.Date和java.util.Calendar代码转换为日期时间API的技巧。
总结
本节提供了标准日历课程的总结。
概述
有两种基本的表示时间的方式。一种方式以人类术语表示时间,称为人类时间,如年、月、日、小时、分钟和秒。另一种方式是机器时间,它以纳秒分辨率沿着时间线连续测量时间,起点称为纪元。日期-时间包提供了一系列用于表示日期和时间的类。日期-时间API中的一些类用于表示机器时间,而另一些类更适合表示人类时间。
首先确定您需要的日期和时间方面,然后选择满足这些需求的类或多个类。在选择基于时间的类时,首先确定您需要表示人类时间还是机器时间。然后,确定您需要表示时间的哪些方面。您需要时区吗?日期和时间?仅日期?如果需要日期,您需要月、日、和年还是子集?
术语:在本教程中,捕获和处理日期或时间值的日期-时间API中的类(如Instant、LocalDateTime和ZonedDateTime)在整个教程中被称为基于时间的类(或类型)。不包括支持类型,如TemporalAdjuster接口或DayOfWeek枚举。
例如,您可以使用LocalDate对象表示出生日期,因为大多数人在同一天庆祝生日,无论他们是在出生城市还是跨越国际日期线的地球的另一边。如果您正在跟踪天文时间,那么您可能希望使用LocalDateTime对象表示出生日期和时间,或者使用ZonedDateTime,它还包括时区。如果您正在创建一个时间戳,那么您很可能希望使用Instant,它允许您将时间线上的一个瞬时点与另一个瞬时点进行比较。
下表总结了java.time包中的基于时间的类,这些类存储日期和/或时间信息,或用于测量时间量。在列中打勾表示该类使用该特定类型的数据,toString输出列显示使用toString方法打印的实例。讨论位置列链接到教程中相关的页面。
DayOfWeek和Month枚举类
日期时间 API 提供了枚举来指定星期几和每年的月份。
DayOfWeek
DayOfWeek 枚举由七个常量组成,用于描述星期几: MONDAY 到 SUNDAY。 DayOfWeek 常量的整数值范围从 1(星期一)到 7(星期日)。使用定义的常量(DayOfWeek.FRIDAY)可以使您的代码更具可读性。
该枚举还提供了许多方法,类似于基于时间的类提供的方法。例如,以下代码将 3 天添加到 "Monday" 并打印结果。输出为 "THURSDAY":
System.out.printf("%s%n", DayOfWeek.MONDAY.plus(3));
通过使用 getDisplayName(TextStyle, Locale) 方法,您可以在用户的区域设置中检索用于标识星期几的字符串。 TextStyle 枚举使您可以指定要显示的字符串类型: FULL、NARROW(通常是一个字母)或 SHORT(缩写)。STANDALONE TextStyle 常量用于某些语言,当作为日期的一部分使用时,输出与单独使用时不同。以下示例打印了 "Monday" 的三种主要 TextStyle 形式:
DayOfWeek dow = DayOfWeek.MONDAY; Locale locale = Locale.getDefault(); System.out.println(dow.getDisplayName(TextStyle.FULL, locale)); System.out.println(dow.getDisplayName(TextStyle.NARROW, locale)); System.out.println(dow.getDisplayName(TextStyle.SHORT, locale));
对于 en 区域设置,此代码的输出如下:
Monday M Mon
Month
Month 枚举包含了十二个月的常量,从 JANUARY 到 DECEMBER。与 DayOfWeek 枚举一样,Month 枚举是强类型的,每个常量的整数值对应于 ISO 范围从 1(一月)到 12(十二月)。使用定义的常量(Month.SEPTEMBER)可以使您的代码更具可读性。
Month 枚举还包括许多方法。以下代码使用 maxLength 方法打印二月份可能的最大天数。输出为 "29":
System.out.printf("%d%n", Month.FEBRUARY.maxLength());
Month 枚举类还实现了 getDisplayName(TextStyle, Locale) 方法,通过指定的 TextStyle 以用户的区域设置来获取标识月份的字符串。如果某个特定的 TextStyle 未定义,则返回表示常量数值的字符串。以下代码使用三种主要的文本样式打印出了八月份的月份:
Month month = Month.AUGUST; Locale locale = Locale.getDefault(); System.out.println(month.getDisplayName(TextStyle.FULL, locale)); System.out.println(month.getDisplayName(TextStyle.NARROW, locale)); System.out.println(month.getDisplayName(TextStyle.SHORT, locale));
对于 en 区域设置,该代码的输出如下:
August A Aug
日期类
日期时间 API 提供了四个专门处理日期信息的类,不考虑时间或时区。这些类的使用建议通过类名来判断:LocalDate、YearMonth、MonthDay 和 Year。
LocalDate
LocalDate 表示 ISO 日历中的年月日,用于表示不带时间的日期。你可以使用 LocalDate 来追踪重要事件,如出生日期或结婚日期。以下示例使用 of 和 with 方法创建 LocalDate 实例:
LocalDate date = LocalDate.of(2000, Month.NOVEMBER, 20); LocalDate nextWed = date.with(TemporalAdjusters.next(DayOfWeek.WEDNESDAY));
有关 TemporalAdjuster 接口的更多信息,请参阅 Temporal Adjuster。
除了通常的方法外,LocalDate 类还提供了一些用于获取给定日期信息的 getter 方法。例如,以下代码返回 "MONDAY":
DayOfWeek dotw = LocalDate.of(2012, Month.JULY, 9).getDayOfWeek();
下面的示例使用 TemporalAdjuster 来获取特定日期之后的第一个星期三。
LocalDate date = LocalDate.of(2000, Month.NOVEMBER, 20); TemporalAdjuster adj = TemporalAdjusters.next(DayOfWeek.WEDNESDAY); LocalDate nextWed = date.with(adj); System.out.printf("对于日期 %s,下一个星期三是 %s.%n", date, nextWed);
运行该代码将输出:
对于日期 2000-11-20,下一个星期三是 2000-11-22.
Period 和 Duration 部分还使用了 LocalDate 类的示例。
YearMonth
YearMonth 类表示特定年份的月份。以下示例使用 YearMonth.lengthOfMonth() 方法来确定几个年份和月份组合的天数。
YearMonth date = YearMonth.now(); System.out.printf("%s: %d%n", date, date.lengthOfMonth()); YearMonth date2 = YearMonth.of(2010, Month.FEBRUARY); System.out.printf("%s: %d%n", date2, date2.lengthOfMonth()); YearMonth date3 = YearMonth.of(2012, Month.FEBRUARY); System.out.printf("%s: %d%n", date3, date3.lengthOfMonth());
代码的输出如下所示:
2013-06: 30 2010-02: 28 2012-02: 29
MonthDay
MonthDay类表示特定月份的某一天,例如1月1日的元旦。
以下示例使用MonthDay.isValidYear方法确定2010年是否可以使用2月29日。调用返回false,确认2010年不是闰年。
MonthDay date = MonthDay.of(Month.FEBRUARY, 29); boolean validLeapYear = date.isValidYear(2010);
Year
Year类表示一年。以下示例使用Year.isLeap方法确定给定年份是否为闰年。调用返回true,确认2012年是闰年。
boolean validLeapYear = Year.of(2012).isLeap();
日期和时间类
LocalTime
LocalTime类与其他以Local为前缀的类类似,但仅处理时间。这个类非常适用于表示基于人类的一天中的时间,例如电影时间,或当地图书馆的开放和关闭时间。它还可以用来创建一个数字时钟,如下面的示例所示:
LocalTime thisSec; for (;;) { thisSec = LocalTime.now(); // display代码的实现留给读者 display(thisSec.getHour(), thisSec.getMinute(), thisSec.getSecond()); }
LocalTime类不存储时区或夏令时信息。
LocalDateTime
处理日期和时间(不含时区)的类是LocalDateTime,它是日期时间API的核心类之一。此类用于表示日期(月-日-年)与时间(小时-分钟-秒-纳秒)的组合,实际上是LocalDate与LocalTime的组合。该类可用于表示特定事件,例如2013年8月17日下午1:10开始的美洲杯挑战者系列赛的第一场比赛。注意,这表示当地时间下午1:10。要包含时区,必须使用ZonedDateTime或OffsetDateTime,如时区和偏移类中所讨论的。
除了每个基于时间的类都提供的now方法外,LocalDateTime类还有各种of方法(或以of为前缀的方法),用于创建LocalDateTime实例。还有一个from方法,将另一个时间格式的实例转换为LocalDateTime实例。还有用于添加或减去小时、分钟、天、周和月份的方法。以下示例显示了其中的几个方法。日期时间表达式以粗体显示:
System.out.printf("当前时间: %s%n", LocalDateTime.now()); System.out.printf("1994年4月15日上午11:30: %s%n", LocalDateTime.of(1994, Month.APRIL, 15, 11, 30)); System.out.printf("当前时间(来自Instant): %s%n", LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault())); System.out.printf("6个月后的时间: %s%n", LocalDateTime.now().plusMonths(6)); System.out.printf("6个月前的时间: %s%n", LocalDateTime.now().minusMonths(6));
此代码将产生类似以下的输出:
当前时间: 2013-07-24T17:13:59.985 1994年4月15日上午11:30: 1994-04-15T11:30 当前时间(来自Instant): 2013-07-24T17:14:00.479 6个月后的时间: 2014-01-24T17:14:00.480 6个月前的时间: 2013-01-24T17:14:00.481
时区和偏移类
一个时区是地球上使用相同标准时间的区域。每个时区由一个标识符描述,通常采用区域/城市的格式(Asia/Tokyo),以及与格林威治/世界标准时间的偏移量。例如,东京的偏移量为+09:00。
ZoneId和ZoneOffset
日期时间API提供了两个类来指定时区或偏移量:
- ZoneId指定一个时区标识符,并提供在Instant和LocalDateTime之间进行转换的规则。
- ZoneOffset指定相对于格林威治/世界标准时间的时区偏移量。
-
相对于格林威治/世界标准时间的偏移量通常以整小时为单位定义,但也有例外情况。以下代码来自TimeZoneId示例,打印出使用不以整小时为定义的格林威治/世界标准时间偏移量的所有时区的列表。
Set<String> allZones = ZoneId.getAvailableZoneIds(); LocalDateTime dt = LocalDateTime.now(); // 使用区域集合创建一个列表并进行排序。 List<String> zoneList = new ArrayList<String>(allZones); Collections.sort(zoneList); ... for (String s : zoneList) { ZoneId zone = ZoneId.of(s); ZonedDateTime zdt = dt.atZone(zone); ZoneOffset offset = zdt.getOffset(); int secondsOfHour = offset.getTotalSeconds() % (60 * 60); String out = String.format("%35s %10s%n", zone, offset); // 仅打印没有整小时偏移量的时区到标准输出。 if (secondsOfHour != 0) { System.out.printf(out); } ... }
这个示例会将以下列表打印到标准输出:
America/Caracas -04:30 America/St_Johns -02:30 Asia/Calcutta +05:30 Asia/Colombo +05:30 Asia/Kabul +04:30 Asia/Kathmandu +05:45 Asia/Katmandu +05:45 Asia/Kolkata +05:30 Asia/Rangoon +06:30 Asia/Tehran +04:30 Australia/Adelaide +09:30 Australia/Broken_Hill +09:30 Australia/Darwin +09:30 Australia/Eucla +08:45 Australia/LHI +10:30 Australia/Lord_Howe +10:30 Australia/North +09:30 Australia/South +09:30 Australia/Yancowinna +09:30 Canada/Newfoundland -02:30 Indian/Cocos +06:30 Iran +04:30 NZ-CHAT +12:45 Pacific/Chatham +12:45 Pacific/Marquesas -09:30 Pacific/Norfolk +11:30
TimeZoneId示例还会将所有时区ID的列表打印到名为timeZones的文件中。
日期时间类
日期时间 API 提供了三个基于时间区域的类:
- ZonedDateTime 使用与格林威治/UTC 的时区偏移来处理日期和时间。
- OffsetDateTime 使用与格林威治/UTC 的时区偏移来处理日期和时间,没有时区 ID。
- OffsetTime 使用与格林威治/UTC 的时区偏移来处理时间,没有时区 ID。
-
OffsetTime
OffsetTime类实际上将LocalTime类与ZoneOffset类组合在一起。它用于表示带有与格林威治/协调世界时(+/-小时:分钟,例如+06:00或-08:00)的偏移的时间(小时,分钟,秒,纳秒)。
OffsetTime类在与OffsetDateTime类相同的情况下使用,但不需要跟踪日期。
什么情况下应该使用 OffsetDateTime 而不是 ZonedDateTime?如果您正在编写基于地理位置的复杂软件,以模拟自己的日期和时间计算规则,或者如果您正在存储数据库中仅跟踪相对于格林威治/UTC 时间的绝对偏移的时间戳,那么您可能想要使用 OffsetDateTime。此外,XML 和其他网络格式将日期时间传输定义为 OffsetDateTime 或 OffsetTime。
尽管所有三个类都维护了与格林威治/UTC 时间的偏移量,但只有 ZonedDateTime 使用 ZoneRules(位于 java.time.zone 包中)来确定特定时区的偏移量如何变化。例如,大多数时区在将时钟向前调整为夏令时时会经历一个间隔(通常为1小时),而在将时钟调回为标准时间时会经历时间重叠,并且在过渡前的最后一个小时会重复。 ZonedDateTime 类适应了这种情况,而没有访问 ZoneRules 的 OffsetDateTime 和 OffsetTime 类则没有。
ZonedDateTime
ZonedDateTime 类实际上将 LocalDateTime 类与 ZoneId 类结合起来。它用于表示完整的日期(年、月、日)和时间(时、分、秒、纳秒)与时区(地区/城市,例如 Europe/Paris)。
以下代码来自 Flight 示例,定义了从旧金山飞往东京的航班的出发时间,使用美国洛杉矶时区的 ZonedDateTime。使用 withZoneSameInstant 和 plusMinutes 方法创建了一个表示预计到达时间的 ZonedDateTime 实例,航班飞行时间为 650 分钟。 ZoneRules.isDaylightSavings 方法确定飞机到达东京时是否为夏令时。
DateTimeFormatter对象用于格式化ZonedDateTime实例以进行打印:
DateTimeFormatter format = DateTimeFormatter.ofPattern("MMM d yyyy hh:mm a"); // 2013年7月20日晚上7:30从旧金山出发 LocalDateTime leaving = LocalDateTime.of(2013, Month.JULY, 20, 19, 30); ZoneId leavingZone = ZoneId.of("America/Los_Angeles"); ZonedDateTime departure = ZonedDateTime.of(leaving, leavingZone); try { String out1 = departure.format(format); System.out.printf("出发时间: %s (%s)%n", out1, leavingZone); } catch (DateTimeException exc) { System.out.printf("%s 无法格式化!%n", departure); throw exc; } // 飞行时间为10小时50分钟,即650分钟 ZoneId arrivingZone = ZoneId.of("Asia/Tokyo"); ZonedDateTime arrival = departure.withZoneSameInstant(arrivingZone) .plusMinutes(650); try { String out2 = arrival.format(format); System.out.printf("到达时间: %s (%s)%n", out2, arrivingZone); } catch (DateTimeException exc) { System.out.printf("%s 无法格式化!%n", arrival); throw exc; } if (arrivingZone.getRules().isDaylightSavings(arrival.toInstant())) System.out.printf(" (亚洲/东京将使用夏令时。)%n", arrivingZone); else System.out.printf(" (亚洲/东京将使用标准时间。)%n", arrivingZone);
这将产生以下输出:
出发时间: Jul 20 2013 07:30 PM (America/Los_Angeles) 到达时间: Jul 21 2013 10:20 PM (Asia/Tokyo) (亚洲/东京将使用标准时间。)
OffsetDateTime
OffsetDateTime类实际上结合了LocalDateTime类和ZoneOffset类。它用于表示具有与格林威治/协调世界时(+/-小时:分钟,例如+06:00或-08:00)的偏移量的完整日期(年、月、日)和时间(时、分、秒、纳秒)。
以下示例使用OffsetDateTime和TemporalAdjuster.lastDay方法找到2013年7月的最后一个星期四。
// 找到2013年7月的最后一个星期四 LocalDateTime localDate = LocalDateTime.of(2013, Month.JULY, 20, 19, 30); ZoneOffset offset = ZoneOffset.of("-08:00"); OffsetDateTime offsetDate = OffsetDateTime.of(localDate, offset); OffsetDateTime lastThursday = offsetDate.with(TemporalAdjusters.lastInMonth(DayOfWeek.THURSDAY)); System.out.printf("2013年7月的最后一个星期四是第%s天。%n", lastThursday.getDayOfMonth());
运行此代码的输出结果为:
2013年7月的最后一个星期四是25日。
Instant类
Date-Time API的核心类之一是Instant类,它表示时间线上的纳秒起始点。这个类用于生成表示机器时间的时间戳。
import java.time.Instant; Instant timestamp = Instant.now();
从Instant类返回的值从1970年1月1日的第一秒开始计算(1970-01-01T00:00:00Z),也称为EPOCH。在纪元之前发生的时刻具有负值,而在纪元之后发生的时刻具有正值。
Instant类提供的其他常量是MIN,表示最小可能(过去的)时刻,以及MAX,表示最大(远未来的)时刻。
调用Instant的toString方法会产生以下输出:
2013-05-30T23:38:23.085Z
这个格式遵循ISO-8601标准表示日期和时间。
Instant类提供了各种方法来操作Instant。有用于添加或减去时间的plus和minus方法。以下代码将1小时添加到当前时间:
Instant oneHourLater = Instant.now().plus(1, ChronoUnit.HOURS);
还有用于比较时刻的方法,如isAfter和isBefore。until方法返回两个Instant对象之间存在多少时间。以下代码报告了自Java纪元开始以来经过了多少秒。
long secondsFromEpoch = Instant.ofEpochSecond(0L).until(Instant.now(), ChronoUnit.SECONDS);
Instant类不适用于年、月或日等人类时间单位。如果要在这些单位上执行计算,可以将Instant转换为另一个类,例如LocalDateTime或ZonedDateTime,通过将Instant与时区绑定。然后可以以所需的单位访问值。以下代码使用ofInstant方法和默认时区将Instant转换为LocalDateTime对象,然后以更可读的形式打印出日期和时间:
Instant timestamp; ... LocalDateTime ldt = LocalDateTime.ofInstant(timestamp, ZoneId.systemDefault()); System.out.printf("%s %d %d 在 %d:%d%n", ldt.getMonth(), ldt.getDayOfMonth(), ldt.getYear(), ldt.getHour(), ldt.getMinute());
输出结果类似于以下内容:
五月 30 2013 在 18:21
可以将ZonedDateTime或OffsetTimeZone对象转换为Instant对象,因为每个对象都映射到时间线上的确切时刻。然而,反过来则不成立。要将Instant对象转换为ZonedDateTime或OffsetDateTime对象,需要提供时区或时区偏移信息。
解析和格式化
日期时间API中的基于时间的类提供了用于解析包含日期和时间信息的字符串的parse方法。这些类还提供了用于将基于时间的对象格式化为显示的format方法。在两种情况下,过程相似:您提供一个模式给DateTimeFormatter来创建一个格式化对象。然后将该格式化对象传递给parse或format方法。
DateTimeFormatter类提供了许多预定义的格式化程序,或者您可以定义自己的格式化程序。
如果在转换过程中出现问题,parse和format方法会抛出异常。因此,您的解析代码应该捕获DateTimeParseException错误,您的格式化代码应该捕获DateTimeException错误。有关异常处理的更多信息,请参阅捕获和处理异常。
DateTimeFormatter类既是不可变的又是线程安全的;在适当的地方,它可以(并且应该)分配给一个静态常量。
版本说明: 可以直接使用java.time日期时间对象与java.util.Formatter和String.format配合使用,使用与旧的java.util.Date和java.util.Calendar类相同的基于模式的格式化方式。
解析
LocalDate类中的单参数parse(CharSequence)方法使用ISO_LOCAL_DATE格式化程序。要指定不同的格式化程序,可以使用两个参数的parse(CharSequence, DateTimeFormatter)方法。下面的示例使用预定义的BASIC_ISO_DATE格式化程序,该程序使用19590709格式表示1959年7月9日。
String in = ...; LocalDate date = LocalDate.parse(in, DateTimeFormatter.BASIC_ISO_DATE);
您还可以使用自己的模式定义格式化程序。下面的代码来自Parse示例,它创建了一个应用格式为"MMM d yyyy"的格式化程序。此格式指定三个字符表示月份,一个数字表示月份的日期,四个数字表示年份。使用此模式创建的格式化程序将识别诸如"Jan 3 2003"或"Mar 23 1994"的字符串。然而,要将格式指定为"MMM dd yyyy",即日期的日期为两个字符,则您必须始终使用两个字符,用零填充一个数字日期:"Jun 03 2003"。
String input = ...; try { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM d yyyy"); LocalDate date = LocalDate.parse(input, formatter); System.out.printf("%s%n", date); } catch (DateTimeParseException exc) { System.out.printf("%s 无法解析!%n", input); throw exc; // 重新抛出异常。 } // 'date' 已经成功解析
DateTimeFormatter类的文档指定了您可以使用的完整符号列表,用于指定格式化或解析的模式。
非ISO日期转换页面上的StringConverter示例提供了另一个日期格式化的示例。
格式化
format(DateTimeFormatter)方法使用指定的格式将基于时间的对象转换为字符串表示形式。下面的代码来自Flight示例,使用格式“MMM d yyy hh:mm a”将ZonedDateTime的实例转换为字符串。日期的定义方式与之前的解析示例相同,但此模式还包括小时、分钟以及上午和下午的组件。
ZoneId leavingZone = ...; ZonedDateTime departure = ...; try { DateTimeFormatter format = DateTimeFormatter.ofPattern("MMM d yyyy hh:mm a"); String out = departure.format(format); System.out.printf("LEAVING: %s (%s)%n", out, leavingZone); } catch (DateTimeException exc) { System.out.printf("%s 无法格式化!%n", departure); throw exc; }
该示例的输出,打印了到达时间和出发时间,如下所示:
LEAVING: Jul 20 2013 07:30 PM (America/Los_Angeles) ARRIVING: Jul 21 2013 10:20 PM (Asia/Tokyo)
时间包
java.time.temporal
这些接口旨在在最低级别使用。典型的应用代码应该使用具体类型,比如LocalDate或ZonedDateTime,而不是使用Temporal接口。这与声明String类型的变量相同,而不是声明CharSequence类型的变量。
Temporal和TemporalAccessor
Temporal接口提供了访问基于时间的对象的框架,被实现在基于时间的类中,比如Instant、LocalDateTime和ZonedDateTime。这个接口提供了添加或减去时间单位的方法,使得基于时间的算术在各种日期和时间类中易于使用和一致。TemporalAccessor接口提供了Temporal接口的只读版本。
无论Temporal还是TemporalAccessor对象都是基于字段来定义的,正如在TemporalField接口中指定的那样。ChronoField枚举是TemporalField接口的一个具体实现,提供了一组丰富的定义常量,比如DAY_OF_WEEK、MINUTE_OF_HOUR和MONTH_OF_YEAR。
这些字段的单位由TemporalUnit接口指定。ChronoUnit枚举实现了TemporalUnit接口。字段ChronoField.DAY_OF_WEEK是ChronoUnit.DAYS和ChronoUnit.WEEKS的组合。ChronoField和ChronoUnit枚举在下面的章节中进行了讨论。
Temporal接口中的基于算术的方法需要以TemporalAmount值为参数。Period和Duration类(在Period和Duration中讨论)实现了TemporalAmount接口。
ChronoField 和 IsoFields
ChronoField 枚举实现了 TemporalField 接口,提供了一组用于访问日期和时间值的常量。一些例子包括 CLOCK_HOUR_OF_DAY、NANO_OF_DAY 和 DAY_OF_YEAR。这个枚举可以用于表示时间的概念方面,比如一年中的第三周、一天中的第11小时或一个月中的第一个星期一。当遇到未知类型的 Temporal 时,可以使用 TemporalAccessor.isSupported(TemporalField) 方法来确定 Temporal 是否支持特定的字段。下面这行代码返回 false,表示 LocalDate 不支持 ChronoField.CLOCK_HOUR_OF_DAY:
boolean isSupported = LocalDate.now().isSupported(ChronoField.CLOCK_HOUR_OF_DAY);
特定于 ISO-8601 日历系统的其他字段在 IsoFields 类中定义。以下示例展示了如何使用 ChronoField 和 IsoFields 获取字段的值:
time.get(ChronoField.MILLI_OF_SECOND) int qoy = date.get(IsoFields.QUARTER_OF_YEAR);
另外还有两个类定义了其他可能有用的字段,它们是 WeekFields 和 JulianFields。
ChronoUnit
ChronoUnit 枚举实现了 TemporalUnit 接口,并提供了一组基于日期和时间的标准单位,从毫秒到千年。注意,并非所有的 ChronoUnit 对象都被所有类支持。例如,Instant 类不支持 ChronoUnit.MONTHS 或 ChronoUnit.YEARS。Date-Time API 中的类包含 isSupported(TemporalUnit) 方法,可以用来验证一个类是否支持特定的时间单位。下面这个对 isSupported 的调用返回 false,确认 Instant 类不支持 ChronoUnit.DAYS。
Instant instant = Instant.now(); boolean isSupported = instant.isSupported(ChronoUnit.DAYS);
时间调整器
TemporalAdjuster接口位于java.time.temporal包中,提供了一些方法,接受一个Temporal值并返回一个调整后的值。这些调整器可以与任何基于时间的类型一起使用。
如果将调整器与ZonedDateTime一起使用,则会计算出一个新日期,该日期保留原始的时间和时区值。
预定义的调整器
TemporalAdjusters类(注意复数形式)提供了一组预定义的调整器,用于找到一个月的第一天或最后一天,一年的第一天或最后一天,一个月的最后一个星期三,或特定日期之后的第一个星期二等等。这些预定义的调整器定义为静态方法,并设计用于与静态导入语句一起使用。
以下示例结合使用了几个TemporalAdjusters方法和基于时间的类中定义的with方法,根据原始日期“2000年10月15日”计算出新日期:
LocalDate date = LocalDate.of(2000, Month.OCTOBER, 15); DayOfWeek dotw = date.getDayOfWeek(); System.out.printf("%s是%s%n", date, dotw); System.out.printf("本月的第一天: %s%n", date.with(TemporalAdjusters.firstDayOfMonth())); System.out.printf("本月的第一个星期一: %s%n", date.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY))); System.out.printf("本月的最后一天: %s%n", date.with(TemporalAdjusters.lastDayOfMonth())); System.out.printf("下个月的第一天: %s%n", date.with(TemporalAdjusters.firstDayOfNextMonth())); System.out.printf("明年的第一天: %s%n", date.with(TemporalAdjusters.firstDayOfNextYear())); System.out.printf("本年的第一天: %s%n", date.with(TemporalAdjusters.firstDayOfYear()));
这将产生以下输出:
2000-10-15是星期日 本月的第一天: 2000-10-01 本月的第一个星期一: 2000-10-02 本月的最后一天: 2000-10-31 下个月的第一天: 2000-11-01 明年的第一天: 2001-01-01 本年的第一天: 2000-01-01
自定义调整器
您也可以创建自己的自定义调整器。为此,您可以创建一个实现TemporalAdjuster接口并具有adjustInto(Temporal)方法的类。来自PaydayAdjuster示例的PaydayAdjuster类是一个自定义调整器。PaydayAdjuster根据传入的日期计算并返回下一个发薪日,假设发薪日每月两次:15号和当月的最后一天。如果计算出的日期是在周末,则使用前一个星期五。假定当前的日历年。
/** * adjustInto方法接受一个Temporal实例并返回一个调整后的LocalDate。 * 如果传入的参数不是LocalDate,则抛出DateTimeException异常。 */ public Temporal adjustInto(Temporal input) { LocalDate date = LocalDate.from(input); int day; if (date.getDayOfMonth() < 15) { day = 15; } else { day = date.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth(); } date = date.withDayOfMonth(day); if (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY) { date = date.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY)); } return input.with(date); }
可以像预定义的adjuster一样使用adjuster,使用with方法调用。以下代码来自NextPayday示例:
LocalDate nextPayday = date.with(new PaydayAdjuster());
在2013年,6月15日和6月30日都是周末。在2013年的6月3日和6月18日分别运行NextPayday示例,得到以下结果:
给定日期:2013年6月3日 下一个发薪日:2013年6月14日 给定日期:2013年6月18日 下一个发薪日:2013年6月28日
时间查询
一个TemporalQuery可以用来从基于时间的对象中检索信息。
预定义查询
TemporalQueries类(注意复数形式)提供了几个预定义的查询,包括在应用程序无法确定基于时间的对象的类型时有用的方法。与调整器一样,预定义查询被定义为静态方法,并且设计用于与static import语句一起使用。
例如,precision查询返回特定基于时间的对象可以返回的最小ChronoUnit。以下示例在几种类型的基于时间的对象上使用precision查询:
TemporalQuery<TemporalUnit> query = TemporalQueries.precision(); System.out.printf("LocalDate的精度为%s%n", LocalDate.now().query(query)); System.out.printf("LocalDateTime的精度为%s%n", LocalDateTime.now().query(query)); System.out.printf("Year的精度为%s%n", Year.now().query(query)); System.out.printf("YearMonth的精度为%s%n", YearMonth.now().query(query)); System.out.printf("Instant的精度为%s%n", Instant.now().query(query));
输出结果如下:
LocalDate的精度为Days LocalDateTime的精度为Nanos Year的精度为Years YearMonth的精度为Months Instant的精度为Nanos
自定义查询
您还可以创建自己的自定义查询。一种方法是创建一个实现TemporalQuery接口并具有queryFrom(TemporalAccessor)方法的类。示例CheckDate实现了两个自定义查询。第一个自定义查询可以在FamilyVacations类中找到,该类实现了TemporalQuery接口。queryFrom方法将传入的日期与计划的假期日期进行比较,如果日期在这些日期范围内,则返回TRUE。
// 如果传入的日期是家庭度假的日期之一,则返回true。因为查询只比较月份和日期,即使Temporal类型不同,检查也会成功。 public Boolean queryFrom(TemporalAccessor date) { int month = date.get(ChronoField.MONTH_OF_YEAR); int day = date.get(ChronoField.DAY_OF_MONTH); // 春假在迪士尼乐园 if ((month == Month.APRIL.getValue()) && ((day >= 3) && (day <= 8))) return Boolean.TRUE; // 史密斯家族重聚在萨各托克湖 if ((month == Month.AUGUST.getValue()) && ((day >= 8) && (day <= 14))) return Boolean.TRUE; return Boolean.FALSE; }
第二个自定义查询实现在FamilyBirthdays类中。该类提供了一个isFamilyBirthday方法,用于将传入的日期与几个生日进行比较,并返回TRUE如果有匹配。
// 如果传入的日期与家庭生日之一相同,则返回true。因为查询只比较月份和日期,即使Temporal类型不同,检查也会成功。 public static Boolean isFamilyBirthday(TemporalAccessor date) { int month = date.get(ChronoField.MONTH_OF_YEAR); int day = date.get(ChronoField.DAY_OF_MONTH); // 安吉的生日是4月3日。 if ((month == Month.APRIL.getValue()) && (day == 3)) return Boolean.TRUE; // 苏的生日是6月18日。 if ((month == Month.JUNE.getValue()) && (day == 18)) return Boolean.TRUE; // 乔的生日是5月29日。 if ((month == Month.MAY.getValue()) && (day == 29)) return Boolean.TRUE; return Boolean.FALSE; }
FamilyBirthday类没有实现TemporalQuery接口,并且可以作为lambda表达式的一部分使用。以下代码来自CheckDate示例,展示了如何调用这两个自定义查询。
// 不使用lambda表达式调用查询。 Boolean isFamilyVacation = date.query(new FamilyVacations()); // 使用lambda表达式调用查询。 Boolean isFamilyBirthday = date.query(FamilyBirthdays::isFamilyBirthday); if (isFamilyVacation.booleanValue() || isFamilyBirthday.booleanValue()) System.out.printf("%s是一个重要的日期!%n", date); else System.out.printf("%s不是一个重要的日期。%n", date);
Period 和 Duration
当您编写代码来指定一段时间时,请使用最适合您需求的类或方法:Duration类、Period类或ChronoUnit.between方法。Duration类使用基于时间的值(秒、纳秒)来测量时间量。Period类使用基于日期的值(年、月、日)。
注意: 一天的Duration确切为24小时。一个Period加到一个ZonedDateTime上,根据时区可能会有所不同,例如发生在夏令时的第一天或最后一天。
Duration
在测量基于机器的时间(例如使用Instant对象的代码)的情况下,Duration是最合适的。Duration对象以秒或纳秒为单位进行测量,不使用基于日期的构造(如年、月和日),尽管该类提供了将其转换为天、小时和分钟的方法。如果创建一个终点在开始点之前的Duration,它可以有一个负值。
以下代码计算两个时刻之间的持续时间(以纳秒为单位):
Instant t1, t2; ... long ns = Duration.between(t1, t2).toNanos();
以下代码将10秒添加到一个Instant中:
Instant start; ... Duration gap = Duration.ofSeconds(10); Instant later = start.plus(gap);
Duration与时间线无关,即它不会跟踪时区或夏令时。将等于1天的Duration添加到ZonedDateTime中将确切添加24小时,而不考虑夏令时或其他可能导致的时间差异。
ChronoUnit
ChronoUnit枚举在Temporal包中进行了讨论,它定义了用于测量时间的单位。当您想要以单个时间单位(如天或秒)来测量一段时间时,ChronoUnit.between方法非常有用。between方法适用于所有基于时间的对象,但它只返回一个单位的数量。以下代码计算两个时间戳之间的间隔(以毫秒为单位):
import java.time.Instant; import java.time.temporal.Temporal; import java.time.temporal.ChronoUnit; Instant previous, current, gap; ... current = Instant.now(); if (previous != null) { gap = ChronoUnit.MILLIS.between(previous,current); } ...
时间段
要使用日期为基础的值(年、月、日)来定义一段时间,可以使用Period类。 Period类提供了各种get方法,例如getMonths、getDays和getYears,以便从时间段中提取时间量。
总的时间段由三个单位组成:月、日和年。要以单个时间单位(如天)表示测量的时间量,可以使用ChronoUnit.between方法。
下面的代码报告了你的年龄,假设你的出生日期是1960年1月1日。使用Period类来确定年、月和日的时间。使用ChronoUnit.between方法确定的总天数显示在括号中:
LocalDate today = LocalDate.now(); LocalDate birthday = LocalDate.of(1960, Month.JANUARY, 1); Period p = Period.between(birthday, today); long p2 = ChronoUnit.DAYS.between(birthday, today); System.out.println("你今年已经" + p.getYears() + "岁," + p.getMonths() + "个月,并且" + p.getDays() + "天了。 (总共" + p2 + "天)");
该代码生成类似以下的输出:
你今年已经53岁,4个月,并且29天了。 (总共19508天)
要计算距离你下一个生日还有多久,可以使用来自Birthday示例的以下代码。使用Period类确定月份和天数的值。 ChronoUnit.between方法返回总天数的值,并在括号中显示。
LocalDate birthday = LocalDate.of(1960, Month.JANUARY, 1); LocalDate nextBDay = birthday.withYear(today.getYear()); //如果今年已经过了你的生日,将年份加1。 if (nextBDay.isBefore(today) || nextBDay.isEqual(today)) { nextBDay = nextBDay.plusYears(1); } Period p = Period.between(today, nextBDay); long p2 = ChronoUnit.DAYS.between(today, nextBDay); System.out.println("距离你的下一个生日还有" + p.getMonths() + "个月," + p.getDays() + "天。 (总共" + p2 + "天)");
代码产生的输出类似于下面的内容:
距离你的下一个生日还有7个月,2天。(总共216天)
这些计算没有考虑时区的差异。例如,如果你出生在澳大利亚,但目前居住在班加罗尔,这会对你的确切年龄计算有轻微影响。在这种情况下,可以使用Period结合ZonedDateTime类。当将Period添加到ZonedDateTime时,会考虑时间差异。
时钟
大多数基于时间的对象都提供一个无参数的now()方法,使用系统时钟和默认时区提供当前日期和时间。这些基于时间的对象还提供一个一参数的now(Clock)方法,允许您传入一个替代的Clock。
当前日期和时间取决于时区,对于全球化应用程序,必须使用Clock确保日期/时间使用正确的时区创建。因此,尽管使用Clock类是可选的,但此功能允许您为其他时区测试代码,或者使用固定的时钟,其中时间不会改变。
Clock类是抽象的,因此不能创建其实例。以下工厂方法对于测试很有用。
- Clock.offset(Clock, Duration) 返回一个偏移了指定Duration的时钟。
- Clock.systemUTC() 返回代表格林威治/协调世界时的时钟。
- Clock.fixed(Instant, ZoneId) 始终返回相同的Instant。对于该时钟,时间停止。
非ISO日期转换
本教程不详细讨论java.time.chrono包。然而,知道这个包提供了几个预定义的非ISO基准的年代历法可能会有用,例如日本、伊斯兰、民国和泰国佛教历法。您也可以使用这个包来创建自己的年代历法。
本节将向您展示如何在ISO基准日期和其他预定义年代历法之间进行转换。
转换为非ISO基准日期
您可以使用from(TemporalAccessor)方法将ISO基准日期转换为其他年代历法的日期,例如JapaneseDate.from(TemporalAccessor)。如果无法将日期转换为有效的实例,则此方法会抛出DateTimeException。以下代码将LocalDateTime实例转换为几个预定义的非ISO日历日期:
LocalDateTime date = LocalDateTime.of(2013, Month.JULY, 20, 19, 30); JapaneseDate jdate = JapaneseDate.from(date); HijrahDate hdate = HijrahDate.from(date); MinguoDate mdate = MinguoDate.from(date); ThaiBuddhistDate tdate = ThaiBuddhistDate.from(date);
示例StringConverter将LocalDate转换为ChronoLocalDate,再转换为String,然后再转换回来。toString方法接受LocalDate实例和Chronology,并使用提供的Chronology返回转换后的字符串。使用DateTimeFormatterBuilder构建可用于打印日期的字符串:
/** * 使用提供的年代历法将ISO的LocalDate值转换为ChronoLocalDate日期, * 然后使用基于年代历法和当前区域设置的SHORT模式的DateTimeFormatter * 格式化ChronoLocalDate为String。 * * @param localDate - 要转换和格式化的ISO日期。 * @param chrono - 可选的年代历法。如果为null,则使用IsoChronology。 */ public static String toString(LocalDate localDate, Chronology chrono) { if (localDate != null) { Locale locale = Locale.getDefault(Locale.Category.FORMAT); ChronoLocalDate cDate; if (chrono == null) { chrono = IsoChronology.INSTANCE; } try { cDate = chrono.date(localDate); } catch (DateTimeException ex) { System.err.println(ex); chrono = IsoChronology.INSTANCE; cDate = localDate; } DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) .withLocale(locale) .withChronology(chrono) .withDecimalStyle(DecimalStyle.of(locale)); String pattern = "M/d/yyyy GGGGG"; return dateFormatter.format(cDate); } else { return ""; } }
当使用以下日期调用方法时,预定义的年表如下:
LocalDate date = LocalDate.of(1996, Month.OCTOBER, 29); System.out.printf("%s%n", StringConverter.toString(date, JapaneseChronology.INSTANCE)); System.out.printf("%s%n", StringConverter.toString(date, MinguoChronology.INSTANCE)); System.out.printf("%s%n", StringConverter.toString(date, ThaiBuddhistChronology.INSTANCE)); System.out.printf("%s%n", StringConverter.toString(date, HijrahChronology.INSTANCE));
输出结果如下:
10/29/0008 H 10/29/0085 1 10/29/2539 B.E. 6/16/1417 1
转换为基于ISO的日期
您可以使用静态的LocalDate.from方法,从非ISO日期转换为LocalDate实例,如下例所示:
LocalDate date = LocalDate.from(JapaneseDate.now());
其他基于时间的类也提供了这个方法,如果无法转换日期,则会抛出DateTimeException异常。
fromString方法,来自StringConverter示例,解析包含非ISO日期的String并返回LocalDate实例。
/** * 使用基于当前语言环境和提供的年表的短格式的DateTimeFormatter * 将字符串解析为ChronoLocalDate,然后将其转换为LocalDate(ISO)值。 * * @param text - Chronology和当前语言环境所期望的SHORT格式的输入日期文本。 * * @param chrono - 可选的年表。如果为null,则使用IsoChronology。 */ public static LocalDate fromString(String text, Chronology chrono) { if (text != null && !text.isEmpty()) { Locale locale = Locale.getDefault(Locale.Category.FORMAT); if (chrono == null) { chrono = IsoChronology.INSTANCE; } String pattern = "M/d/yyyy GGGGG"; DateTimeFormatter df = new DateTimeFormatterBuilder().parseLenient() .appendPattern(pattern) .toFormatter() .withChronology(chrono) .withDecimalStyle(DecimalStyle.of(locale)); TemporalAccessor temporal = df.parse(text); ChronoLocalDate cDate = chrono.date(temporal); return LocalDate.from(cDate); } return null; }
当使用以下字符串调用该方法时:
System.out.printf("%s%n", StringConverter.fromString("10/29/0008 H", JapaneseChronology.INSTANCE)); System.out.printf("%s%n", StringConverter.fromString("10/29/0085 1", MinguoChronology.INSTANCE)); System.out.printf("%s%n", StringConverter.fromString("10/29/2539 B.E.", ThaiBuddhistChronology.INSTANCE)); System.out.printf("%s%n", StringConverter.fromString("6/16/1417 1", HijrahChronology.INSTANCE));
打印的字符串都应该转换回1996年10月29日:
1996-10-29 1996-10-29 1996-10-29 1996-10-29
旧版日期时间代码
在Java SE 8发布之前,Java的日期和时间机制是由
java.util.Date
,java.util.Calendar
和java.util.TimeZone
类及其子类(例如java.util.GregorianCalendar)提供的。这些类有一些缺点,包括: - Calendar类不是类型安全的。
- 由于这些类是可变的,因此无法在多线程应用程序中使用。
- 由于月份的不寻常编号和缺乏类型安全性,应用程序代码中常见错误。
-
与旧代码的互操作性
也许您有使用java.util日期和时间类的旧代码,并且希望在代码中进行最少更改的情况下利用java.time的功能。
JDK 8发布中添加了几个方法,允许在java.util和java.time对象之间进行转换:
- Calendar.toInstant()将Calendar对象转换为Instant。
- GregorianCalendar.toZonedDateTime()将GregorianCalendar实例转换为ZonedDateTime。
- GregorianCalendar.from(ZonedDateTime)使用默认区域设置从ZonedDateTime实例创建GregorianCalendar对象。
- Date.from(Instant)从Instant创建Date对象。
- Date.toInstant()将Date对象转换为Instant。
- TimeZone.toZoneId()将TimeZone对象转换为ZoneId。
-
Instant inst = date.toInstant(); Date newDate = Date.from(inst);
以下示例将 GregorianCalendar 转换为 ZonedDateTime,然后将 ZonedDateTime 转换为 GregorianCalendar。其他基于时间的类使用 ZonedDateTime 实例创建:
GregorianCalendar cal = ...; TimeZone tz = cal.getTimeZone(); int tzoffset = cal.get(Calendar.ZONE_OFFSET); ZonedDateTime zdt = cal.toZonedDateTime(); GregorianCalendar newCal = GregorianCalendar.from(zdt); LocalDateTime ldt = zdt.toLocalDateTime(); LocalDate date = zdt.toLocalDate(); LocalTime time = zdt.toLocalTime();
将 java.util 日期和时间功能映射到 java.time
由于 Java SE 8 版本中的 Java 日期和时间实现已完全重新设计,您不能仅仅替换一个方法来使用另一个方法。如果您想要使用 java.time 包提供的丰富功能,最简单的解决方案是使用前面部分列出的 toInstant 或 toZonedDateTime 方法。然而,如果您不想使用该方法或该方法不满足您的需求,则必须重写您的日期时间代码。
在 概述 页面上介绍的表格是评估哪个 java.time 类满足您需求的好起点。
两个 API 之间没有一一对应的映射关系,但下表大致列出了 java.util 日期和时间类的哪些功能对应到 java.time 的 API。
以下示例将Calendar实例转换为ZonedDateTime实例。请注意,必须提供时区才能从Instant转换为ZonedDateTime:
Calendar now = Calendar.getInstance(); ZonedDateTime zdt = ZonedDateTime.ofInstant(now.toInstant(), ZoneId.systemDefault()));
以下示例展示了一个 Date 到 Instant 的转换:
日期和时间格式化
尽管java.time.format.DateTimeFormatter提供了一个强大的机制来格式化日期和时间值,但你也可以直接使用java.time基于时间的类与java.util.Formatter和String.format一起使用,使用与java.util日期和时间类相同的基于模式的格式化。
概述
java.time包包含许多类,您的程序可以使用这些类来表示时间和日期。这是一个非常丰富的API。基于ISO的日期的关键入口点如下:
- Instant类提供了时间线的机器视图。
- LocalDate、LocalTime和LocalDateTime类提供了人类视图的日期和时间,不涉及任何时区。
- ZoneId、ZoneRules和ZoneOffset类描述时区、时区偏移和时区规则。
-
其他非ISO日历系统可以使用java.time.chrono包来表示。这个包超出了本教程的范围,但非ISO日期转换页面提供了有关将基于ISO的日期转换为其他日历系统的信息。
日期时间API是作为Java社区过程的一部分在JSR 310的指定下开发的。有关更多信息,请参阅JSR 310: 日期和时间API。
- ZonedDateTime类表示具有时区的日期和时间。OffsetDateTime和OffsetTime类分别表示日期和时间或时间,并考虑时区偏移。
- Duration类以秒和纳秒为单位测量时间的数量。
- Period类使用年、月和天来测量时间的数量。
问题和练习:日期时间API
问题
1. 你会使用哪个类来存储你的生日,包括年、月、日、秒和纳秒?
2. 给定一个随机日期,你如何找到上一个星期四的日期?
3. ZoneId和ZoneOffset之间有什么区别?
4. 如何将Instant转换为ZonedDateTime?如何将ZonedDateTime转换为Instant?
练习
1. 编写一个示例,针对给定的年份,报告该年份中每个月的长度。
2. 编写一个示例,针对当前年份的给定月份,列出该月份中所有的星期一。
3. 编写一个示例,测试给定的日期是否是一个星期五的13日。