操纵、解析和格式化日期
如果你已经有一个LocalDate对象,想要创建它的一个修改版,最直接也最简单的方法是使用withAttribute方法。withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。注意,下面的这段代码中所有的方法都返回一个修改了属性的对象。它们都不会修改原来的对象!
// 2018-11-17 LocalDate date1 = LocalDate.of(2018, 11, 17); // 2019-11-17 LocalDate date2 = date1.withYear(2019); // 2019-11-25 LocalDate date3 = date2.withDayOfMonth(25); // 2019-09-25 LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9); 复制代码
它们都声明于Temporal接口,所有的日期和时间API类都实现这两个方法,它们定义了单点的时间,比如LocalDate、LocalTime、LocalDateTime以及Instant。更确切地说,使用get和with方法,我们可以将Temporal对象值的读取和修改区分开。如果Temporal对象不支持请求访问的字段,它会抛出一个UnsupportedTemporalTypeException异常,比如试图访问Instant对象的ChronoField.MONTH_OF_YEAR字段,或者LocalDate对象的ChronoField.NANO_OF_SECOND字段时都会抛出这样的异常。
它甚至能以声明的方式操纵LocalDate对象。比如,你可以像下面这段代码那样加上或者减去一段时间。
// 2018-11-17 LocalDate date1 = LocalDate.of(2018, 11, 17); // 2018-11-24 LocalDate date2 = date1.plusWeeks(1); // 2015-11-24 LocalDate date3 = date2.minusYears(3); // 2016-05-24 LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); 复制代码
与我们刚才介绍的get和with方法类似最后一行使用的plus方法也是通用方法,它和minus方法都声明于Temporal接口中。通过这些方法,对TemporalUnit对象加上或者减去一个数字,我们能非常方便地将Temporal对象前溯或者回滚至某个时间段,通过ChronoUnit枚举我们可以非常方便地实现TemporalUnit接口。
大概你已经猜到,像LocalDate、LocalTime、LocalDateTime以及Instant这样表示时 间点的日期时间类提供了大量通用的方法,我们目前所使用的只有一小部分,有兴趣的可以去看官网文档。
使用TemporalAdjuster
截至目前,你所看到的所有日期操作都是相对比较直接的。有的时候,你需要进行一些更加复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可以使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象,更加灵活地处理日期。对于最常见的用例, 日期和时间API已经提供了大量预定义的TemporalAdjuster。你可以通过TemporalAdjuster类的静态工厂方法访问它们,如下所示。
// 2018-11-17 LocalDate date1 = LocalDate.of(2018, 11, 17); // 2018-11-19 LocalDate date2 = date1.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY)); // 2018-11-30 LocalDate date3 = date2.with(TemporalAdjusters.lastDayOfMonth()); 复制代码
正如我们看到的,使用TemporalAdjuster我们可以进行更加复杂的日期操作,而且这些方法的名称也非常直观,方法名基本就是问题陈述。此外,即使你没有找到符合你要求的预定义的TemporalAdjuster,创建你自己的TemporalAdjuster也并非难事。实际上,TemporalAdjuster接口只声明了单一的一个方法(这使得它成为了一个函数式接口),定义如下。
@FunctionalInterface public interface TemporalAdjuster { Temporal adjustInto(Temporal temporal); } 复制代码
这意味着TemporalAdjuster接口的实现需要定义如何将一个Temporal对象转换为另一个Temporal对象。你可以把它看成一个UnaryOperator。
你可能希望对你的日期时间对象进行的另外一个通用操作是,依据你的业务领域以不同的格式打印输出这些日期和时间对象。类似地,你可能也需要将那些格式的字符串转换为实际的日期对象。接下来的一节,我们会演示新的日期和时间API提供那些机制是如何完成这些任务的。
打印输出及解析日期-时间对象
处理日期和时间对象时,格式化以及解析日期时间对象是另一个非常重要的功能。新的java.time.format包就是特别为这个目的而设计的。这个包中,最重要的类是DateTimeFormatter。创建格式器最简单的方法是通过它的静态工厂方法以及常量。像BASIC_ISO_DATE和ISO_LOCAL_DATE 这样的常量是DateTimeFormatter 类的预定义实例。所有的DateTimeFormatter实例都能用于以一定的格式创建代表特定日期或时间的字符串。比如,下面的这个例子中,我们使用了两个不同的格式器生成了字符串:
LocalDate date1 = LocalDate.of(2018, 11, 17); // 20181117 String s1 = date1.format(DateTimeFormatter.BASIC_ISO_DATE); // 2018-11-17 String s2 = date1.format(DateTimeFormatter.ISO_LOCAL_DATE); 复制代码
你也可以通过解析代表日期或时间的字符串重新创建该日期对象。所有的日期和时间API都提供了表示时间点或者时间段的工厂方法,你可以使用工厂方法parse达到重创该日期对象的目的:
LocalDate date2 = LocalDate.parse("20181117", DateTimeFormatter.BASIC_ISO_DATE); LocalDate date3 = LocalDate.parse("2018-11-17", DateTimeFormatter.ISO_LOCAL_DATE); 复制代码
和老的java.util.DateFormat相比较,所有的DateTimeFormatter实例都是线程安全的。所以,你能够以单例模式创建格式器实例,就像DateTimeFormatter所定义的那些常量,并能在多个线程间共享这些实例。DateTimeFormatter类还支持一个静态工厂方法,它可以按照某个特定的模式创建格式器,代码清单如下。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); // 17/11/2018 String formattedDate = date1.format(formatter); LocalDate date4 = LocalDate.parse(formattedDate, formatter); 复制代码
这段代码中,LocalDate的formate方法使用指定的模式生成了一个代表该日期的字符串。紧接着,静态的parse方法使用同样的格式器解析了刚才生成的字符串,并重建了该日期对象。ofPattern方法也提供了一个重载的版本,使用它你可以创建某个Locale的格式器,代码清单如下所示。
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN); LocalDate date5 = LocalDate.of(2018, 11, 16); // 16. novembre 2018 String formattedDate2 = date5.format(italianFormatter); // 2018-11-16 LocalDate date6 = LocalDate.parse(formattedDate2, 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); LocalDate now = LocalDate.now(); // 17. novembre 2018 String s1 = now.format(italianFormatter); 复制代码
目前为止,你已经学习了如何创建、操纵、格式化以及解析时间点和时间段,但是你还不了解如何处理日期和时间之间的微妙关系。比如,你可能需要处理不同的时区,或者由于不同的历法系统带来的差异。接下来的一节,我们会探究如何使用新的日期和时间API解决这些问题。
处理不同的时区和历法
之前你看到的日期和时间的种类都不包含时区信息。时区的处理是新版日期和时间API新增加的重要功能,使用新版日期和时间API时区的处理被极大地简化了。新的java.time.ZoneId类是老版java.util.TimeZone的替代品。它的设计目标就是要让你无需为时区处理的复杂和繁琐而操心,比如处理日光时(Daylight Saving Time,DST)这种问题。跟其他日期和时间类一样,ZoneId类也是无法修改的。
时区是按照一定的规则将区域划分成的标准时间相同的区间。在ZoneRules这个类中包含了40个这样的实例。你可以简单地通过调用ZoneId的getRules()得到指定时区的规则。每个特定的ZoneId对象都由一个地区ID标识,比如:
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai"); 复制代码
地区ID都为“{区域}/{城市}”的格式,这些地区集合的设定都由英特网编号分配机构(IANA)的时区数据库提供。你可以通过Java 8的新方法toZoneId将一个老的时区对象转换为ZoneId:
ZoneId zoneId = TimeZone.getDefault().toZoneId(); 复制代码
一旦得到一个ZoneId对象,你就可以将它与LocalDate、LocalDateTime或者是Instant对象整合起来,构造为一个ZonedDateTime实例,它代表了相对于指定时区的时间点,代码清单如下所示。
LocalDate date = LocalDate.of(2018, 11, 17); ZonedDateTime zdt1 = date.atStartOfDay(shanghaiZone); LocalDateTime dateTime = LocalDateTime.of(2018, 11, 27, 18, 13, 15); ZonedDateTime zdt2 = dateTime.atZone(shanghaiZone); Instant instant = Instant.now(); ZonedDateTime zdt3 = instant.atZone(shanghaiZone); 复制代码
通过ZoneId,你还可以将LocalDateTime转换为Instant:
LocalDateTime dateTime = LocalDateTime.of(2018, 11, 17, 18, 45); Instant instantFromDateTime = dateTime.toInstant(shanghaiZone); 复制代码
你也可以通过反向的方式得到LocalDateTime对象:
Instant instant = Instant.now(); LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, shanghaiZone); 复制代码
利用和UTC/格林尼治时间的固定偏差计算时区
另一种比较通用的表达时区的方式是利用当前时区和UTC/格林尼治的固定偏差。比如,基于这个理论,你可以说“纽约落后于伦敦5小时”。这种情况下,你可以使用ZoneOffset类,它是ZoneId的一个子类,表示的是当前时间和伦敦格林尼治子午线时间的差异:
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00"); 复制代码
“-05:00”的偏差实际上对应的是美国东部标准时间。注意,使用这种方式定义的ZoneOffset并未考虑任何日光时的影响,所以在大多数情况下,不推荐使用。由于ZoneOffset也是ZoneId,所以你可以像上面的代码那样使用它。你甚至还可以创建这样的OffsetDateTime,它使用ISO-8601的历法系统,以相对于UTC/格林尼治时间的偏差方式表示日期时间。
LocalDateTime dateTime = LocalDateTime.of(2018, 11, 17, 18, 45); OffsetDateTime offsetDateTime = OffsetDateTime.of(dateTime, newYorkOffset); 复制代码
总结
- Java 8之前老版的java.util.Date类以及其他用于建模日期时间的类有很多不一致及设计上的缺陷,包括易变性以及糟糕的偏移值、默认值和命名。
- 新版的日期和时间API中,日期-时间对象是不可变的。
- 新的API提供了两种不同的时间表示方式,有效地区分了运行时人和机器的不同需求。
- 你可以用绝对或者相对的方式操纵日期和时间,操作的结果总是返回一个新的实例,老的日期时间对象不会发生变化。
- TemporalAdjuster让你能够用更精细的方式操纵日期,不再局限于一次只能改变它的一个值,并且你还可按照需求定义自己的日期转换器。
- 你现在可以按照特定的格式需求,定义自己的格式器,打印输出或者解析日期时间对象。这些格式器可以通过模板创建,也可以自己编程创建,并且它们都是线程安全的。
- 你可以用相对于某个地区/位置的方式,或者以与UTC/格林尼治时间的绝对偏差的方式表示时区,并将其应用到日期时间对象上,对其进行本地化。
可以说《Java8实战》的读书笔记相关的已经写完了,这本书后面还有最后一部分超越Java8,这一部分相关的章节都是跟函数式编程的思考与技巧相关,以及Java以后的未来等等。《Java8实战》这本书真的写的太好了而且这本书完全可以当作一本关于Java8使用的工具书,随时可以翻开看看,看看关于Java8的特性是如何使用,该如何去避免一些坑,该如何使用Stream和Lambda表达式去简化你的代码。
代码
Gitee:chap12
Github:chap12