Java8学习笔记之新日期和时间API

1、LocalDate、LocalTime、Instant、Duration、Period
1)使用LocalDate和LocalTime

LocalDate类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关的信息。
可以通过静态工厂方法of创建一个LocalDate实例。LocalDate实例提供了多种方法来读取常用的值,比如年份、月份、星期几等,如下所示。
创建一个LocalDate对象并读取其值:

LocalDate date = LocalDate.of(2019, 8, 18);//2019-08-18
int year = date.getYear();//2019
Month month = date.getMonth();//AUGUST
int day = date.getDayOfMonth();//18
DayOfWeek dow = date.getDayOfWeek();//SUNDAY
int len = date.lengthOfMonth();//31
boolean leap = date.isLeapYear();//false
LocalDate today = LocalDate.now();//2019-08-13

还可以通过传递一个TemporalField参数给get方法拿到同样的信息。TemporalField是一个接口,它定义了如何访问temporal对象某个字段的值。ChronoField枚举实现了这一接口,所以你可以很方便地使用get方法得到枚举元素的值,如下所示。
使用TemporalField读取LocalDate的值:

int year = date.get(ChronoField.YEAR);//2019
int month = date.get(ChronoField.MONTH_OF_YEAR);//08
int day = date.get(ChronoField.DAY_OF_MONTH);//18

一天中的时间,比如10:05:10,可以使用LocalTime类表示。可以使用of重载的两个工厂方法创建LocalTime的实例。第一个重载函数接收小时和分钟,第二个重载函数同时还 接收秒。同LocalDate一样,LocalTime类也提供了一些getter方法访问这些变量的值,如下所示。
创建LocalTime并读取其值:

LocalTime time = LocalTime.of(10, 5, 10);//10:05:10
int hour = time.getHour();//10
int minute = time.getMinute();//5
int second = time.getSecond();//10

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

LocalDate date = LocalDate.parse("2019-08-18");//2019-08-18
LocalTime time = LocalTime.parse("10:05:10");//10:05:10

可以向parse方法传递一个DateTimeFormatter。该类的实例定义了如何格式化一个日期或者时间对象,它是java.util.DateFormat的推荐替代品。
注意:一旦传递的字符串参数无法被解析为合法的LocalDate或LocalTime对象,这两个parse方法都会抛出一个继承自RuntimeException的DateTimeParseException异常。

2)合并日期和时间

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

LocalDate date = LocalDate.parse("2019-08-18");//2019-08-18
LocalTime time = LocalTime.parse("10:05:10");//10:05:10
LocalDateTime dt1 = LocalDateTime.of(2019, Month.AUGUST, 18, 10, 05, 10);//2019-08-18T10:05:10
LocalDateTime dt2 = LocalDateTime.of(date, time);//2019-08-18T10:05:10
LocalDateTime dt3 = date.atTime(10, 05, 10);//2019-08-18T10:05:10
LocalDateTime dt4 = date.atTime(time);//2019-08-18T10:05:10
LocalDateTime dt5 = time.atDate(date);//2019-08-18T10:05:10

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

LocalDate date1 = dt1.toLocalDate();//2019-08-18
LocalTime time1 = dt1.toLocalTime();//10:05:10
3)机器的日期和时间格式

我们习惯于以星期几、几号、几点、几分这样的方式理解日期和时间。但这种方式对于计算机而言并不容易理解。从计算机的角度来看,建模时间自然的格式是表示一个持续时间段上某个点的单一大整型数。这也是新的java.time.Instant类对时间建模的方式,基本上它是以Unix元年时间(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的秒数进行计算。
可以通过向静态工厂方法ofEpochSecond传递一个代表秒数的值,创建一个该类的实例。静态工厂方法ofEpochSecond还有一个增强的重载版本,它接收第二个以纳秒为单位的参数值,对传入作为秒数的参数进行调整。重载的版本会调整纳秒参数,确保保存的纳秒分片在0到999 999 999之间。

Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000);//2秒之后再加上100万纳秒(1秒) 
Instant.ofEpochSecond(4, -1_000_000_000);//4秒之前的100万纳秒(1秒)

Instant类也支持静态工厂方法now,它能获取当前时刻的时间戳。Instant的设计初衷是为了便于机器使用,它包含的是由秒及纳秒所构成的数字。所以,它无法处理那些我们非常容易理解的时间单位。

int day = Instant.now().get(ChronoField.DAY_OF_MONTH);

它会抛出下面这样的异常:

java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth
4)定义Duration或Period

Temporal接口定义了如何读取和操纵为时间建模的对象的值。可以创建两个LocalTimes对象、两个LocalDateTimes对象,或者两个Instant对象之间的duration,如下所示:

Duration d1 = Duration.between(time1, time2);
Duration d1 = Duration.between(dateTime1, dateTime2);
Duration d2 = Duration.between(instant1, instant2);

由于LocalDateTime和Instant是为不同的目的而设计的,一个是为了便于人阅读使用, 另一个是为了便于机器处理,所以不能将二者混用。如果试图在这两类对象之间创建duration,会触发一个DateTimeException异常。此外,由于Duration类主要用于以秒和纳秒衡量时间的长短,不能仅向between方法传递一个LocalDate对象做参数。
如果需要以年、月或者日的方式对多个时间单位建模,可以使用Period类。使用该类的工厂方法between,可以使用得到两个LocalDate之间的时长,如下所示:

Period tenDays1 = Period.between(LocalDate.of(2019, 8, 10), LocalDate.of(2019, 8, 18));//P8D
Period tenDays2 = Period.between(LocalDate.of(2019, 8, 10), LocalDate.of(2019, 8, 8));//P-2D

Duration和Period类都提供了很多非常方便的工厂类,直接创建对应的实例;就像下面这段代码那样,不再是只能以两个temporal对象的差值的方式来定义它们的对象。

Duration threeMinutes1 = Duration.ofMinutes(3);//PT3M
Duration threeMinutes2 = Duration.of(3, ChronoUnit.MINUTES);//PT3M
Period tenDays = Period.ofDays(10);//P10D
Period threeWeeks = Period.ofWeeks(3);//P21D
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);//P2Y6M1D
Java8学习笔记之新日期和时间API_第1张图片
日期-时间类中表示时间间隔的通用方法

日期-时间类中表示时间间隔的通用方法

以上这些日期-时间对象都是不可修改的,这是为了更好地支持函数式编程,确保线程安全,保持领域模式一致性而做出的重大设计决定。当然,新的日期和时间API也提供了一些便利的方法来创建这些对象的可变版本。

2、操纵、解析和格式化日期

如果已经有一个LocalDate对象,想要创建它的一个修改版,最直接也最简单的方法是使用withAttribute方法。withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。

LocalDate date1 = LocalDate.of(2019, 8, 18);//2019-08-18
LocalDate date2 = date1.withYear(2019);//2019-08-18
LocalDate date3 = date2.withDayOfMonth(25);//2019-08-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);//2019-09-25

采用更通用的with方法能达到同样的目的,它接受的第一个参数是一个TemporalField对象。 date.with()与date.get()有些类似,它们都声明于Temporal接口,所有的日期和时间API类都实现这两个方法,它们定义了单点的时间,比如LocalDate、LocalTime、LocalDateTime以及Instant。使用get和with方法,我们可以将Temporal对象值的读取和修改区分开。如果Temporal对象不支持请求访问的字段,它会抛出一个UnsupportedTemporalTypeException异常,比如试图访问Instant对象的ChronoField.MONTH_OF_YEAR字段,或LocalDate对象的ChronoField.NANO_OF_SECOND字段时都会抛出这样的异常。

LocalDate date1 = LocalDate.of(2019, 8, 18);//2019-08-18
LocalDate date2 = date1.plusWeeks(1);//2019-08-25
LocalDate date3 = date2.minusYears(3);//2016-08-25
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);//2017-02-25

与get和with方法类似,plus方法也是通用方法,它和minus方法都声明于Temporal接口中。通过这些方法,对TemporalUnit对象加上或者减去一个数字,我们能非常方便地将Temporal对象前溯或者回滚至某个时间段,通过ChronoUnit枚举我们可以非常方便地实现TemporalUnit接口。


Java8学习笔记之新日期和时间API_第2张图片
表示时间点的日期-时间类的通用方法
1)使用TemporalAdjuster

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

import static java.time.temporal.TemporalAdjusters.*;
LocalDate date1 = LocalDate.of(2019, 8, 18);//2019-08-18
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));//2019-08-18
LocalDate date3 = date2.with(lastDayOfMonth());//2019-08-31
Java8学习笔记之新日期和时间API_第3张图片
TemporalAdjuster类中的工厂方法

使用TemporalAdjuster我们可以进行更加复杂的日期操作,方法名基本就是问题陈述。即使没有找到符合你要求的预定义的TemporalAdjuster,创建自己的TemporalAdjuster也并非难事。实际上,TemporalAdjuster接口只声明了单一的一个方法(这使得它成为了一个函数式接口),定义如下。

@FunctionalInterface
public interface TemporalAdjuster {
    Temporal adjustInto(Temporal var1);
}

TemporalAdjuster接口的实现需要定义如何将一个Temporal对象转换为另一个Temporal对象,可以把它看成一个UnaryOperator
示例:设计一个NextWorkingDay类,该类实现了TemporalAdjuster接口,能够计算明天的日期,同时过滤掉周六和周日这些节假日。如果当天的星期介于周一至周五之间,日期向后移动一天;如果当天是周六或者周日,则返回下一个周一。

public class NextWorkingDay implements TemporalAdjuster {
  @Override
  public Temporal adjustInto(Temporal temporal) {
    DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));//读取当前日期 
    int dayToAdd = 1;//正常情况,增加1天 
    if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;//如果当天是周五,增加3天 
    else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;//如果当天是周六,增加2天
    return temporal.plus(dayToAdd, ChronoUnit.DAYS);//增加恰当的天数后,返回修改的日期
  }
}

该TemporalAdjuster通常情况下将日期往后顺延一天,如果当天是周六或者周日,则依据情况分别将日期顺延3天或者2天。由于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对象,推荐使用TemporalAdjusters类的静态工厂方法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;
    if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
    return temporal.plus(dayToAdd, ChronoUnit.DAYS);
});
date = date.with(nextWorkingDay);
2)打印输出及解析日期-时间对象

处理日期和时间对象时,格式化以及解析日期-时间对象是一个非常重要的功能。新 java.time.format包就是特别为这个目的而设计的。这个包中,重要的类是DateTimeFormatter。创建格式器简单的方法是通过它的静态工厂方法以及常量。像BASIC_ISO_DATE和ISO_LOCAL_DATE这样的常量是DateTimeFormatter类的预定义实例。所有的DateTimeFormatter实例都能用于以一定的格式创建代表特定日期或时间的字符串。

LocalDate date = LocalDate.of(2019, 8, 18);//2019-08-18
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);//20190818
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);//2019-08-18

也可以通过解析代表日期或时间的字符串重新创建该日期对象。所有的日期和时间API都提供了表示时间点或者时间段的工厂方法,可以使用工厂方法parse实现:

LocalDate date1 = LocalDate.parse("20190818", DateTimeFormatter.BASIC_ISO_DATE);//2019-08-18
LocalDate date2 = LocalDate.parse("2019-08-18", DateTimeFormatter.ISO_LOCAL_DATE);//2019-08-18

和java.util.DateFormat相比,所有的DateTimeFormatter实例都是线程安全的。你能够以单例模式创建格式器实例,就像DateTimeFormatter所定义的那些常量,并在多个线程间共享这些实例。DateTimeFormatter类还支持一个静态工厂方法,它可以按照某个特定的模式创建格式器。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2019, 8, 18);//2019-08-18
String formattedDate = date1.format(formatter);//18/08/2019
LocalDate date2 = LocalDate.parse(formattedDate, formatter);//2019-08-18

如果还需要更细粒度的控制,DateTimeFormatterBuilder类还提供了更复杂的格式器。另外,它还提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精确地匹配指定的模式)、填充,以及在格式器中指定可选节。
构造一个DateTimeFormatter:

DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
  .appendText(ChronoField.DAY_OF_MONTH)
  .appendLiteral(". ")
  .appendText(ChronoField.MONTH_OF_YEAR)
  .appendLiteral(" ")
  .appendText(ChronoField.YEAR)
  .parseCaseInsensitive()
  .toFormatter(Locale.ITALIAN);
3、处理不同的时区和历法

时区的处理是新版日期和时间API新增加的重要功能,使用新版日期和时间API时区的处理被极大地简化了。新的java.time.ZoneId类是老版java.util.TimeZone的替代品。它的设计目标就是要让你无需为时区处理的复杂和繁琐而操心,比如处理日光时(Daylight Saving Time,DST)这种问题。跟其他日期和时间类一样,ZoneId类也是无法修改的。
时区是按照一定规则将区域划分成的标准时间相同的区间。在ZoneRules这个类中包含了40个这样的实例。你可以简单地通过调用ZoneId的getRules()得到指定时区的规则。每个特定的ZoneId对象都由一个地区ID标识,比如:
ZoneId romeZone = ZoneId.of("Europe/Rome");
地区ID都为“{区域}/{城市}”的格式,这些地区集合的设定都由英特网编号分配机构(IANA)的时区数据库提供。可以通过Java 8的新方法toZoneId将一个老的时区对象转换为ZoneId:ZoneId zoneId = TimeZone.getDefault().toZoneId();
一旦得到一个ZoneId对象,就可以将它与LocalDate、LocalDateTime或Instant对象整合起来,构造一个ZonedDateTime实例,它代表了相对于指定时区的时间点。

LocalDate date = LocalDate.of(2019, Month.AUGUST, 13);//2019-08-13
ZoneId zone = ZoneId.of("Asia/Shanghai");//Asia/Shanghai
ZonedDateTime zdt1 = date.atStartOfDay(zone);//2019-08-13T00:00+08:00[Asia/Shanghai]
LocalDateTime dateTime = LocalDateTime.of(2019, Month.AUGUST, 13, 10, 15);//2019-08-13T10:15
ZonedDateTime zdt2 = dateTime.atZone(zone);//2019-08-13T10:15+08:00[Asia/Shanghai]
Instant instant = Instant.now();//2019-08-13T03:20:05.177Z
ZonedDateTime zdt3 = instant.atZone(zone);//2019-08-13T11:20:05.177+08:00[Asia/Shanghai]

通过ZoneId,还可以将LocalDateTime转换为Instant:

LocalDateTime dateTime = LocalDateTime.of(2019, Month.AUGUST, 13, 10, 45);//2019-08-13T10:45
Instant instantFromDateTime = dateTime.atZone(zone).toInstant();//2019-08-13T02:45:00Z

也可以通过反向的方式得到LocalDateTime对象:

Instant instant = Instant.now();//2019-08-13T06:33:38.711Z
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, zone);//22019-08-13T14:33:38.711
1)利用和UTC/格林尼治时间的固定偏差计算时区

一种比较通用的表达时区的方式是利用当前时区和UTC/格林尼治的固定偏差。这种情况下,可以使用ZoneOffset类,它是ZoneId的一个子类,表示当前时间和伦敦格林尼治子午线时间的差异:
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
“-05:00”的偏差实际上对应的是美国东部标准时间。注意:使用这种方式定义的ZoneOffset并未考虑任何日光时的影响,所以在大多数情况下,不推荐使用。可以创建这样的OffsetDateTime,它使用ISO-8601的历法系统,以相对于UTC/格林尼治时间的偏差方式表示日期时间。

ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");//-05:00
LocalDateTime dateTime = LocalDateTime.of(2019, Month.AUGUST, 18, 10, 15);//2019-08-18T10:15
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(dateTime, newYorkOffset);//2019-08-18T10:15-05:00
2)使用别的日历系统

ISO-8601日历系统是世界文明日历系统的事实标准。但Java 8中另外还提供了4种其他的日历系统。这些日历系统中的每个都有一个对应的日志类,分别是ThaiBuddhistDate、 MinguoDate、JapaneseDate以及HijrahDate。这些类及LocalDate都实现了ChronoLocalDate接口,能够对公历的日期进行建模。利用LocalDate对象,可以创建这些类的实例。使用它们提供的静态工厂方法,可以创建任何一个Temporal对象的实例,如下所示:

LocalDate date = LocalDate.of(2019, Month.AUGUST, 18);//2019-08-18
JapaneseDate japaneseDate = JapaneseDate.from(date);//Japanese Heisei 31-08-18

还可以为某个Locale显式地创建日历系统,接着创建该Locale对应的日期实例。新的日期和时间API中,Chronology接口建模了一个日历系统,使用它的静态工厂方法ofLocale,可以得到它的一个实例,代码如下:

Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);//ISO
ChronoLocalDate now = japaneseChronology.dateNow();//2019-08-13

日期及时间API的设计者建议我们使用LocalDate,尽量避免使用ChronoLocalDate,原因是开发者在他们的代码中可能会做一些假设,而这些假设在不同的日历系统中,有可能不成立。尽量在应用中使用LocalDate,包括存储、操作、业务规则的解读;如果需要将程序的输入或者输出本地化,这时应该使用 ChronoLocalDate类。
伊斯兰教日历
在Java 8新添加的几种日历类型中,HijrahDate(伊斯兰教日历)是复杂一个,因为它会发生各种变化。Hijrah日历系统构建于农历月份继承之上。Java 8提供了多种方法判断一个月份,比如新月,在世界的哪些地方可见,或者说它只能首先可见于沙特阿拉伯。withVariant方法可以用于选择期望的变化。为了支持HijrahDate这一标准,Java 8中还包括了乌姆库拉(Umm Al-Qura)变量。
示例:在ISO日历中计算当前伊斯兰年中斋月的起始和终止日期

HijrahDate ramadanDate = HijrahDate.now()
    .with(ChronoField.DAY_OF_MONTH, 1)
    .with(ChronoField.MONTH_OF_YEAR, 9);//取得当前的Hijrah日期,接着对其进 行修正,得到斋月的第一天,即第9个月。输出:Hijrah-umalqura AH 1440-09-01
System.out.println("Ramadan starts on "+IsoChronology.INSTANCE.date(ramadanDate)+" and ends on "
+IsoChronology.INSTANCE.date(ramadanDate.with(TemporalAdjusters.lastDayOfMonth())));//Ramadan starts on 2019-05-06 and ends on 2019-06-03

--参考文献《Java8实战》

你可能感兴趣的:(Java8学习笔记之新日期和时间API)