Java8新特性整理之新的时间和日期API(终章)

时间:2022-01-18 22:05:31

前言

Java8之前我们使用DateCalendar这两个类处理时间,但有的特性只在某一个类有提供,比如用
于以语言无关的方式格式化和解析日期或时间的DateFormat方法就只在Date类里有。DateFormat方法也有它自己的问题。

比如,它不是线程安全的。这意味着两个线程如果尝试使用同一个formatter解析日期,你可能会得到无法预期的结果。

最后,Date和Calendar类都是可以变的。能把2014年3月18日修改成4月18日意味着什么呢?

这种设计会将你拖入维护的噩梦,接下来我们从最基本的用例入手,比如创建同时适合人与机器的日期和时间,逐渐转入到日期和时间API更高级的一些应用,比如
操纵、解析、打印输出日期-时间对象,使用不同的时区。

JDK1.8提供的日期处理类都是不可变对象,所以是线程安全的。

使用 LocalDate 和 LocalTime

开始使用新的日期和时间API时,你最先碰到的可能是LocalDate类。该类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关
的信息。

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

@Test
public void testLocalDateOf() {
LocalDate localDate = LocalDate.of(2017, 12, 12);
int year = localDate.getYear();
int month = localDate.getMonthValue();
int day = localDate.getDayOfWeek().getValue();
int maxLength = localDate.getMonth().maxLength();
int minLength = localDate.getMonth().minLength();
boolean isLeap = localDate.isLeapYear();
System.out.println("TimeTest.testLocalDateOf year: " + year + "\tmonth: " + month + "\tday: " + day
+ "\tmaxLength: " + maxLength + "\tminLength: " + minLength + "\tisLeap: " + isLeap);
}

获取当前的日期:

LocalDate now = LocalDate.now();

使用TemporalField读取LocalDate的值

TemporalField是一个接口,它定义了如何访问temporal对象某个字段的值。ChronoField枚举类实现了这一接口,所以你可以很方便地使用get方法得到枚举元素的值:

@Test
public void testTemporalField() {
LocalDate localDate = LocalDate.of(2017, 12, 12);
int year = localDate.get(ChronoField.YEAR);
int month = localDate.get(ChronoField.MONTH_OF_YEAR);
int day = localDate.get(ChronoField.DAY_OF_MONTH);

System.out.println("TimeTest.testTemporalField year: " + year + "\tmonth: " + month + "\tday: " + day);
}

创建LocalTime并读取其值

@Test
public void testLocalTime() {
LocalTime time = LocalTime.of(14, 22, 28);
int hour = time.getHour();
int minute = time.getMinute();
int second = time.getSecond();
System.out.println("TimeTest.testLocalTime hour: " + hour + "\tminute: " + minute + "\tsecond: " + second);
}

LocalDate和LocalTime都可以通过解析代表它们的字符串创建。使用静态方法parse

LocalDate date = LocalDate.parse("2017-12-12"); 
LocalTime time = LocalTime.parse("14:22:28");

合并日期和时间

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

@Test
public void testLocalDateTimeCombine() {
LocalDate date = LocalDate.of(2017, 12, 12);
LocalTime time = LocalTime.of(14, 22, 28);

LocalDateTime dt1 = LocalDateTime.of(2017, Month.MARCH, 12, 14, 22, 28);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
System.out.println("TimeTest.testLocalDateTimeCombine dt1: " + dt1 + "\td2: " + dt2 + "\tdt3: " + dt3
+ "\tdt4: " + dt4 +"\tdt5: " +dt5);
}

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

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

机器的日期和时间格式

Instant类的设计初衷是为了便于机器使用,获取当前时刻的时间戳:

@Test
public void testInstant() {
Instant instant = Instant.now();
System.out.println("TimeTest.testInstant 时间戳 : " + System.currentTimeMillis());
System.out.println("TimeTest.testInstant 时间戳 : " + instant.atZone(ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli());
System.out.println("TimeTest.testInstant 时间戳 : " + instant.atZone(ZoneId.of("GMT+08:00")).toInstant().toEpochMilli());
System.out.println("TimeTest.testInstant 时间戳 : " + instant.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
}

持续时间,时间间隔

两个日期的持续时间:

@Test
public void testDuration() {
LocalDate date1 = LocalDate.of(2017, 12, 12);
LocalDate date2 = LocalDate.of(2017, 12, 24);
LocalTime time1 = LocalTime.of(15, 7, 50);
LocalTime time2 = LocalTime.of(16, 8, 50);
LocalDateTime dateTime1 = LocalDateTime.of(2017, Month.MARCH, 12, 14, 22, 28);
LocalDateTime dateTime2 = LocalDateTime.of(2018, Month.MARCH, 12, 14, 22, 28);
Instant instant1 = Instant.ofEpochSecond(1);
Instant instant2 = Instant.now();
Duration d1 = Duration.between(time1, time2);
Duration d2 = Duration.between(dateTime1, dateTime2);
Duration d3 = Duration.between(instant1, instant2);
// 这里会抛异常java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds
Duration d4 = Duration.between(date1, date2);
System.out.println("TimeTest.testDuration d1: " + d1.getSeconds() +"\td2: " + d2.getSeconds() +"\td3: " + d3.getSeconds());
}

由于LocalDateTime和Instant是为不同的目的而设计的,一个是为了便于人阅读使用,
另一个是为了便于机器处理,所以你不能将二者混用。如果你试图在这两类对象之间创建
duration,会触发一个DateTimeException异常。此外,由于**Duration类主要用于以秒和纳
秒衡量时间的长短**,你不能向between方法传递一个LocalDate对象做参数,否则会
抛异常java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds。

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

@Test
public void testPeriod() {
LocalDate now = LocalDate.now();
LocalDate dates = LocalDate.of(2017, Month.JULY, 2);
Period period = Period.between(dates, now);
System.out.println("TimeTest.testPeriod 两个日期间隔 : " + period.getYears() + "年"
+ period.getMonths() +"月" + period.getDays() + "天");
}

操纵、解析和格式化日期

以比较直观的方式操纵LocalDate的属性

@Test
public void testModifyLocalDate() {
LocalDate date1 = LocalDate.of(2017, 3, 18);
LocalDate date2 = date1.withYear(2011);
LocalDate date3 = date2.withDayOfMonth(25);
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);
System.out.println("TimeTest.testModifyLocalDate date1: " + date1);
System.out.println("TimeTest.testModifyLocalDate date2: " + date2);
System.out.println("TimeTest.testModifyLocalDate date3: " + date3);
System.out.println("TimeTest.testModifyLocalDate date4: " + date4);
}

输出:

TimeTest.testModifyLocalDate date1: 2017-03-18
TimeTest.testModifyLocalDate date2: 2011-03-18
TimeTest.testModifyLocalDate date3: 2011-03-25
TimeTest.testModifyLocalDate date4: 2011-09-25

这些方法都声明在Temporal接口,所有的日期和时间API类都实现这两个方法,
使用getwith方法,我们可以将Temporal对象值的读取和修改区分开。

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

@Test
public void testModifyLocalDate2() {
LocalDate date1 = LocalDate.of(2017, 3, 18);
LocalDate date2 = date1.plusWeeks(1);
LocalDate date3 = date2.minusYears(3);
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);
System.out.println("TimeTest.testModifyLocalDate2 date1: " + date1);
System.out.println("TimeTest.testModifyLocalDate2 date2: " + date2);
System.out.println("TimeTest.testModifyLocalDate2 date3: " + date3);
System.out.println("TimeTest.testModifyLocalDate2 date4: " + date4);
}

输出:

TimeTest.testModifyLocalDate2 date1: 2017-03-18
TimeTest.testModifyLocalDate2 date2: 2017-03-25
TimeTest.testModifyLocalDate2 date3: 2014-03-25
TimeTest.testModifyLocalDate2 date4: 2014-09-25

与我们刚才介绍的get和with方法类似,代码最后一行使用的plus方法也是通用
方法,它和minus方法都声明于Temporal接口中。

一个求未来或历史的今天的栗子:

@Test
public void testChronology() {
LocalDate today = LocalDate.now();
System.out.println("TimeTest.testChronology today : " + today);
LocalDate oneToday = today.plus(1, ChronoUnit.WEEKS);
System.out.println("TimeTest.testChronology 一周后的今天 : " + oneToday);
LocalDate preYear = today.minus(1, ChronoUnit.YEARS);
System.out.println("TimeTest.testChronology 一年前的今天 : " + preYear);
LocalDate postYear = today.plus(1, ChronoUnit.YEARS);
System.out.println("TimeTest.testChronology 一年后的今天 : " + postYear);
}

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

下表对这些通用的方法进行了总结

方 法 名 描 述
from 静态方法,依据传入的 Temporal 对象创建对象实例
now 静态方法,依据系统时钟创建 Temporal 对象
of 静态方法,由 Temporal 对象的某个部分创建该对象的实例
parse 静态方法,由字符串创建 Temporal 对象的实例
atOffset 非静态方法,将 Temporal 对象和某个时区偏移相结合
atZone 非静态方法,将 Temporal 对象和某个时区相结合
format 非静态方法,使用某个指定的格式器将Temporal对象转换为字符串(Instant类不提供该方法)
get 非静态方法,读取 Temporal 对象的某一部分的值
minus 非静态方法,创建 Temporal 对象的一个副本,通过将当前 Temporal 对象的值减去一定的时长创建该副本
plus 非静态方法,创建 Temporal 对象的一个副本,通过将当前 Temporal 对象的值加上一定的时长创建该副本
with 非静态方法,以该 Temporal 对象为模板,对某些状态进行修改创建该对象的副本

使用 TemporalAdjuster

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

@Test
public void testTemporalAdjuster() {
LocalDate date1 = LocalDate.of(2017, 3, 18);
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));
LocalDate date3 = date2.with(lastDayOfMonth());
System.out.println("TimeTest.testTemporalAdjuster date1: " + date1);
System.out.println("TimeTest.testTemporalAdjuster date2: " + date2);
System.out.println("TimeTest.testTemporalAdjuster date3: " + date3);
}

输出:

TimeTest.testTemporalAdjuster date1: 2017-03-18
TimeTest.testTemporalAdjuster date2: 2017-03-19
TimeTest.testTemporalAdjuster date3: 2017-03-31

下表提供了TemporalAdjuster类中的工厂方法

方 法 名 描 述
dayOfWeekInMonth 创建一个新的日期,它的值为同一个月中每一周的第几天
firstDayOfMonth 创建一个新的日期,它的值为当月的第一天
firstDayOfNextMonth 创建一个新的日期,它的值为下月的第一天
firstDayOfNextYear 创建一个新的日期,它的值为明年的第一天
firstDayOfYear 创建一个新的日期,它的值为当年的第一天
firstInMonth 创建一个新的日期,它的值为同一个月中,第一个符合星期几要求的值
lastDayOfMonth 创建一个新的日期,它的值为当月的最后一天
lastDayOfNextMonth 创建一个新的日期,它的值为下月的最后一天
lastDayOfNextYear 创建一个新的日期,它的值为明年的最后一天
lastDayOfYear 创建一个新的日期,它的值为今年的最后一天
lastInMonth 创建一个新的日期,它的值为同一个月中,最后一个符合星期几要求的值
next/previous 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期
nextOrSame/previousOrSame 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期,如果该日期已经符合要求,直接返回该对象

格式化以及解析日期-时间对象

日期转字符串格式:

@Test
public void testDateToString() {
LocalDateTime localDateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatDate = localDateTime.format(formatter);
System.out.println("TimeTest.testDateToString 格式化后的日期 : " + formatDate);
}

字符串转日期格式:

@Test
public void testDateFormat() {
String datetime = "2017-12-02T16:46:48";
LocalDateTime parseDate = LocalDateTime.parse(datetime, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println("TimeTest.testDateFormat 字符串转日期 : " + parseDate);
}

处理时区

新的java.time.ZoneId类是老版java.util.TimeZone的替代品。

地区ID都为{区域}/{城市}的格式:

ZoneId romeZone = ZoneId.of("Asia/Shanghai"); 

为时间点添加时区信息 :

@Test
public void testZoneId() {
ZoneId romeZone = ZoneId.of("Asia/Shanghai");
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);
System.out.println("TimeTest.testZoneId zdt1: " + zdt1);
System.out.println("TimeTest.testZoneId zdt2: " + zdt2);
System.out.println("TimeTest.testZoneId zdt3: " + zdt3);
}

输出

TimeTest.testZoneId zdt1: 2014-03-18T00:00+08:00[Asia/Shanghai]
TimeTest.testZoneId zdt2: 2014-03-18T13:45+08:00[Asia/Shanghai]
TimeTest.testZoneId zdt3: 2018-02-08T17:08:31.929+08:00[Asia/Shanghai]

通过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);

UTC/GMT固定偏差计算时区

GMT(格林威治时间)、CST(可视为美国、澳大利亚、古巴或中国的标准时间)、PST(太平洋时间)

GMT: UTC +0 = GMT: GMT +0
CST: UTC +8 = CST: GMT +8
PST: UTC -8 = PST: GMT -8

以相对于UTC/格林尼治时间的偏差方式表示日期时间:

@Test
public void testZoneOffset() {
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
LocalDateTime dateTime = LocalDateTime.now();
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(dateTime, newYorkOffset);
System.out.println("TimeTest.testZoneOffset dateTimeInNewYork: " + dateTimeInNewYork);
}

更多用例查看此文:
https://www.cnblogs.com/comeboo/p/5378922.html