java-8-实战读书笔记-第十二章-新的日期和时间-API

41次阅读

共计 8798 个字符,预计需要花费 22 分钟才能阅读完成。

一、LocalDate、LocalTime、Instant、Duration 以及 Period

1. 使用 LocalDate 和 LocalTime

创建一个 LocalDate 对象并读取其值

LocalDate date = LocalDate.of(2014, 3, 18); //2014-03-18
int year = date.getYear(); //2014
Month month = date.getMonth();//MARCH 
int day = date.getDayOfMonth();//18 
DayOfWeek dow = date.getDayOfWeek();//TUESDAY
int len = date.lengthOfMonth();//31
boolean leap = date.isLeapYear();//false

你还可以通过传递一个 TemporalField 参数给 get 方法拿到同样的信息。TemporalField 是一个接口,它定义了如何访问 temporal 对象某个字段的值。ChronoField 枚举实现了这一接口,所以你可以很方便地使用 get 方法得到枚举元素的值,如下所示。
使用 TemporalField 读取 LocalDate 的值

int year = date.get(ChronoField.YEAR); 
int month = date.get(ChronoField.MONTH_OF_YEAR); 
int day = date.get(ChronoField.DAY_OF_MONTH);

类似地,一天中的时间,比如 13:45:20,可以使用 LocalTime 类表示。类似地,一天中的时间,比如 13:45:20,可以使用 LocalTime 类表示。

LocalTime time = LocalTime.of(13, 45, 20); 
int hour = time.getHour(); 
int minute = time.getMinute(); 
int second = time.getSecond();

LocalDate 和 LocalTime 都可以通过解析代表它们的字符串创建。使用静态方法 parse,你可以实现这一目的:

LocalDate date = LocalDate.parse("2014-03-18"); 
LocalTime time = LocalTime.parse("13:45:20"); 

你可以向 parse 方法传递一个 DateTimeFormatter。它是替换老版 java.util.DateFormat 的推荐替代品。

2. 合并日期和时间

这个复合类名叫 LocalDateTime,是 LocalDate 和 LocalTime 的合体。它同时表示了日期和时间,但不带有时区信息,你可以直接创建,也可以通过合并日期和时间对象构造,如下所示。
直接创建 LocalDateTime 对象,或者通过合并日期和时间的方式创建

// 2014-03-18T13:45:20 
LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20); 
LocalDateTime dt2 = LocalDateTime.of(date, time); 
LocalDateTime dt3 = date.atTime(13, 45, 20); 
LocalDateTime dt4 = date.atTime(time); 
LocalDateTime dt5 = time.atDate(date);

注意,通过它们各自的 atTime 或者 atDate 方法,向 LocalDate 传递一个时间对象,或者向 LocalTime 传递一个日期对象的方式,你可以创建一个 LocalDateTime 对象。你也可以使用 toLocalDate 或者 toLocalTime 方法,从 LocalDateTime 中提取 LocalDate 或者 LocalTime 组件:

LocalDate date1 = dt1.toLocalDate(); 
LocalTime time1 = dt1.toLocalTime();

3. 机器的日期和时间格式

新的 java.time.Instant 类对时间建模的方式,基本上它是以 Unix 元年时间(传统的设定为 UTC 时区 1970 年 1 月 1 日午夜时分)开始所经历的
秒数进行计算。你可以通过向静态工厂方法 ofEpochSecond 传递一个代表秒数的值创建一个该类的实例。静态工厂方法 ofEpochSecond 还有一个增强的重载版本,它接收第二个以纳秒为单位的参数值,对传入作为秒数的参数进行调整。重载的版本会调整纳秒参数,确保保存的纳秒分片在 0 到 999 999 999 之间。这意味着下面这些对 ofEpochSecond 工厂方法的调用会返回几乎同样的 Instant 对象:

Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000); 
Instant.ofEpochSecond(4, -1_000_000_000);

从 Java7 开始,你就可以在你的 Java 代码里把长整型数字比如 10000000000 写成一个更具可读性 10_000_000_000。

Instant 类也支持静态工厂方法 now,它能够帮你获取当前时刻的时间戳。它包含的是由秒及纳秒所构成的数字。所以,它无法处理那些我们非常容易理解的时间单位。比如下面这段语句:

int day = Instant.now().get(ChronoField.DAY_OF_MONTH); 

它会抛出下面这样的异常:

java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: 
 DayOfMonth

4. 定义 Duration 或 Period

你可以创建两个 LocalTimes 对象、两个 LocalDateTimes 对象,或者两个 Instant 对象之间的duration(持续期间),如下所示:

Duration d1 = Duration.between(time1, time2); 
Duration d1 = Duration.between(dateTime1, dateTime2); 
Duration d2 = Duration.between(instant1, instant2);

如果你试图在这两类对象之间创建 duration,会触发一个 DateTimeException 异常。此外,由于 Duration 类主要用于以秒和纳秒衡量时间的长短,你不能仅向 between 方法传递一个 LocalDate 对象做参数。

如果你需要以 年、月或者日 的方式对多个时间单位建模,可以使用 Period 类。使用该类的工厂方法 between,你可以使用得到两个 LocalDate 之间的时长,如下所示:

Period tenDays = Period.between(LocalDate.of(2014, 3, 8), 
 LocalDate.of(2014, 3, 18));

Duration 和 Period 类都提供了很多非常方便的工厂类,直接创建对应的实例;不再是只能以两个 temporal 对象的差值的方式来定义它们的对象。
创建 Duration 和 Period 对象

Duration threeMinutes = Duration.ofMinutes(3); 
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES); 
Period tenDays = Period.ofDays(10); 
Period threeWeeks = Period.ofWeeks(3); 
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

日期 / 时间类中表示时间间隔的通用方法

Temporal接口的实现类:
HijrahDate, Instant, JapaneseDate, LocalDate, LocalDateTime, LocalTime, MinguoDate, OffsetDateTime, OffsetTime, ThaiBuddhistDate, Year, YearMonth, ZonedDateTime

二、操纵、解析和格式化日期

如果你已经有一个 LocalDate 对象,想要创建它的一个修改版,最直接也最简单的方法是使用 withAttribute 方法。withAttribute 方法会创建对象的一个副本,并按照需要修改它的属性。注意,下面的这段代码中所有的方法都返回一个修改了属性的对象。它们都不会修改原来的对象!

LocalDate date1 = LocalDate.of(2014, 3, 18); 
LocalDate date2 = date1.withYear(2011); 
LocalDate date3 = date2.withDayOfMonth(25); 
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);

采用更通用的 with 方法能达到同样的目的,它接受的第一个参数是一个 TemporalField 对象,格式类似上面代码最后一行。更确切地说,使用 get 和 with 方法,我们可以将 Temporal 对象值的读取和修改区分开。如果 Temporal 对象不支持请求访问的字段,它会抛出一个 UnsupportedTemporalTypeException 异常。

以相对方式修改 LocalDate 对象的属性:

LocalDate date1 = LocalDate.of(2014, 3, 18); 
LocalDate date2 = date1.plusWeeks(1); 
LocalDate date3 = date2.minusYears(3); 
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);

像 LocalDate、LocalTime、LocalDateTime 以及 Instant 这样表示时间点的日期时间类提供了大量通用的方法

1. 使用 TemporalAdjuster

有的时候,你需要进行一些更加复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可以使用重载版本的 with 方法,向其传递一个提供了更多定制化选择的 TemporalAdjuster 对象,更加灵活地处理日期。对于最常见的用例,日期和时间 API 已经提供了大量预定义的 TemporalAdjuster。你可以通过 TemporalAdjuster 类的静态工厂方法访问它们。

使用预定义的 TemporalAdjuster

import static java.time.temporal.TemporalAdjusters.*; 
LocalDate date1 = LocalDate.of(2014, 3, 18); 
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); //2014-03-23
LocalDate date3 = date2.with(lastDayOfMonth());//2014-03-31

TemporalAdjuster 类中的工厂方法

TemporalAdjuster 接口

@FunctionalInterface 
public interface TemporalAdjuster {Temporal adjustInto(Temporal temporal); 
}

如果你想要使用 Lambda 表达式定义 TemporalAdjuster 对象,推荐使用 TemporalAdjusters 类的静态工厂方法 ofDateAdjuster,它接受一个 UnaryOperator<LocalDate>

3. 打印输出及解析日期时间对象

新的 java.time.format 包就是特别为这个目的而设计的。这个包中,最重要的类是 DateTimeFormatter。创建格式器最简单的方法是通过它的静态工厂方法以及常量。像 BASIC_ISO_DATE 和 ISO_LOCAL_DATE 这样的常量是 DateTimeFormatter 类的预定义实例。

LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); //20140318
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);//2014-03-18

你可以使用工厂方法 parse 达到重创该日期对象的目的:

LocalDate date1 = LocalDate.parse("20140318", 
 DateTimeFormatter.BASIC_ISO_DATE); 
LocalDate date2 = LocalDate.parse("2014-03-18", 
 DateTimeFormatter.ISO_LOCAL_DATE);

和老的 java.util.DateFormat 相比较,所有的 DateTimeFormatter 实例都是线程安全的。

按照某个模式创建 DateTimeFormatter

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); 
LocalDate date1 = LocalDate.of(2014, 3, 18); 
String formattedDate = date1.format(formatter); 
LocalDate date2 = LocalDate.parse(formattedDate, formatter);

创建一个本地化的 DateTimeFormatter

DateTimeFormatter italianFormatter = 
 DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN); 
LocalDate date1 = LocalDate.of(2014, 3, 18); 
String formattedDate = date.format(italianFormatter); // 18. marzo 2014 
LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);

DateTimeFormatterBuilder 类还提供了更复杂的格式器, 你可以通过 DateTimeFormatterBuilder 自己编程实现我们在上面代码使用的 italianFormatter,代码清单如下。

DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder() 
 .appendText(ChronoField.DAY_OF_MONTH) 
 .appendLiteral(".") 
 .appendText(ChronoField.MONTH_OF_YEAR) 
 .appendLiteral(" ") 
 .appendText(ChronoField.YEAR) 
 .parseCaseInsensitive() 
 .toFormatter(Locale.ITALIAN);

三、处理不同的时区和历法

新的 java.time.ZoneId 类是老版 java.util.TimeZone 的替代品。跟其他日期和时间类一样,ZoneId 类也是无法修改的。时区是按照一定的规则将区域划分成的标准时间相同的区间。在 ZoneRules 这个类中包含了 40 个这样的实例。你可以简单地通过调用 ZoneId 的 getRules()得到指定时区的规则。每个特定的 ZoneId 对象都由一个地区 ID 标识,比如:

ZoneId romeZone = ZoneId.of("Europe/Rome");

地区 ID 都为“{区域}/{城市}”的格式, 你可以通过 Java 8 的新方法 toZoneId 将一个老的时区对象转换为 ZoneId:

ZoneId zoneId = TimeZone.getDefault().toZoneId();

为时间点添加时区信息

LocalDate date = LocalDate.of(2014, Month.MARCH, 18); 
ZonedDateTime zdt1 = date.atStartOfDay(romeZone); 
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45); 
ZonedDateTime zdt2 = dateTime.atZone(romeZone); 
Instant instant = Instant.now(); 
ZonedDateTime zdt3 = instant.atZone(romeZone);

通过 ZoneId,你还可以将 LocalDateTime 转换为 Instant:

LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45); 
Instant instantFromDateTime = dateTime.toInstant(romeZone); 

你也可以通过反向的方式得到 LocalDateTime 对象:

Instant instant = Instant.now(); 
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);

1. 利用和 UTC/ 格林尼治时间的固定偏差计算时区

ZoneOffset 类,它是 ZoneId 的一个子类,表示的是当前时间和伦敦格林尼治子午线时间的差异:

ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");

注意,使用这种方式定义的 ZoneOffset 并未考虑任何日光时的影响,所以在大多数情况下,不推荐使用。ZoneOffset 也是 ZoneId。
你甚至还可以创建这样的 OffsetDateTime,它使用 ISO-8601 的历法系统,以相对于 UTC/ 格林尼治时间的偏差方式表示日期时间。

LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45); 
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(date, newYorkOffset)

2. 使用别的日历系统

Java 8 中另外还提供了 4 种其他的日历系统。这些日历系统中的每一个都有一个对应的日志类,分别是 ThaiBuddhistDate、MinguoDate、JapaneseDate 以及 HijrahDate。所有这些类以及 LocalDate 都实现了 ChronoLocalDate 接口。
如下所示:

LocalDate date = LocalDate.of(2014, Month.MARCH, 18); 
JapaneseDate japaneseDate = JapaneseDate.from(date); 

或者,你还可以为某个 Locale 显式地创建日历系统,接着创建该 Locale 对应的日期的实例。新的日期和时间 API 中,Chronology 接口建模了一个日历系统,使用它的静态工厂方法 ofLocale,可以得到它的一个实例,代码如下:

Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN); 
ChronoLocalDate now = japaneseChronology.dateNow(); 

日期及时间 API 的设计者建议我们使用 LocalDate,尽量避免使用 ChronoLocalDate,原因是开发者在他们的代码中可能会做一些假设,而这些假设在不同的日历系统中,有可能不成立。比如,有人可能会做这样的假设,即一个月天数不会超过 31 天,一年包括 12 个月,或者一年中包
含的月份数目是固定的。由于这些原因,我们建议你尽量在你的应用中使用 LocalDate,包括存储、操作、业务规则的解读;不过如果你需要将程序的输入或者输出本地化,这时你应该使用 ChronoLocalDate 类。

伊斯兰教日历
在 Java 8 新添加的几种日历类型中,HijrahDate(伊斯兰教日历)是最复杂一个,因为它会发生各种变化。Hijrah 日历系统构建于农历月份继承之上。Java 8 提供了多种方法判断一个月份,比如新月,在世界的哪些地方可见,或者说它只能首先可见于沙特阿拉伯。withVariant 方法可以用于选择期望的变化。为了支持 HijrahDate 这一标准,Java 8 中还包括了乌姆库拉(Umm Al-Qura)变量。
下面这段代码作为一个例子说明了如何在 ISO 日历中计算当前伊斯兰年中斋月的起始和终止日期:

HijrahDate ramadanDate = 
 HijrahDate.now().with(ChronoField.DAY_OF_MONTH, 1)
 .with(ChronoField.MONTH_OF_YEAR, 9); 
System.out.println("Ramadan starts on" + 
 IsoChronology.INSTANCE.date(ramadanDate) + 
 "and ends on" + 
 IsoChronology.INSTANCE.date( 
 ramadanDate.with(TemporalAdjusters.lastDayOfMonth())));

正文完
 0