12.2 操纵、解析和格式化日期
如果你已经有一个LocalDate对象,想要创建它的一个修改版,最直接也最简单的方法是使用withAttribute方法。withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。注意,下面的这段代码中所有的方法都返回一个修改了属性的对象。它们都不会修改原来的对象!
代码清单 12-6 以比较直观的方式操纵
LocalDate的属性
LocalDate date1 = LocalDate.of(2017, 9, 21); ←---- 2017-09-21LocalDate date2 = date1.withYear(2011); ←---- 2011-09-21LocalDate date3 = date2.withDayOfMonth(25); ←---- 2011-09-21LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 2); ←---- 2011-02-25
采用更通用的with方法能达到同样的目的,它接受的第一个参数是一个TemporalField对象,格式类似代码清单12-6的最后一行。最后这一行中使用的with方法和代码清单12-2中的get方法有些类似。它们都声明于Temporal接口,所有的日期和时间API类都实现这两个方法,它们定义了单点的时间,比如LocalDate、LocalTime、LocalDateTime以及Instant。更确切地说,使用get和with方法,可以将Temporal对象值的读取和修改1区分开。如果Temporal对象不支持请求访问的字段,它就会抛出一个UnsupportedTemporalTypeException异常,比如试图访问Instant对象的ChronoField.MONTH_OF_YEAR字段,或者LocalDate对象的ChronoField.NANO_OF_SECOND字段时都会抛出这样的异常。
1请注意,使用with方法并不会直接修改现有的Temporal对象,它会创建现有对象的副本并更新对应的字段。这一过程也被称作函数式更新(更多内容请参见第19章)。
它甚至能以声明的方式操纵LocalDate对象。比如,你可以像下面这段代码那样加上或者减去一段时间。
代码清单 12-7 以相对方式修改
LocalDate对象的属性
LocalDate date1 = LocalDate.of(2017, 9, 21); ←---- 2017-09-21LocalDate date2 = date1.plusWeeks(1); ←---- 2017-09-28LocalDate date3 = date2.minusYears(6); ←---- 2011-09-28LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); ←---- 2012-03-28
与刚才介绍的get和with方法类似,代码清单12-7中最后一行使用的plus方法也是通用方法,它和minus方法都声明于Temporal接口中。通过这些方法,对TemporalUnit对象加上或者减去一个数字,我们能非常方便地将Temporal对象前溯或者回滚至某个时间段,通过ChronoUnit枚举可以非常方便地实现TemporalUnit接口。
大概你已经猜到,像LocalDate、LocalTime、LocalDateTime以及Instant这样表示时间点的日期–时间类提供了大量通用的方法,表12-2对这些通用的方法进行了总结。
表 12-2 表示时间点的日期–时间类的通用方法
| 方法名 | 是否是静态方法 | 描述 |
|---|---|---|
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对象为模板,对某些状态进行修改创建该对象的副本
|
你可以尝试用测验12.1检查一下到目前为止你都掌握了哪些操纵日期的技能。
测验12.1:操纵
LocalDate对象经过下面这些操作,
date变量的值是什么?
LocalDate date = LocalDate.of(2014, 3, 18);date = date.with(ChronoField.MONTH_OF_YEAR, 9);date = date.plusYears(2).minusDays(10);date.withYear(2011);答案:
2016-09-08。正如刚才看到的,你可以通过绝对方式和相对方式操纵日期。你甚至还可以在一个语句中连接多个操作,因为每个动作都会创建一个新的
LocalDate对象,后续的方法调用可以操纵前一方法创建的对象。这段代码的最后一行不会产生任何能看到的效果,因为它像前面的那些操作一样,会创建一个新的LocalDate实例,不过我们并没有将这个新创建的值赋给任何的变量。
12.2.1 使用TemporalAdjuster
截至目前,你所看到的所有日期操作都是相对比较直接的。有的时候,你需要进行一些更加复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可以使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象,更加灵活地处理日期。对于最常见的用例,日期和时间API已经提供了大量预定义的TemporalAdjuster。你可以通过TemporalAdjusters类的静态工厂方法访问它们,如下所示。
代码清单 12-8 使用预定义的
TemporalAdjuster
import static java.time.temporal.TemporalAdjusters.*;LocalDate date1 = LocalDate.of(2014, 3, 18); ←---- 2014-03-18LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); ←---- 2014-03-23LocalDate date3 = date2.with(lastDayOfMonth()); ←---- 2014-03-31
表12-3提供了TemporalAdjusters中包含的工厂方法列表。
表 12-3 TemporalAdjusters类中的工厂方法
| 方法名 | 描述 |
|---|---|
dayOfWeekInMonth
| 创建一个新的日期,它的值为同一个月中每一周的第几天(负数表示从月末往月初计数) |
firstDayOfMonth
| 创建一个新的日期,它的值为当月的第一天 |
firstDayOfNextMonth
| 创建一个新的日期,它的值为下月的第一天 |
firstDayOfNextYear
| 创建一个新的日期,它的值为明年的第一天 |
firstDayOfYear
| 创建一个新的日期,它的值为当年的第一天 |
firstInMonth
| 创建一个新的日期,它的值为同一个月中,第一个符合星期几要求的值 |
lastDayOfMonth
| 创建一个新的日期,它的值为当月的最后一天 |
lastDayOfNextMonth
| 创建一个新的日期,它的值为下月的最后一天 |
lastDayOfNextYear
| 创建一个新的日期,它的值为明年的最后一天 |
lastDayOfYear
| 创建一个新的日期,它的值为当年的最后一天 |
lastInMonth
| 创建一个新的日期,它的值为同一个月中,最后一个符合星期几要求的值 |
next/previous
| 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期 |
nextOrSame/previousOrSame
| 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期,如果该日期已经符合要求,则直接返回该对象 |
正如我们看到的,使用TemporalAdjuster可以进行更加复杂的日期操作,而且这些方法的名称也非常直观,方法名基本就是问题陈述。此外,即使你没有找到符合要求的预定义的TemporalAdjuster,创建你自己的TemporalAdjuster也并非难事。实际上,TemporalAdjuster接口只声明了单一的一个方法(这使得它成为了一个函数式接口),定义如下。
代码清单 12-9
TemporalAdjuster接口
@FunctionalInterfacepublic interface TemporalAdjuster {Temporal adjustInto(Temporal temporal);}
这意味着TemporalAdjuster接口的实现需要定义如何将一个Temporal对象转换为另一个Temporal对象。你可以把它看成一个UnaryOperator。花几分钟时间完成测验12.2,练习一下到目前为止所学习的东西,请实现你自己的TemporalAdjuster。
测验12.2:实现一个定制的
TemporalAdjuster请设计一个
NextWorkingDay类,该类实现了TemporalAdjuster接口,能够计算明天的日期,同时过滤掉周六和周日这些节假日。格式如下所示:
date = date.with(new NextWorkingDay());如果当天的星期数介于周一至周五之间,就将日期向后移动一天;如果当天是周六或者周日,则返回下一个周一。
答案:下面是参考的
NextWorkingDay类的实现。
public class NextWorkingDay implements TemporalAdjuster {@Overridepublic Temporal adjustInto(Temporal temporal) {DayOfWeek dow =DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK)); ←—— 读取当前日期int dayToAdd = 1; ←—— 正常情况,增加一天if (dow == DayOfWeek.FRIDAY) dayToAdd = 3; ←—— 如果当天是周五,增加三天else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2; ←—— 如果当天是周六,增加两天return temporal.plus(dayToAdd, ChronoUnit.DAYS); ←—— 增加恰当的天数后,返回修改的日期}}该
TemporalAdjuster通常情况下将日期往后顺延一天,如果当天是周五或者周六,则依据情况分别将日期顺延三天或者两天。注意,由于TemporalAdjuster是一个函数式接口,因此你只能以Lambda表达式的方式向该adjuster接口传递行为:
date = date.with(temporal -> {DayOfWeek dow =DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));int dayToAdd = 1;if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;return temporal.plus(dayToAdd, ChronoUnit.DAYS);});你大概会希望在你代码的多个地方使用同样的方式去操作日期,为了达到这一目的,建议你像示例那样将它的逻辑封装到一个类中。对于经常使用的操作,都应该采用类似的方式进行封装。最终,你会创建自己的类库,让你和你的团队能轻松地实现代码复用。
如果你想要使用Lambda表达式定义
TemporalAdjuster对象,那么推荐使用TemporalAdjuster类的静态工厂方法ofDateAdjuster,它接受一个UnaryOperator类型的参数,代码如下:
TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(temporal -> {DayOfWeek dow =DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));int dayToAdd = 1;if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;return temporal.plus(dayToAdd, ChronoUnit.DAYS);});date = date.with(nextWorkingDay);
你可能希望对你的日期和时间对象进行的另外一个通用操作是,依据你的业务领域以不同的格式打印输出这些日期和时间对象。类似地,你可能也需要将那些格式的字符串转换为实际的日期对象。接下来的一节会演示新的日期和时间API提供的那些机制是如何完成这些任务的。
12.2.2 打印输出及解析日期–时间对象
处理日期和时间对象时,格式化以及解析日期–时间对象是另一个非常重要的功能。新的java.time.format包就是特别为这个目的而设计的。这个包中,最重要的类是DateTimeFormatter。创建格式器最简单的方法是通过它的静态工厂方法以及常量。像BASIC_ISO_DATE和ISO_LOCAL_DATE这样的常量是DateTimeFormatter类的预定义实例。所有的DateTimeFormatter实例都能用于以一定的格式创建代表特定日期或时间的字符串。比如,下面这个例子使用两个不同的格式器生成了字符串:
LocalDate date = LocalDate.of(2014, 3, 18);String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); ←---- 20140318String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); ←---- 2014-03-18
你也可以通过解析代表日期或时间的字符串重新创建该日期对象。所有的日期和时间API都提供了表示时间点或者时间段的工厂方法,你可以使用工厂方法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类还支持一个静态工厂方法,它可以按照某个特定的模式创建格式器,代码清单如下。
代码清单 12-10 按照某个模式创建
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);
这段代码中,LocalDate的formate方法使用指定的模式生成了一个代表该日期的字符串。紧接着,静态的parse方法使用同样的格式器解析了刚才生成的字符串,并重建了该日期对象。ofPattern方法也提供了一个重载的版本,使用它你可以创建某个Locale的格式器,代码清单如下。
代码清单 12-11 创建一个本地化的
DateTimeFormatter
DateTimeFormatter italianFormatter =DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);LocalDate date1 = LocalDate.of(2014, 3, 18);String formattedDate = date.format(italianFormatter); // 18. marzo 2014LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);
最后,如果你还需要更加细粒度的控制,DateTimeFormatterBuilder类还提供了更复杂的格式器,你可以选择恰当的方法,一步一步地构造自己的格式器。另外,它还提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精确地匹配指定的模式)、填充,以及在格式器中指定可选节。比如,你可以通过DateTimeFormatterBuilder自己编程实现代码清单12-11中使用的italianFormatter,代码清单如下。
代码清单 12-12 构造一个
DateTimeFormatter
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder().appendText(ChronoField.DAY_OF_MONTH).appendLiteral(". ").appendText(ChronoField.MONTH_OF_YEAR).appendLiteral(" ").appendText(ChronoField.YEAR).parseCaseInsensitive().toFormatter(Locale.ITALIAN);
目前为止,你已经学习了如何创建、操纵、格式化以及解析时间点和时间段,但是还不了解如何处理日期和时间之间的微妙关系。比如,你可能需要处理不同的时区,或者由于不同的历法系统带来的差异。接下来的一节会探究如何使用新的日期和时间API解决这些问题。
