本篇内容
- 为什么在java 8 中需要引入新的日期和时间库。
- 同时为人和机器表示日期和时间。
- 定义时间的度。
- 操纵、格式化以及解析日期。
- 处理不同的时区和历法。
开始使用新的日期和时间API时,你最先碰到的可能是LocalDate类。该类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关的信息。
你可以通过静态工厂方法of创建一个LocalDate实例。LocalDate实例提供了多种方法来读取常用的值,比如年份、月份、星期几等,如下所示。
LocalDate date = LocalDate.of(2020, 12, 1);
int year = date.getYear();//2020
Month month = date.getMonth(); //December
int dayOfMonth = date.getDayOfMonth(); //一个月的第几天 1
DayOfWeek dayOfWeek = date.getDayOfWeek();//一周的第几天 TUESDAY 这个格式
int len = date.lengthOfMonth();//这个月有多少天
boolean leapYear = date.isLeapYear(); //是否是闰年
你还可以使用工厂方法从系统时钟中获取当前的日期:
LocalDate today = LocalDate.now();
int year1 = date.get(ChronoField.YEAR);
int month1 = date.get(ChronoField.DAY_OF_MONTH);
int day1 = date.get(ChronoField.DAY_OF_WEEK);
LocalTime time = LocalTime.of(12, 45, 20);
int hour = time.getHour();//12
int minute = time.getMinute();//45
int second = time.getSecond();//20
LocalDate和LocalTime都可以通过解析代表它们的字符串创建。使用静态方法parse,你可以实现这一目的:
LocalDate date = LocalDate.parse("2020-03-18");
LocalTime time1 = LocalTime.parse("12:45:20");
LocalDateTime localDateTime = LocalDateTime.of(2020, Month.MARCH, 18, 12, 45, 20);
LocalDate date = LocalDate.of(2020, 12, 1);
LocalTime time = LocalTime.of(12, 45, 20);
LocalDateTime localDateTime1 = LocalDateTime.of(date, time);
LocalDateTime localDateTime2 = date.atTime(12, 45, 20);
LocalDateTime localDateTime3 = date.atTime(time);
LocalDateTime localDateTime4 = time.atDate(date);
//你也可以使用toLocalDate或者toLocalTime方法,从LocalDateTime中提取LocalDate或者LocalTime
LocalDate localDate = localDateTime.toLocalDate();
LocalTime localTime = localDateTime.toLocalTime();
Instant的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3,0);
//2秒之后再加上100万纳秒
Instant.ofEpochSecond(2,1_000_000_000);
//4秒之前的100万纳秒
Instant.ofEpochSecond(4,-1_000_000_000);
Duration类主要用于以秒和纳秒衡量时间的长短,你不能仅向between方法传递一个LocalDate对象做参数。
LocalTime time = LocalTime.of(15, 20, 0);
LocalTime time1 = LocalTime.of(14, 30, 0);
Duration d1 = Duration.between(time1, time);
Instant instant1 = Instant.ofEpochSecond(3);
Instant instant2 = Instant.ofEpochSecond(5);
Duration d3 = Duration.between(instant1, instant2);
如果你需要以年、月或者日的方式对多个时间单位建模,可以使用Period类。使用该类的工厂方法between,你可以使用得到两个LocalDate之间的时长,如下所示:
Period between = Period.between(LocalDate.of(2020, 3, 8), LocalDate.of(2020, 5, 8));
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes1 = Duration.of(3, ChronoUnit.MINUTES);
Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthOneDay = Period.of(2, 6, 1);
Duration类和Period类共享了很多相似的方法,参见下表所示。
方法名 | 是否是静态方法 | 方法描述 |
---|---|---|
between | 是 | 创建两个时间点之间的 interval |
from | 是 | 由一个临时时间点创建 interval |
of | 是 | 由它的组成部分创建 interval 的实例 |
parse | 是 | 由字符串创建 interval 的实例 |
addTo | 否 | 创建该 interval 的副本,并将其叠加到某个指定的 temporal 对象 |
get | 否 | 读取该 interval 的状态 |
isNegative | 否 | 检查该 interval 是否为负值,不包含零 |
isZero | 否 | 检查该 interval 的时长是否为零 |
minus | 否 | 通过减去一定的时间创建该 interval 的副本 |
multipliedBy | 否 | 将 interval 的值乘以某个标量创建该 interval 的副本 |
negated | 否 | 以忽略某个时长的方式创建该 interval 的副本 |
plus | 否 | 以增加某个指定的时长的方式创建该 interval 的副本 |
subtractFrom | 否 | 从指定的 temporal 对象中减去该 interval |
截至目前,我们介绍的这些日期、时间对象都是不可修改的,这是为了更好地支持函数式编程,确保线程安全,保持领域模式一致性而做出的重大设计决定。当然,新的日期和时间API也提供了一些便利的方法来创建这些对象的可变版本。比如,你可能希望在已有的LocalDate实例上增加3天。我们在下一节中会针对这一主题进行介绍。除此之外,我们还会介绍如何依据指定的模式,比如dd/MM/yyyy,创建日期、时间格式器,以及如何使用这种格式器解析和输出日期。
如果你已经有一个LocalDate对象,想要创建它的一个修改版,最直接也最简单的方法是使用withAttribute方法。withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。注意,下面的这段代码中所有的方法都返回一个修改了属性的对象。它们都不会修改原来的对象!
以比较直观的方式操纵LocalDate的属性
LocalDate localDate = LocalDate.of(2020, 3, 18);
LocalDate localDate1 = localDate.withYear(2019);
LocalDate localDate2 = localDate1.withDayOfMonth(25);
LocalDate localDate3 = localDate1.with(ChronoField.MONTH_OF_YEAR, 9);
以相对方式修改LocalDate对象的属性
LocalDate date1 = LocalDate.of(2020, 3, 18);
LocalDate date2 = date1.plusWeeks(1);
LocalDate date3 = date2.minusWeeks(3);
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);
大概你已经猜到,像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 对象为模板,对某些状态进行修改创建该对象的副本 |
截至目前,你所看到的所有日期操作都是相对比较直接的。有的时候,你需要进行一些更加复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可以使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象,更加灵活地处理日期。对于最常见的用例,日期和时间API已经提供了大量预定义的TemporalAdjuster。你可以通过TemporalAdjuster类的静态工厂方法访问它们,如下所示。
使用预定义的TemporalAdjuster
LocalDate localDate = LocalDate.of(2020, 3, 18);
LocalDate localDate1 = localDate.with(nextOrSame(DayOfWeek.SUNDAY));//下一个周六
LocalDate localDate2 = localDate1.with(lastDayOfMonth());//本月最后一天
下表提供了TemporalAdjuster中包含的工厂方法列表。
方法名 | 描 述 |
---|---|
dayOfWeekInMonth | 创建一个新的日期,它的值为同一个月中每一周的第几天 |
firstDayOfMonth | 创建一个新的日期,它的值为当月的第一天 |
firstDayOfNextMonth | 创建一个新的日期,它的值为下月的第一天 |
firstDayOfNextYear | 创建一个新的日期,它的值为明年的第一天 |
firstDayOfYear | 创建一个新的日期,它的值为当年的第一天 |
firstInMonth | 创建一个新的日期,它的值为同一个月中,第一个符合星期几要求的值 |
lastDayOfMonth | 创建一个新的日期,它的值为当月的最后一天 |
lastDayOfNextMonth | 创建一个新的日期,它的值为下月的最后一天 |
lastDayOfNextYear | 创建一个新的日期,它的值为明年的最后一天 |
lastDayOfYear | 创建一个新的日期,它的值为今年的最后一天 |
lastInMonth | 创建一个新的日期,它的值为同一个月中,最后一个符合星期几要求的值 |
next/previous | 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期 |
nextOrSame/previousOrSame | 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期,如果该日期已经符合要求,直接返回该对象 |
TemporalAdjuster接口
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
这意味着TemporalAdjuster接口的实现需要定义如何将一个Temporal对象转换为另一个Temporal对象。你可以把它看成一个UnaryOperator
实现一个定制的TemporalAdjuster
请设计一个NextWorkingDay类,该类实现了TemporalAdjuster接口,能够计算明天的日期,同时过滤掉周六和周日这些节假日。格式如下所示:
date = date.with(new NextWorkingDay());
//如果当天的星期介于周一至周五之间,日期向后移动一天;如果当天是周六或者周日,则返回下一个周一。
public class NextWorkingDay implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
DayOfWeek dayOfWeek = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int addDay = 1;
if (dayOfWeek == DayOfWeek.FRIDAY) {
addDay = 3;
} else if (dayOfWeek == DayOfWeek.SATURDAY) {
addDay = 2;
}
return temporal.plus(addDay, ChronoUnit.DAYS);
}
}
处理日期和时间对象时,格式化以及解析日期时间对象是另一个非常重要的功能。新的java.time.format包就是特别为这个目的而设计的。这个包中,最重要的类是DateTimeFormatter。创建格式器最简单的方法是通过它的静态工厂方法以及常量。像BASIC_ISO_DATE和 ISO_LOCAL_DATE 这样的常量是 DateTimeFormatter 类的预定义实例。所有的DateTimeFormatter实例都能用于以一定的格式创建代表特定日期或时间的字符串。比如,下面的这个例子中,我们使用了两个不同的格式器生成了字符串:
LocalDate date = LocalDate.of(2020, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);//20200318
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);//2020-03-18
LocalDate date1 = LocalDate.parse("20200318", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2020-03-18", DateTimeFormatter.ISO_LOCAL_DATE);
和老的java.util.DateFormat相比较,所有的DateTimeFormatter实例都是线程安全的。所以,你能够以单例模式创建格式器实例,就像DateTimeFormatter所定义的那些常量,并能在多个线程间共享这些实例。
按照某个模式创建DateTimeFormatter
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate localDate = LocalDate.of(2020, 03, 18);
String format = localDate.format(formatter);
LocalDate date2 = LocalDate.parse(format, formatter);
创建一个本地化的DateTimeFormatter
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
LocalDate date1 = LocalDate.of(2020, 3, 18);
String formattedDate = date.format(italianFormatter); // 18. marzo 2020
LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);
最后,如果你还需要更加细粒度的控制,DateTimeFormatterBuilder类还提供了更复杂的格式器,你可以选择恰当的方法,一步一步地构造自己的格式器。另外,它还提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精 确 地 匹 配 指 定 的 模 式 )、 填 充 , 以 及 在 格 式 器 中 指 定 可 选 节 。 比 如 , 你 可 以 通 过DateTimeFormatterBuilder自己编程实现(d. MMMM yyyy)上面代码使用的italianFormatter,代码清单如下。
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(". ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.YEAR)
.parseCaseInsensitive()
.toFormatter(Locale.ITALIAN);
之前你看到的日期和时间的种类都不包含时区信息。时区的处理是新版日期和时间API新增加的重要功能,使用新版日期和时间API时区的处理被极大地简化了。新的java.time.ZoneId类是老版java.util.TimeZone的替代品。它的设计目标就是要让你无需为时区处理的复杂和繁琐而操心,比如处理日光时(Daylight Saving Time,DST)这种问题。跟其他日期和时间类一样,ZoneId类也是无法修改的。
你可以通过Java 8的新方法toZoneId将一个老的时区对象转换为ZoneId:
ZoneId zoneId = TimeZone.getDefault().toZoneId();
一旦得到一个ZoneId对象,你就可以将它与LocalDate、LocalDateTime或者是Instant对象整合起来,构造为一个ZonedDateTime实例,它代表了相对于指定时区的时间点,代码清单如下所示。
//为时间点添加时区信息
ZoneId romeZone = ZoneId.of("Europe/Rome");
LocalDate date = LocalDate.of(2020, Month.MARCH, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2020, Month.MARCH, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant now = Instant.now();
ZonedDateTime zdt3 = now.atZone(romeZone);
通过ZoneId,你还可以将LocalDateTime转换为Instant:
LocalDateTime dateTime = LocalDateTime.of(2020, Month.MARCH, 18, 13, 45);
Instant instantFromDateTime = dateTime.toInstant(romeZone);
你也可以通过反向的方式得到LocalDateTime对象:
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);
另一种比较通用的表达时区的方式是利用当前时区和UTC/格林尼治的固定偏差。比如,基于这个理论,你可以说“纽约落后于伦敦5小时”。这种情况下,你可以使用ZoneOffset类,它是ZoneId的一个子类,表示的是当前时间和伦敦格林尼治子午线时间的差异:
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
“-05:00”的偏差实际上对应的是美国东部标准时间。注意,使用这种方式定义的ZoneOffset并未考虑任何日光时的影响,所以在大多数情况下,不推荐使用。由于ZoneOffset也是ZoneId,所以你可以像代码清单12-13那样使用它。你甚至还可以创建这样的OffsetDateTime,它使用ISO-8601的历法系统,以相对于UTC/格林尼治时间的偏差方式表示日期时间。
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
LocalDateTime dateTime = LocalDateTime.of(2020, Month.MARCH, 18, 13, 45);
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(dateTime, newYorkOffset);
ISO-8601日历系统是世界文明日历系统的事实标准。但是,Java 8中另外还提供了4种其他的日历系统。这些日历系统中的每一个都有一个对应的日志类,分别是ThaiBuddhistDate、MinguoDate 、 JapaneseDate 以 及 HijrahDate 。所有这些类以及 LocalDate 都实现了ChronoLocalDate接口,能够对公历的日期进行建模。利用LocalDate对象,你可以创建这些类的实例。更通用地说,使用它们提供的静态工厂方法,你可以创建任何一个Temporal对象的实例,如下所示:
LocalDate localDate = LocalDate.of(2020, Month.MARCH, 18);
JapaneseDate japaneseDate = JapaneseDate.from(localDate);
或者,你还可以为某个Locale显式地创建日历系统,接着创建该Locale对应的日期的实例。新的日期和时间API中,Chronology接口建模了一个日历系统,使用它的静态工厂方法ofLocale,可以得到它的一个实例,代码如下:
LocalDate localDate = LocalDate.of(2020, Month.MARCH, 18);
JapaneseDate japaneseDate = JapaneseDate.from(localDate);
或者,你还可以为某个Locale显式地创建日历系统,接着创建该Locale对应的日期的实例。新的日期和时间API中,Chronology接口建模了一个日历系统,使用它的静态工厂方法ofLocale,可以得到它的一个实例,代码如下:
Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);
ChronoLocalDate now = japaneseChronology.dateNow();
伊斯兰教日历
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())));
本节中需要掌握: