Dating Java8系列之新的日期和时间

发布时间 2024-01-04 23:59:16作者: 翎野君
翎野君/文

 

图片

 

图片

旧的日期时间

 

在Java 1.0中,对日期和时间的支持只能依赖java.util.Date类。正如类名所表达的,这个类无法表示日期,只能以毫秒的精度表示时间。更糟糕的是它的易用性,由于某些原因和未知的设计决策,这个类的易用性被深深地损害了,比如:年份的起始选择是1900年,月份的起始从0开始。这意味着,如果你想要用Date表示Java 8的发布日期,即2014年3月18日,需要创建下面这样的Date实例:

Date date = new Date(114, 2, 18);

它的打印输出效果为:

Tue Mar 18 00:00:00 CET 2014

看起来不那么直观,不是吗?此外,甚至Date类的toString方法返回的字符串也容易误导人。以我们的例子而言,它的返回值中甚至还包含了JVM的默认时区CET,即中欧时间(Central Europe Time)。但这并不表示Date类是一个支持时区的日期时间API。

随着Java 1.0退出历史舞台,Date类的种种问题和限制几乎一扫而光,但很明显,这些历史旧账如果不牺牲前向兼容性是无法解决的。所以,在Java1.1中,Date类中的很多方法被废弃了,取而代之的是java.util.Calendar类。很不幸,Calendar类也有类似的问题和设计缺陷,导致使用这些方法写出的代码非常容易出错。比如,月份依旧是从0开始计算(不过,至少Calendar类拿掉了由1900年开始计算年份这一设计)。更糟的是,同时存在Date和Calendar这两个类,也增加了程序员的在选择上的困难。到底该使用哪一个类呢?此外,有的特性只在某一个类有提供,比如用于以语言无关方式格式化和解析日期或时间的DateFormat方法就只在Date类里有。

DateFormat方法也有它自己的问题。比如,它不是线程安全的。这意味着两个线程如果使用同一个formatter解析日期,你可能会得到无法预期的结果。

 

图片

新的日期和时间

 

LocalDate

LocalDate类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不携带任何与时区相关的信息。

LocalDate localDate1 = LocalDate.now();System.out.println(localDate1);LocalDate localDate2 = LocalDate.of(2019, 06, 06);LocalDate localDate3 = LocalDate.of(2019, Month.JUNE, 06);System.out.println(localDate2 + " " + localDate3);System.out.println(localDate2.getDayOfYear());System.out.println(localDate2.getDayOfWeek());System.out.println(localDate2.getDayOfMonth());System.out.println(localDate2.getMonth());System.out.println(localDate2.get(ChronoField.DAY_OF_WEEK));System.out.println(localDate2.get(ChronoField.DAY_OF_YEAR));

LocalTime

类似地,一天中的时间,比如13:45:20,可以使用LocalTime类表示。你可以使用of重载的两个工厂方法创建LocalTime的实例。第一个重载函数接收小时和分 ,第二个重载函数同时还接收秒。同LocalDate一样,LocalTime类也提供了一些getter方法访问这些变量的值。

LocalTime localTime1 = LocalTime.now();System.out.println(localTime1);LocalTime localTime2 = LocalTime.of(12, 02);LocalTime localTime3 = LocalTime.of(12, 02, 03);LocalTime localTime4 = LocalTime.of(12, 02, 03, 900_000_000);System.out.println(localTime2 + " " + localTime3 + " " + localTime4);System.out.println(localTime4.getHour());System.out.println(localTime4.getMinute());System.out.println(localTime4.getSecond());

LocalDateTime

这个复合类名叫LocalDateTime,是LocalDate和LocalTime的合体。它同时表示了日期和时间,但不带有时区信息,你可以直接创建,也可以通过合并日期和时间对象进行构造。

LocalDateTime localDateTime1 = LocalDateTime.now();System.out.println(localDateTime1);LocalDateTime localDateTime2 = LocalDateTime.of(2000, Month.AUGUST, 22, 04, 05, 06);System.out.println(localDateTime2);System.out.println(LocalDateTime.of(localDate1, localTime1));System.out.println(localDate2.atTime(localTime2));System.out.println(localTime3.atDate(localDate3));System.out.println(localTime3.get(ChronoField.HOUR_OF_DAY));System.out.println(localTime3.get(ChronoField.MINUTE_OF_HOUR));

Instant

作为人,我们习惯于以星期几、几号、几点、几分这样的方式理解日期和时间。毫无疑问,这种方式对于计算机而言并不容易理解。从计算机的角度来看,建模时间最自然的格式是表示一个持续时间段上某个点的单一大整型数。这也是新的java.time.Instant类对时间建模的方式,基本上它是以Unix元年时间(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的秒数进行计算。

// InstantInstant instant = Instant.now();System.out.println(instant);// 时间戳,秒,毫秒System.out.println(instant.toEpochMilli());System.out.println(instant.getEpochSecond());// Date、Calendar、SystemDate date = new Date();System.out.println(date.getTime());System.out.println(Calendar.getInstance().getTimeInMillis());System.out.println(System.currentTimeMillis());System.out.println(instant.toEpochMilli() + " " + instant.getNano());

Duration和Period

Duration类的静态工厂方法between就是为获取两个时间的间隔。可以创建两个LocalTime对象、两个LocalDateTime。

由于LocalDateTime和Instant是为不同的目的而设计的,一个是为了便于人阅读使用, 另一个是为了便于机器处理,所以你不能将二者混用。如果试图在这两类对象之间创建duration,会触发一个DateTimeException异常。此外,由于Duration类主要用于以秒和纳秒衡量时间的长短,你不能仅向between方法传递一个LocalDate对象做参数。

如果你需要以年、月或者日的方式对多个时间单位建模,可以使用Period类。使用该类的工厂方法between,你可以使用得到两个LocalDate之间的时长。

图片

// Duration 用在LocalTime、LocalDateTimeDuration duration1 = Duration.between(localTime1, localTime2);System.out.println(duration1);System.out.println(Duration.between(localDateTime1, localDateTime2));System.out.println(Duration.ofDays(2));System.out.println(Duration.ofHours(5));System.out.println(Duration.ofMinutes(100));System.out.println(Duration.of(2, ChronoUnit.MINUTES));System.out.println(Duration.of(2, ChronoUnit.SECONDS));
// Period用在LocalDate上面Period period = Period.between(localDate1, localDate2);System.out.println(period);System.out.println(Period.ofYears(2));

 

图片

操纵日期和时间

 

简单的操纵对象

如果你已经有一个LocalDate对象,想要创建它的一个修改版,最直接也最简单的方法是使用withAttribute方法。withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。注意,下面的这段代码中所有的方法都返回一个修改了属性的对象。它们都不会修改原来的对象!

LocalDate date1 = LocalDate.of(2014, 3, 18);LocalDate date2 = date1.withYear(2011);LocalDate date3 = date2.withDayOfMonth(25);LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);

像LocalDate、LocalTime、LocalDateTime以及Instant这样表示时间点的日期和时间类提供了大量通用的方法

图片

使用TemporalAdjuster

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

import static java.time.temporal.TemporalAdjusters.*;LocalDate date1 = LocalDate.of(2014, 3, 18);LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));LocalDate date3 = date2.with(lastDayOfMonth());

使用TemporalAdjuster我们可以进行更加复杂的日期操作,而且这些方法的名称也非常直观,方法名基本就是问题陈述。此外,即使你没有找到符合你要求的预定义TemporalAdjuster,创建你自己的TemporalAdjuster也并非难事。

实际上,TemporalAdjuster接口只声明了单一的一个方法这使得它成为了一个函数式接口),定义如下。

图片

// 使用TemporalAdjuster中预定义的方法快速操纵和修改日期System.out.println(localDate.with(TemporalAdjusters.lastDayOfYear()));System.out.println(localDate.with(TemporalAdjusters.firstDayOfNextMonth()));System.out.println(localDate.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)));System.out.println(localDate.with(TemporalAdjusters.firstInMonth(DayOfWeek.TUESDAY)));// 函数式接口LocalDate firstMonthOfYear = localDate.with((temporal) -> localDate.withMonth(1));LocalDate lastMonthOfYear = localDate.with((temporal) -> localDate.withMonth(12));System.out.println(firstMonthOfYear + " " + lastMonthOfYear);

 

图片

解析日期对象

 

解析日期时间

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

LocalDate ld4 = LocalDate.of(2019, 06, 06);System.out.println(ld1.format(DateTimeFormatter.BASIC_ISO_DATE));LocalTime lt3 = LocalTime.now();System.out.println(lt3.format(DateTimeFormatter.ISO_LOCAL_TIME));System.out.println(LocalDate.parse("20190606", DateTimeFormatter.BASIC_ISO_DATE));System.out.println(LocalTime.parse(“12:02:03”, DateTimeFormatter.ISO_LOCAL_TIME));

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

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");String ld4Str = ld4.format(dateTimeFormatter);System.out.println(ld4Str);System.out.println(LocalDate.parse(ld4Str, dateTimeFormatter));

 时区

时区是按照一定的规则将区域划分成的标准时间相同的区间。在ZoneRules这个类中包含了40个这样的实例。你可以简单地通过调用ZoneId的getRules()得到指定时区的规则。每个特定的ZoneId对象都由一个地区ID标识,比如:

ZoneId zoneId = ZoneId.of("Asia/Tokyo");

地区ID都为“{区域}/{城市}”的格式,这些地区集合的设定都由因特网编号分配机构(IANA)的时区数据库提供。你可以通过Java8的新方法toZoneId将一个老的时区对象转换为ZoneId:

ZoneId zoneId = TimeZone.getDefault().toZoneId();

一旦得到一个ZoneId对象你就可以将它与LocalDate、LocalDateTime或者是Instant对象整合起来,构造为一个ZonedDateTime实例,它代表了相对于指定时区的时间点。

 

图片

// 不同时区ZoneId zoneId = ZoneId.of("Asia/Tokyo");ZoneId defaultZoneId = TimeZone.getDefault().toZoneId();LocalDateTime localDateTime2 = LocalDateTime.now();// 标记这个是哪个时区的时间System.out.println(localDateTime2);System.out.println(" !! " + localDateTime2.atZone(zoneId));// 把时间戳转成指定时区的时间System.out.println(Instant.now().atZone(zoneId));

 通过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/格林尼治时间的固定偏差计算时区。比如,基于这个理论,你可以说“北京早于伦敦8小时”。这种情况下,你可以使用ZoneOffset类,它是ZoneId的一个子类,表示的是当前时间和伦敦格林尼治子午线时间的差异:

ZoneOffset beijing = ZoneOffset.of("+08:00");OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDateTime.now(), beijing);System.out.println(offsetDateTime);

 

图片

总结

 

  • 新版的日期和时间API中,日期-时间对象是不可变的。

  • 新的API提供了两种不同的时间表示方式,有效地区分了运行时人和机器的不同需求。

  • 可以用绝对或者相对的方式操作日期和时间,操作的结果总是返回一个新的实例,老的日期时间对象不会发生变化。

  • TemporalAdjuster能够用更精细的方式操纵日期,不再局限于一次只能改变它的一个值,并且你还可按照需求定义自己的日期转换器。

  • 现在可以按照特定的格式需求,定义自己的格式器,打印输出或者解析日期时间对象。这些格式器可以通过模板创建,也可以自己编程创建,并且它们都是线程安全的。

  • 可以用相对于某个地区/位置的方式,或者以与UTC/格林尼治时间的绝对偏差的方式表示时区,并将其应用到日期-时间对象上,对其进行本地化。

 

作者:翎野君
博客:https://www.cnblogs.com/lingyejun/

 

本篇文章如有帮助到您,请给「翎野君」点个赞,感谢您的支持。