Java 中文官方教程 2022 版(二十八)(1)https://developer.aliyun.com/article/1486863
日期时间类
日期时间 API 提供了三个基于时间的类,可以与时区一起使用:
ZonedDateTime
处理具有与格林威治/协调世界时的相应时区偏移的日期和时间。OffsetDateTime
处理具有与格林威治/协调世界时的相应时区偏移的日期和时间,没有时区 ID。OffsetTime
处理具有与格林威治/协调世界时的相应时区偏移的时间,没有时区 ID。
何时使用OffsetDateTime
而不是ZonedDateTime
?如果您正在编写基于地理位置的自己的日期和时间计算规则的复杂软件,或者如果您正在存储仅跟踪与格林威治/协调世界时的绝对偏移的时间戳的数据库,那么您可能希望使用OffsetDateTime
。此外,XML 和其他网络格式将日期时间传输定义为OffsetDateTime
或OffsetTime
。
尽管所有三个类都保持与格林威治/协调世界时的偏移,但只有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"); // Leaving from San Francisco on July 20, 2013, at 7:30 p.m. 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("LEAVING: %s (%s)%n", out1, leavingZone); } catch (DateTimeException exc) { System.out.printf("%s can't be formatted!%n", departure); throw exc; } // Flight is 10 hours and 50 minutes, or 650 minutes ZoneId arrivingZone = ZoneId.of("Asia/Tokyo"); ZonedDateTime arrival = departure.withZoneSameInstant(arrivingZone) .plusMinutes(650); try { String out2 = arrival.format(format); System.out.printf("ARRIVING: %s (%s)%n", out2, arrivingZone); } catch (DateTimeException exc) { System.out.printf("%s can't be formatted!%n", arrival); throw exc; } if (arrivingZone.getRules().isDaylightSavings(arrival.toInstant())) System.out.printf(" (%s daylight saving time will be in effect.)%n", arrivingZone); else System.out.printf(" (%s standard time will be in effect.)%n", arrivingZone);
这将产生以下输出:
LEAVING: Jul 20 2013 07:30 PM (America/Los_Angeles) ARRIVING: Jul 21 2013 10:20 PM (Asia/Tokyo) (Asia/Tokyo standard time will be in effect.)
OffsetDateTime
OffsetDateTime类实际上将LocalDateTime
类与ZoneOffset
类结合在一起。它用于表示完整的日期(年、月、日)和时间(时、分、秒、纳秒)以及与格林威治/UTC 时间的偏移(+/-小时:分钟,如+06:00
或-08:00
)。
以下示例使用OffsetDateTime
与TemporalAdjuster.lastDay
方法找到 2013 年 7 月最后一个星期四。
// Find the last Thursday in July 2013. 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("The last Thursday in July 2013 is the %sth.%n", lastThursday.getDayOfMonth());
运行此代码的输出为:
The last Thursday in July 2013 is the 25th.
OffsetTime
OffsetTime类实际上将LocalTime
类与ZoneOffset
类结合在一起。它用于表示时间(时、分、秒、纳秒)以及与格林威治/UTC 时间的偏移(+/-小时:分钟,如+06:00
或-08:00
)。
OffsetTime
类在与OffsetDateTime
类相同的情况下使用,但不需要跟踪日期时使用。
Instant 类
原文:
docs.oracle.com/javase/tutorial/datetime/iso/instant.html
日期时间 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
与时区绑定将Instant
转换为另一个类,例如LocalDateTime
或ZonedDateTime
。然后可以访问所需单位的值。以下代码使用ofInstant
方法和默认时区将Instant
转换为LocalDateTime
对象,然后以更可读的形式打印出日期和时间:
Instant timestamp; ... LocalDateTime ldt = LocalDateTime.ofInstant(timestamp, ZoneId.systemDefault()); System.out.printf("%s %d %d at %d:%d%n", ldt.getMonth(), ldt.getDayOfMonth(), ldt.getYear(), ldt.getHour(), ldt.getMinute());
输出将类似于以下内容:
MAY 30 2013 at 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 is not parsable!%n", input); throw exc; // Rethrow the exception. } // 'date' has been successfully parsed
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 can't be formatted!%n", departure); throw exc; }
这个示例的输出,打印了到达和离开时间,如下所示:
LEAVING: Jul 20 2013 07:30 PM (America/Los_Angeles) ARRIVING: Jul 21 2013 10:20 PM (Asia/Tokyo)
时间包
原文:
docs.oracle.com/javase/tutorial/datetime/iso/temporal.html
java.time.temporal
包提供了支持日期和时间代码的一组接口、类和枚举,特别是日期和时间计算。
这些接口旨在在最低级别使用。典型的应用代码应该根据具体类型(如LocalDate
或ZonedDateTime
)声明变量和参数,而不是根据Temporal
接口。这与声明类型为String
而不是CharSequence
的变量完全相同。
时间和 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 and Duration 中讨论)实现了TemporalAmount
接口。
ChronoField 和 IsoFields
实现了TemporalField
接口的ChronoField
枚举提供了一组丰富的常量,用于访问日期和时间值。一些示例包括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
实现了TemporalUnit
接口的ChronoUnit
枚举提供了一组基于日期和时间的标准单位,从毫秒到千年。请注意,并非所有的ChronoUnit
对象都被所有类支持。例如,Instant
类不支持ChronoUnit.MONTHS
或ChronoUnit.YEARS
。日期时间 API 中的类包含isSupported(TemporalUnit)
方法,可用于验证一个类是否支持特定的时间单位。下面对isSupported
的调用返回false
,确认Instant
类不支持ChronoUnit.DAYS
。
Instant instant = Instant.now(); boolean isSupported = instant.isSupported(ChronoUnit.DAYS);
时间调整器
原文:
docs.oracle.com/javase/tutorial/datetime/iso/adjusters.html
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 is on a %s%n", date, dotw); System.out.printf("first day of Month: %s%n", date.with(TemporalAdjusters.firstDayOfMonth())); System.out.printf("first Monday of Month: %s%n", date.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY))); System.out.printf("last day of Month: %s%n", date.with(TemporalAdjusters.lastDayOfMonth())); System.out.printf("first day of next Month: %s%n", date.with(TemporalAdjusters.firstDayOfNextMonth())); System.out.printf("first day of next Year: %s%n", date.with(TemporalAdjusters.firstDayOfNextYear())); System.out.printf("first day of Year: %s%n", date.with(TemporalAdjusters.firstDayOfYear()));
这将产生以下输出:
2000-10-15 is on a SUNDAY first day of Month: 2000-10-01 first Monday of Month: 2000-10-02 last day of Month: 2000-10-31 first day of next Month: 2000-11-01 first day of next Year: 2001-01-01 first day of Year: 2000-01-01
自定义调整器
您还可以创建自定义调整器。为此,您需要创建一个实现了TemporalAdjuster
接口并带有adjustInto(Temporal)
方法的类。来自NextPayday
示例的PaydayAdjuster
类是一个自定义调整器。PaydayAdjuster
评估传入的日期并返回下一个发薪日,假设发薪日每月发生两次:在每月的 15 日和最后一天。如果计算出的日期落在周末,则使用前一个星期五。假定当前日历年。
/** * The adjustInto method accepts a Temporal instance * and returns an adjusted LocalDate. If the passed in * parameter is not a LocalDate, then a DateTimeException is thrown. */ 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); }
调整器的调用方式与预定义调整器相同,使用with
方法。以下代码行来自NextPayday
示例:
LocalDate nextPayday = date.with(new PaydayAdjuster());
在 2013 年,6 月 15 日和 6 月 30 日都落在周末。使用 2013 年 6 月 3 日和 6 月 18 日的相应日期运行NextPayday
示例,得到以下结果:
Given the date: 2013 Jun 3 the next payday: 2013 Jun 14 Given the date: 2013 Jun 18 the next payday: 2013 Jun 28
时间查询
原文:
docs.oracle.com/javase/tutorial/datetime/iso/queries.html
TemporalQuery
可用于从基于时间的对象中检索信息。
预定义查询
TemporalQueries
类(注意是复数形式)提供了几个预定义查询,包括在应用程序无法识别基于时间的对象类型时有用的方法。与调整器一样,预定义查询被定义为静态方法,并设计用于与静态导入语句一起使用。
例如,precision
查询返回特定基于时间的对象可以返回的最小ChronoUnit
。以下示例在几种类型的基于时间的对象上使用了precision
查询:
TemporalQuery<TemporalUnit> query = TemporalQueries.precision(); System.out.printf("LocalDate precision is %s%n", LocalDate.now().query(query)); System.out.printf("LocalDateTime precision is %s%n", LocalDateTime.now().query(query)); System.out.printf("Year precision is %s%n", Year.now().query(query)); System.out.printf("YearMonth precision is %s%n", YearMonth.now().query(query)); System.out.printf("Instant precision is %s%n", Instant.now().query(query));
输出如下所示:
LocalDate precision is Days LocalDateTime precision is Nanos Year precision is Years YearMonth precision is Months Instant precision is Nanos
自定义查询
您还可以创建自定义查询。一种方法是创建一个实现了TemporalQuery
接口的类,并使用queryFrom(TemporalAccessor)
方法。CheckDate
示例实现了两个自定义查询。第一个自定义查询可以在FamilyVacations
类中找到,该类实现了TemporalQuery
接口。queryFrom
方法将传入的日期与计划的假期日期进行比较,并在日期范围内返回TRUE
。
// Returns true if the passed-in date occurs during one of the // family vacations. Because the query compares the month and day only, // the check succeeds even if the Temporal types are not the same. public Boolean queryFrom(TemporalAccessor date) { int month = date.get(ChronoField.MONTH_OF_YEAR); int day = date.get(ChronoField.DAY_OF_MONTH); // Disneyland over Spring Break if ((month == Month.APRIL.getValue()) && ((day >= 3) && (day <= 8))) return Boolean.TRUE; // Smith family reunion on Lake Saugatuck if ((month == Month.AUGUST.getValue()) && ((day >= 8) && (day <= 14))) return Boolean.TRUE; return Boolean.FALSE; }
第二个自定义查询在FamilyBirthdays
类中实现。该类提供了一个isFamilyBirthday
方法,用于将传入的日期与几个生日进行比较,如果匹配则返回TRUE
。
// Returns true if the passed-in date is the same as one of the // family birthdays. Because the query compares the month and day only, // the check succeeds even if the Temporal types are not the same. public static Boolean isFamilyBirthday(TemporalAccessor date) { int month = date.get(ChronoField.MONTH_OF_YEAR); int day = date.get(ChronoField.DAY_OF_MONTH); // Angie's birthday is on April 3. if ((month == Month.APRIL.getValue()) && (day == 3)) return Boolean.TRUE; // Sue's birthday is on June 18. if ((month == Month.JUNE.getValue()) && (day == 18)) return Boolean.TRUE; // Joe's birthday is on May 29. if ((month == Month.MAY.getValue()) && (day == 29)) return Boolean.TRUE; return Boolean.FALSE; }
FamilyBirthday
类未实现TemporalQuery
接口,可作为 lambda 表达式的一部分使用。来自CheckDate
示例的以下代码展示了如何调用两个自定义查询。
// Invoking the query without using a lambda expression. Boolean isFamilyVacation = date.query(new FamilyVacations()); // Invoking the query using a lambda expression. Boolean isFamilyBirthday = date.query(FamilyBirthdays::isFamilyBirthday); if (isFamilyVacation.booleanValue() || isFamilyBirthday.booleanValue()) System.out.printf("%s is an important date!%n", date); else System.out.printf("%s is not an important date.%n", date);
Period 和 Duration
当编写代码来指定一段时间时,请使用最符合您需求的类或方法:Duration
类、Period
类或ChronoUnit.between
方法。Duration
使用基于时间的值(秒、纳秒)来衡量一段时间。Period
使用基于日期的值(年、月、日)。
注意: 一天的Duration
确切地为 24 小时。将一天的Period
添加到ZonedDateTime
中时,根据时区可能会有所变化。例如,如果发生在夏令时的第一天或最后一天。
Java 中文官方教程 2022 版(二十八)(3)https://developer.aliyun.com/article/1486870