前言
Java8之前我们使用Date
和Calendar
这两个类处理时间,但有的特性只在某一个类有提供,比如用
于以语言无关的方式格式化和解析日期或时间的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类都实现这两个方法,
使用get
和with
方法,我们可以将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);
}