Java JDK8时间API

Table of Contents generated with DocToc

JDK8 时间API

Class Summary

  • Clock
  • Instant
  • LocalDate
  • LocalTime
  • LocalDateTime
  • OffsetTime
  • OffsetDateTime
  • Duration
  • Period
  • MonthDay
  • YearMonth
  • Year
  • ZoneId
  • ZoneOffset
  • ZonedDateTime

时间戳 & 时区

时间戳,指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,时间戳没有时区概念,不同时区下,同一时间拥有相同时间戳。比如0时区在1970年1月1日00时00分01秒的时间戳为1000,而在东八区(1970年1月1日08时00分01秒)的时间戳也为1000。

时区,时区的划分能方便世界各地本地使用时间,全球总共分为24个时区,东1区~东11区,西1区~西11区,0区和12区。0区中心经度0,东西辐射7.5度。

Java 8 时间工具

无时区时间类

  • LocalDate
  • LocalTime
  • LocalDateTime

public void testLocal() {
        long timeStamp = System.currentTimeMillis();

        // epochDays 从1970年1月1日开始,到timeStamp的天数
        LocalDate localDate = LocalDate.ofEpochDay(Math.floorDiv(timeStamp, 24 * 60 * 60 * 1000));
        // 转换成一天的时间(无日期信息)
        LocalTime localTime = LocalTime.ofSecondOfDay(Math.floorDiv(timeStamp, 1000) % (24 * 60 * 60));
        // 转换成时间
        LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
        System.out.println("now: " + LocalDateTime.now());
        System.out.println("timeStamp: " + timeStamp);
        System.out.println("localDate: " + localDate.toString());
        System.out.println("localTime: " + localTime.toString());
        System.out.println("localDateTime: " + localDateTime.toString());
}

结果:

now: 2019-09-28T02:28:02.258
timeStamp: 1569608882253
localDate: 2019-09-27
localTime: 18:28:02
localDateTime: 2019-09-27T18:28:02

顾名思义,误以为local的含义默认为本地时间,实际上默认的local为0时区的时间,并不会帮你自动转换成本地时间。换一种理解方式,Local类的时间中,没有时区信息,从而无法表示成其他时区的时间。

而且,Local类的时间API很难用哦,有个时间戳,想转换成时间类型还的自己先算算。

时区时间类

  • ZoneId
  • ZoneOffset
  • ZoneDateTime

时区ID对应关系

Short IDs IDs
EST -05:00
HST -10:00
MST -07:00
ACT Australia/Darwin
AET Australia/Sydney
AGT America/Argentina/Buenos_Aires
ART Africa/Cairo
AST America/Anchorage
BET America/Sao_Paulo
BST Asia/Dhaka
CAT Africa/Harare
CNT America/St_Johns
CST America/Chicago
CTT Asia/Shanghai
EAT Africa/Addis_Ababa
ECT Europe/Paris
IET America/Indiana/Indianapolis
IST Asia/Kolkata
JST Asia/Tokyo
MIT Pacific/Apia
NET Asia/Yerevan
NST Pacific/Auckland
PLT Asia/Karachi
PNT America/Phoenix
PRT America/Puerto_Rico
PST America/Los_Angeles
SST Pacific/Guadalcanal
VST Asia/Ho_Chi_Minh
public void testZoned() {
        long timeStamp = System.currentTimeMillis();
        LocalDate localDate = LocalDate.ofEpochDay(Math.floorDiv(timeStamp, 24 * 60 * 60 * 1000));
        LocalTime localTime = LocalTime.ofSecondOfDay(Math.floorDiv(timeStamp, 1000) % (24 * 60 * 60));
        LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);

        // 系统默认时间ID
        ZoneId zoneId = ZoneId.systemDefault();

        // 根据和0时区相差的小时数获取时区偏移
        ZoneOffset zoneOffset = ZoneOffset.ofHours(8);

        ZonedDateTime zonedDateTimeOfId = ZonedDateTime.of(localDateTime, zoneId);
        ZonedDateTime zonedDateTimeOfIdAndOffset = ZonedDateTime.ofInstant(localDateTime, zoneOffset, zoneId);
        ZonedDateTime zonedDateTimeOfIdAndOffsetLocal = ZonedDateTime.ofLocal(localDateTime, zoneId, zoneOffset);

        long beforeConversion = Math.floorDiv(timeStamp, 1000);
        long afterConversion = zonedDateTimeOfIdAndOffset.toEpochSecond();


        System.out.println("now: " + ZonedDateTime.now());
        System.out.println("timeStamp: " + timeStamp);
        System.out.println("zoneId: " + zoneId.toString());
        System.out.println("zoneOffset: " + zoneOffset.toString());
        System.out.println("zonedDateTimeOfId: " + zonedDateTimeOfId.toString());
        System.out.println("zonedDateTimeOfIdAndOffset: " + zonedDateTimeOfIdAndOffset.toString());
        System.out.println("zonedDateTimeOfIdAndOffsetLocal: " + zonedDateTimeOfIdAndOffsetLocal.toString());
        System.out.println("beforeConversion: " + beforeConversion);
        System.out.println("afterConversion: " + afterConversion);
        System.out.println("conversionOffsetHours: " + ((afterConversion - beforeConversion)/3600));
}
now: 2019-09-28T02:26:47.884+08:00[Asia/Shanghai]
timeStamp: 1569608807874
zoneId: Asia/Shanghai
zoneOffset: +08:00
zonedDateTimeOfId: 2019-09-27T18:26:47+08:00[Asia/Shanghai]
zonedDateTimeOfIdAndOffset: 2019-09-27T18:26:47+08:00[Asia/Shanghai]
zonedDateTimeOfIdAndOffsetLocal: 2019-09-27T18:26:47+08:00[Asia/Shanghai]
beforeConversion: 1569608807
afterConversion: 1569580007
conversionOffsetHours: -8

由对比结果可以下结论:ZonedDateTime在获取时间的时候,并不会根据时间戳+时区偏移来得到时间,而是将其变成:时间戳时间 == 带时区的时间。从上面测试可以看出,当直接获取系统时间时,时间为正常的东8区时间。当用时间戳转换成ZonedDateTime时,时间“损失”了8小时,原因即ZonedDateTime在将时间转换成某时区时间的时候,并不会对时间进行便宜,而仅仅是打上了一个时区标签而已。(跟我没有测试之前的直觉完全相反)

或者说,人家不是以时间戳为基准的[摊手]…

所以,如果我在开发一个接口,入参是一个时间戳,而我要将这个时间戳转换成东八区的时间的时候,在Java8的时间API上我要进行非常多步乱七八糟的转换,才能用上它的ZonedDateTime,而且极易出错。(比如上面的测试,实际的时间提前了8小时)

Java8的时间API真的好用吗???为什么of静态方法不增加一个从时间戳转换而来的方法呢???

PS. Joda Time

  • DateTime
    public void testJoda() {
        long timeStamp = System.currentTimeMillis();

        DateTime dateTime = new DateTime(timeStamp);
        DateTime offsetDateTime = dateTime.toDateTime(DateTimeZone.forOffsetHours(6));
        DateTime utcDateTime = dateTime.toDateTime(DateTimeZone.UTC);

        System.out.println("now: " + ZonedDateTime.now());
        System.out.println("timeStamp: " + timeStamp);
        System.out.println("dateTime: " + dateTime + "zone: " + dateTime.getZone());
        System.out.println("offset dateTime: " + offsetDateTime + "offsetzone: " + offsetDateTime.getZone());
        System.out.println("utc dateTime: " + utcDateTime + "utc zone: " + utcDateTime.getZone());
    }

结果:

now: 2019-09-28T12:54:06.786+08:00[Asia/Shanghai]
timeStamp: 1569646446711
dateTime: 2019-09-28T12:54:06.711+08:00zone: Asia/Shanghai
offset dateTime: 2019-09-28T10:54:06.711+06:00offsetzone: +06:00
utc dateTime: 2019-09-28T04:54:06.711Zutc zone: UTC

Joda Time会直接很多。

时间段 & 时间点

  • Duration
  • Period
  • Instant
  • Clock

利用Clock生成ZonedDateTime:

Clock clock = Clock.systemDefaultZone();
Instant defaultZoneInstant = clock.instant();
ZonedDateTime atrDT = defaultZoneInstant.atZone(ZoneId.of("Asia/Tokyo"))

Instant为某一个UTC时间点,Instant利用Clock生成,Clock实际上也是利用System.currentTimeMillis生成,Instant可以转换成任意时区的时间。

总结

UTC时间类(不用考虑时区):

  • Clock
  • Instant
  • LocalDate、LocalTime、LocalDateTime

以UTC时间作为接口时间字段时,可以直接使用Instant来初始化,这个类能使用atZone方法转换成本地时间。用Instant的好处是业务之间可以统一用UTC时间来处理,在需要本地时间的情况下再做时区转换。LocalDateTime虽然能作为UTC时间来使用,但是个人认为尽量少用,因为很容易让人困惑该时间属于哪个时区?

简单来讲,如果要用Java8的时间API,用Instant和ZonedDateTime。源码注释很有用。如果你懒得理解这些类之间的关系,那就用Joda Time(跑路…