Java8新引入的时间类

/ JAVA / 0 条评论 / 34浏览 / 自动同步于GITHUB

在本文中我们将重点深入探讨 Java 8 中关于日期和时间的特性,以帮助大家更好的掌握这个类库。

旧版时间类库回顾

在我们深入探讨java8新引入的时间类之前,我们先回顾一下我们曾经使用过的时间类,常用的主要有:

  1. java.lang.System.currentTimeMillis()方法
  2. java.util.Date
  3. java.util.Calendar
  4. java.text.SimpleDateFormat

时区无关性:对于之前我们常用的java.util.Date类和java.util.Calendar类,无论是通过Date.getTime()还是通过Calendar.getTimeInMillis()方法获取到的时间戳都是与时区无关的时间戳,它们都表示自 1970 年 1 月 1 日 00:00:00(格林威治时间,即以0时区为标准)以来的毫秒数,无论当前电脑在哪个时区,同一时间获取到的时间戳数值是一样的。同样的,与时间戳获取相关的System.currentTimeMillis()方法,也是一样与时区无关的,也是表示自 1970 年 1 月 1 日 00:00:00(格林威治时间,即以0时区为标准)以来的毫秒数

时区相关性:在java.util.Date类和java.util.Calendar类中,通过Date.toStringDate.getYearDate.getMonthDate.getDateDate.getHourCalendar.get(Calendar.YEAR)Calendar.get(Calendar.DAY_OF_MONTH)Calendar.get(Calendar.HOUR_OF_DAY)Calendar.toString等等其他方法返回的值,都是与时区有关的,默认情况下它们都是基于当前电脑所在(使用)的时区对时间戳进行转换后返回的值。java.text.SimpleDateFormat类作为时间展示相关的类,常用的几个方法SimpleDateFormat.parseSimpleDateFormat.format也都是与时区相关的,默认情况下也是基于电脑本地时区进行的时间展示字符串转换。

新版时间类库涉及的常用类介绍

新的 java.time 包中提供了一整套全新的类库,包括 LocalDateLocalTimeLocalDateTimeZonedDateTime 等,该套类库以及与此相关的一些类清单如下所示:

  1. java.time.LocalDate
  2. java.time.LocalTime
  3. java.time.LocalDateTime
  4. java.time.OffsetTime
  5. java.time.OffsetDateTime
  6. java.time.ZonedDateTime
  7. java.time.ZoneId
  8. java.time.ZoneOffset
  9. java.time.Instant
  10. java.time.format.DateTimeFormatter
  11. java.time.Duration
  12. java.time.Period

从上述列表中,我们可以看到几对容易混淆的类,单从类名看让人感觉似是而非的样子。下面我们对一些类进行介绍简单,并对几对容易混淆的类做些区别性介绍。

Instant类

Instant 表示时间戳,用于表示自 1970 年 1 月 1 日 00:00:00(格林威治时间,即以0时区为标准)以来的毫秒数。它可以由当前时间构建生成,也可以由时间戳数值构建,还可以由符合 ISO-8601 标准格式的时间字符串构建。与 LocalDateTime 不同,Instant 类没有提供直接获取年、月、日、时、分、秒等的方法,但它包含丰富的对时间戳进行操作的能力,比如增加(减少)指定的时间量、获取时间戳毫秒值(或秒值)、按指定的时间单位截断时间戳、按指定时间单位计算两个时间戳之间的时间差值、比较大小,还能将自身转换为OffsetDateTimeZonedDateTime。参考如下测试代码:

// 从当前时间构建
Instant now = Instant.now();
System.out.println(now);
// 从时间戳构建
System.out.println(Instant.ofEpochMilli(1651914176765));
// 从ISO-860格式字符串构建
System.out.println(Instant.parse("2022-05-07T16:45:14Z"));
System.out.println(Instant.parse("2022-05-07T16:45:14.242Z"));

// 获取时间戳数值(秒数)
System.out.println(now.getEpochSecond());
// 获取时间戳数值(毫秒数)
System.out.println(now.toEpochMilli());
// 获取在一秒内的毫秒数、微秒数、纳秒数(仅能获取到这三个值,获取不了年、月、日、时、分、秒,实际时间也仅仅精确到毫秒后面都是0)
System.out.println(now.get(ChronoField.MILLI_OF_SECOND));
System.out.println(now.get(ChronoField.MICRO_OF_SECOND));
System.out.println(now.get(ChronoField.NANO_OF_SECOND));
// 时间戳加2天
System.out.println(now.plus(2, ChronoUnit.DAYS));
// 时间戳加2秒
System.out.println(now.plus(2, ChronoUnit.SECONDS));
// 时间戳减2天
System.out.println(now.minus(2, ChronoUnit.DAYS));
// 按天截断时间戳(即时、分、秒、毫秒都置为零)
System.out.println(now.truncatedTo(ChronoUnit.DAYS));
// 按小时截断时间戳(即分、秒、毫秒都置为零)
System.out.println(now.truncatedTo(ChronoUnit.HOURS));
// 先构建两个时间
Instant instant1 = Instant.parse("2022-05-08T14:35:24Z")
Instant instant2 = Instant.parse("2022-05-10T12:25:45Z")
// 计算两个时间戳之间相差的秒数
System.out.println(instant1.until(instant2, ChronoUnit.SECONDS));
// 计算两个时间戳之间相差的小时数
System.out.println(instant1.until(instant2, ChronoUnit.HOURS));
// 时间instant1是否在时间instant2之前
System.out.println(instant1.isBefore(instant2));

// 时间戳转OffsetDateTime对象,并设置当前时区为东8区(由于instant时间戳是基于0时区,这里转化为年月日时分秒显示对象后,会在instant打印的时间基础上➕8小时)
OffsetDateTime offsetDateTime = now.atOffset(ZoneOffset.ofHours(8));
// 时间戳转OffsetDateTime对象,并设置当前时区为东8区(由于instant时间戳是基于0时区,这里转化为年月日时分秒显示对象后,会在instant打印的时间基础上➕8小时)
OffsetDateTime offsetDateTime2 = now.atOffset(ZoneOffset.of("+0800"));

// 时间戳转ZonedDateTime对象,并设置当前时区为东8区(由于instant时间戳是基于0时区,这里转化为年月日时分秒显示对象后,会在instant打印的时间基础上➕8小时)
ZonedDateTime zonedDateTime = now.atZone(ZoneId.of("+0800"));
// 时间戳转ZonedDateTime对象,并设置当前时区为上海时间,即东8区(由于instant时间戳是基于0时区,这里转化为年月日时分秒显示对象后,会在instant打印的时间基础上➕8小时)
ZonedDateTime zonedDateTime2 = now.atZone(ZoneId.of("Asia/Shanghai"));

ZoneId和ZoneOffset类

ZoneIdZoneOffset都用于表示时区。ZoneId是一个抽象类,表示时区标识符,它有许多实现类, ZoneOffsetZoneId的实现类。

LocalDateTime、OffsetDateTime和ZonedDateTime类

DateTimeDateTime之间的区别相信大家都懂的,就不加赘述。LocalDateTimeOffsetDateTimeZonedDateTime三个类管理时间的显示,通过它们都可以获取年、月、日、时、分、秒、毫秒等信息。前面介绍的Instant类内部存储时间戳,在需要时通过其他工具类转化为年月日时分秒展示;与Instant相反,LocalDateTimeOffsetDateTimeZonedDateTime三个类内部不存储时间戳,只存储年月日时分秒字面值,在需要时按特定时区将年月日时分秒转化为时间戳来输出时间戳值。以下是这三个类具体的区别介绍:

  1. LocalDateTime:表示日期和时间,内部仅存储了年月日时分秒毫秒等信息,不包含时区信息
  2. OffsetDateTime:表示带有时区偏移的日期时间,内部同时存储了年月日时分秒毫秒信息和时区信息。相较于 ZonedDateTime,它使用了固定的时区偏移量,比如 "+08:00"。这意味着,即使涉及到夏令时调整,OffsetDateTime 依然会保持相同的偏移量。
  3. ZonedDateTime:表示带有时区信息的日期时间,内部同时存储了年月日时分秒毫秒信息和时区信息。它包括时区的名称(如 "America/New_York")以及相对于 UTC 的偏移量(可能是夏令时和冬令时之间的多个偏移量)。这使得它非常适合处理全球范围内的时间,特别是涉及夏令时调整等时区变化的情况。(在有的国家存在夏令时和冬令时,夏令时和冬令时时,时钟显示的时候实际还会加/减一个小时)

OffsetDateTimeZonedDateTime都是带有时区信息的对象,总体而言,ZonedDateTime 更适用于全球性的应用或涉及到与不同时区合作的场景,而 OffsetDateTime 更适合于那些只关心一个固定的时区偏移,而不需要考虑夏令时等因素的场景。

LocalDateTime、OffsetDateTime、ZonedDateTime、Instant相互转换

  1. Instant 转 LocalDateTime:

    // 获取当前时间戳
    Instant instant = Instant.now();
    
    // 转换为本地日期时间
    LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
    
  2. Instant转OffsetDateTime:

    // 获取当前时间戳
    Instant instant = Instant.now();
    
    // 转换为本地日期时间
    OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));
    
  3. Instant转ZonedDateTime:

    // 获取当前时间戳
    Instant instant = Instant.now();
    
    // 转换为本地日期时间
    ZonedDateTime zonedDateTime = instant.atZone(ZoneId.of("Asia/Shanghai"));
    
  4. LocalDateTime 转 Instant:

    // 获取当前本地日期时间
    LocalDateTime localDateTime = LocalDateTime.now();
    
    // 转换为时间戳
    Instant instant = localDateTime.toInstant(ZoneOffset.ofHours(0));
    
  5. OffsetDateTime 转 Instant:

    // 获取当前带有偏移的日期时间
    OffsetDateTime offsetDateTime = OffsetDateTime.now();
    
    // 转换为时间戳
    Instant instant = offsetDateTime.toInstant();
    
  6. ZonedDateTime 转 Instant:

    // 获取当前带有时区的日期时间
    ZonedDateTime zonedDateTime = ZonedDateTime.now();
    
    // 转换为时间戳
    Instant instant = zonedDateTime.toInstant();
    

Duration和Period类

DurationPeriod 是 Java java.time 包中用于处理时间差异的两个不同的类。

  1. Duration:

    • Duration 主要用于表示两个时刻之间的时间差,以秒和纳秒为单位。它适用于处理较短时间间隔,比如几小时、几分钟、几秒等。
    • 使用 Duration 的典型场景包括计算两个 Instant 之间的时间差、两个 LocalDateTime 之间的时间差等。
    javaCopy code
    Instant start = Instant.now();
    // ... 执行一些操作 ...
    Instant end = Instant.now();
    
    Duration timeElapsed = Duration.between(start, end);
    
  2. Period:

    • Period 用于表示日期之间的差异,以年、月、日为单位。它适用于处理较长的时间间隔,主要用于计算两个日期之间的差异。
    • 使用 Period 的典型场景包括计算两个 LocalDate 之间的时间差、处理生日之类的周期性事件等。
    javaCopy code
    LocalDate startDate = LocalDate.of(2022, 1, 1);
    LocalDate endDate = LocalDate.of(2022, 12, 31);
    
    Period period = Period.between(startDate, endDate);
    

总结:

在实际应用中,根据你要处理的时间单位,选择使用 DurationPeriod,可以更方便地进行日期和时间的计算

DateTimeFormatter

DateTimeFormatter是用于格式化和解析日期时间对象。DateTimeFormatter可以通过模式匹配字符串来构建,也可以通过Java代码一步步的完成声明构建,这一点上来说它的功能比Java8版本前的SimpleDateFormat更加强大,同时使用Java代码构建时也对使用者的要求更高了一点。下面我们看看DateTimeFormatter使用的几个例子:

使用模式字符串匹配生成时间字符串

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH🇲🇲ss");
// 打印结果:2022-05-07 20:28:31
System.out.println(localDateTime.format(formatter));

使用Java代码构造匹配器

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
        .appendValue(YEAR, 4, 4, SignStyle.EXCEEDS_PAD)
        .appendLiteral('-')
        .appendValue(MONTH_OF_YEAR, 2)
        .appendLiteral('-')
        .appendValue(DAY_OF_MONTH, 2)
        .appendLiteral('T')
        .appendValue(ChronoField.HOUR_OF_DAY, 2)
        .appendLiteral(":")
        .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
        .appendLiteral(":")
        .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
        .appendLiteral(".")
        .appendValue(ChronoField.MICRO_OF_SECOND, 6)
        // parse时:根据时间字符串中声明的时区转换为时间戳
        // format时:输出时区信息
        .appendOffset("+HHMM", "Z")  // 参数2声明0时区时使用"Z"
        .toFormatter()
        // parse时:如果时间字符串中有时区信息,以时间字符串时区为准
        // parse时:如果时间字符串中没有时区信息,则这个时区字段必须设置并以此时区为准
        // format时,以这个时区为标准转换输出时间和时区信息
        .withZone(ZoneId.of("+0800"))
        ;
// 打印结果:2022-05-07T20:38:10.409000+0800
System.out.println(formatter.format(ZonedDateTime.now()));

// 注意点:
// 1. 带有时区格式的匹配模式(即包含.appendOffset(...)或者包含.appendZoneId())对象,不能用来转换LocalDateTime对象,否则将会报错,因为LocalDateTime不包含有时区信息
// 2. 带有时区格式的匹配模式对象,同样不能用来转换Instant对象,也会报错;但是与LocalDateTime有一点不同,如果formatter中包含.withZone(ZoneId.of("+0800")),则能执行成功,它会将withZone中的时区当作Instant的输出时区

最后输出值改为时区

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
        .appendValue(YEAR, 4, 4, SignStyle.EXCEEDS_PAD)
        .appendLiteral('-')
        .appendValue(MONTH_OF_YEAR, 2)
        .appendLiteral('-')
        .appendValue(DAY_OF_MONTH, 2)
        .appendLiteral('T')
        .appendValue(ChronoField.HOUR_OF_DAY, 2)
        .appendLiteral(":")
        .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
        .appendLiteral(":")
        .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
        .appendLiteral(".")
        .appendValue(ChronoField.MICRO_OF_SECOND, 6)
        // parse时:根据时间字符串中声明的时区转换为时间戳
        // format时:输出时区信息
//        .appendOffset("+HHMM", "Z")  // 参数2声明0时区时使用"Z"
        .appendZoneId()
        .toFormatter()
        // parse时:如果时间字符串中有时区信息,以时间字符串时区为准
        // parse时:如果时间字符串中没有时区信息,则这个时区字段必须设置并以此时区为准
        // format时,以这个时区为标准转换输出时间和时区信息
        .withZone(ZoneId.of("Asia/Shanghai"))
        ;
// 打印结果:2022-05-07T20:58:43.429000Asia/Shanghai
System.out.println(formatter.format(OffsetDateTime.now()));

字符串转时间

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
        .appendValue(YEAR, 4, 4, SignStyle.EXCEEDS_PAD)
        .appendLiteral('-')
        .appendValue(MONTH_OF_YEAR, 2)
        .appendLiteral('-')
        .appendValue(DAY_OF_MONTH, 2)
        .appendLiteral('T')
        .appendValue(ChronoField.HOUR_OF_DAY, 2)
        .appendLiteral(":")
        .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
        .appendLiteral(":")
        .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
        .appendLiteral(".")
        .appendValue(ChronoField.MICRO_OF_SECOND, 6)
        // parse时:根据时间字符串中声明的时区转换为时间戳
        // format时:输出时区信息
        .appendOffset("+HHMM", "Z")  // 处理时区 "Z"
        .toFormatter()
        // parse时:如果时间字符串中有时区信息,以时间字符串时区为准
        // parse时:如果时间字符串中没有时区信息,则这个时区字段必须设置并以此时区为准
        // format时,以这个时区为标准转换输出时间和时区信息
        .withZone(ZoneId.of("+0000"))
        ;

TemporalAccessor parse = formatter.parse("2022-05-07T12:48:08.845000Z");
ZonedDateTime zonedDateTime = ZonedDateTime.from(parse);
// 打印结果:2022-05-07T12:48:08.845Z
System.out.println(zonedDateTime);
// 打印结果:Z
System.out.println(zonedDateTime.getOffset());
ZonedDateTime shanghaiDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of("Asia/Shanghai"));
// 打印结果:2022-05-07T20:48:08.845+08:00[Asia/Shanghai]
System.out.println(shanghaiDateTime);
ZonedDateTime sameLocalTime = zonedDateTime.withZoneSameLocal(ZoneId.of("Asia/Shanghai"));
// 打印结果:2022-05-07T12:48:08.845+08:00[Asia/Shanghai]
System.out.println(sameLocalTime);


// 注意:
// 输入的字符串是"2022-05-07T12:48:08.845000Z"这种末尾是Z的,不能使用appendZoneId(),必须使用.appendOffset("+HHMM", "Z")