Java时间类API(java.util.Date、java.util.Calendar、JSR 310)的概念、用法

Java 8之前用Date类型来表示日期/时间,Java 8起引入了JSR 310日期/时间类型。两套体系对于本地时间、时区时间、带时区的格式化都有着不同的处理办法。

  • 想了解新旧时间API使用的话,欢迎往下看。相信对你有帮助。
  • 对时间标准如GMT、UTC、CST等概念不清楚的,可以先看🔗这里

在这里插入图片描述 在这里插入图片描述

Java时间

众所周知,JDK以版本8为界,有两套处理日期/时间的API:

  • 旧的 ── java.util.Date、java.util.TimeZone
  • 新的 ── JSR 310

时间工具(Date)❌

描述一个时间需要用到Date + TimeZone

java.util.Date

java.util.Date在JDK 1.0就已存在,用于表示日期 + 时间的类型,纵使年代已非常久远,并且此类的具有职责不单一,使用很不方便等诸多毛病,但由于十几二十年的历史原因存在,它的生命力依旧顽强,用户量巨大。

# 创建、打印
@Test
public void test() {
    Date date = new Date();
    System.out.println(date);
    // @Deprecated
    System.out.println(date.toLocaleString());
    // @Deprecated
    System.out.println(date.toGMTString());
    System.out.println(date.getTime());
}

控制台输出(可见,何等混乱,同一个工具类不同方法输出不同的格式)

Mon Feb 06 08:07:42 CST 2023
2023-2-6 8:07:42
6 Feb 2023 00:07:42 GMT
1675642062903

分析各输出含义

输出格式含义
Mon Feb 06 07:55:59 CST 2023星期 月 日 时:分秒 时区 年CST(China Standard Time,中国标准时间。GMT+8)
2023-2-6 7:55:59格式不必多说应用的本地时区时间(东8)
6 Feb 2023 00:07:42 GMT日 月 年 时:分:秒 时区GMT为“格林威治时间”
1675642062903GMT(格林威治时间)1970年1月1日0点至今经过的毫秒数
💡这是Date实际存储的数据,上面输出的字符串都是以此为基础转换的
# 格式化

常用的格式化工具有DateFormat、SimpleDateFormat

DateFormat

DateFormat特点是预设了不少的格式,但是不能直接指定格式(pattern)

预设的格式根据两个参数选择:

  • style(样式)
    • FULL(星期、年月日、时分秒、时间标准)
    • LONG(年月日、时分秒、时间标准)
    • MEDIUM(年月日、时分秒)
    • SHORT(「简写的」年月日、「简写的」时分秒)
  • locale(地区、方言) ── zh_CN(简体中文)、zh(中文)、en(english)、…

💡locale和timezone类似,其默认值是由应用的环境(启动参数/系统参数)决定的

输出的内容有三种: date、time、date+time

示例(format)

@Test
public void testDateFormat() {
    Date date = new Date();

    // 默认style
    assertEquals(DateFormat.DEFAULT, DateFormat.MEDIUM);
    // 默认locale
    assertEquals(Locale.getDefault(), Locale.SIMPLIFIED_CHINESE);

    // 打印
    showDateFormatInstanceFormat(date, Locale.getDefault());
    showDateFormatInstanceFormat(date, Locale.CHINESE);
    showDateFormatInstanceFormat(date, Locale.ENGLISH);
}

private void showDateFormatInstanceFormat(Date date, Locale locale) {
    System.out.println("========== "+locale);

    // 默认日期、日期时间格式(美式)
    System.out.println(DateFormat.getDateInstance(DateFormat.DEFAULT, locale).format(date)); // DateFormat.DEFAULT is DateFormat.MEDIUM
    System.out.println(DateFormat.getTimeInstance(DateFormat.DEFAULT, locale).format(date));
    System.out.println(DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, locale).format(date));

    System.out.println("------- style for \"date | time\"");
    System.out.println(joinDateAndTime(DateFormat.FULL, locale, date));
    System.out.println(joinDateAndTime(DateFormat.LONG, locale, date));
    System.out.println(joinDateAndTime(DateFormat.MEDIUM, locale, date));
    System.out.println(joinDateAndTime(DateFormat.SHORT, locale, date));

    System.out.println("------- style for \"date time\"");
    System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, locale).format(date));
    System.out.println(DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale).format(date));
    System.out.println(DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale).format(date));
    System.out.println(DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale).format(date));
}

private String joinDateAndTime(int style, Locale locale, Date date) {
    DateFormat dateFormat = DateFormat.getDateInstance(style, locale);
    DateFormat timeFormat = DateFormat.getTimeInstance(style, locale);
    return String.format("%s | %s", dateFormat.format(date), timeFormat.format(date));
}

在这里插入图片描述
在这里插入图片描述

示例(parse)

@Test
public void testDateFormatParse() {
    DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, Locale.SIMPLIFIED_CHINESE);

    // 正常解析zh_CN方言、FULL style
    try {
        Date parse = dateFormat.parse("2023年2月6日 星期一 下午12时03分00秒 CST");
        System.out.println(parse);
    } catch (ParseException e) {
        throw new RuntimeException(e);
    }

    // 无法识别zh_CN方言、not FULL style
    {
        boolean flag = false;
        try {
            Date parse = dateFormat.parse("2023年2月6日 下午12时03分00秒");
            System.out.println(parse);
        } catch (ParseException e) {
            flag = true;
            e.printStackTrace();
        }
        assertTrue(flag);
    }

    // 无法识别en方言
    {
        boolean flag = false;
        try {
            Date parse = dateFormat.parse("Monday, February 6, 2023 12:03:00 PM CST");
            System.out.println(parse);
        } catch (ParseException e) {
            flag = true;
            e.printStackTrace();
        }
        assertTrue(flag);
    }
}
Mon Feb 06 12:03:00 CST 2023
java.text.ParseException: Unparseable date: "2023年2月6日 下午12时03分00秒"
	at java.text.DateFormat.parse(DateFormat.java:366)
	at com.jackson.core.DateFormatTest.testDateFormatParse(DateFormatTest.java:78)
...
java.text.ParseException: Unparseable date: "Monday, February 6, 2023 12:03:00 PM CST"
	at java.text.DateFormat.parse(DateFormat.java:366)
	at com.jackson.core.DateFormatTest.testDateFormatParse(DateFormatTest.java:91)
...
SimpleDateFormat

SimpleDateFormat继承DateFormat,所以DateFormat的功能在SimpleDateFormat上都存在。而且SimpleDateFormat可以直接new出来,它的特点是能直接指定pattern(格式)。

pattern格式参考: https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html翻译

示例,略

java.util.TimeZone

在JDK8之前,Java对时区和偏移量都是使用java.util.TimeZone来表示的。

一般情况下,使用静态方法TimeZone.getDefault()即可获得当前JVM所运行的时区,比如你在中国运行程序,这个方法返回的就是中国时区(也叫北京时区、北京时间)。

# 时区ID

下面示例展示:当前默认时区、所有时区ID

@Test
public void testTimeZone() {
    System.out.println(TimeZone.getDefault());
    int i = 0;
    for (String availableID : TimeZone.getAvailableIDs()) {
        TimeZone timeZone = TimeZone.getTimeZone(availableID);
        System.out.println(String.format("%3d:%s %s %s", i, availableID, timeZone.getDisplayName(), timeZone.getRawOffset()));
        i++;
    }
}
sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=19,lastRule=null]
  0:Africa/Abidjan 格林威治时间 0
...
167:America/New_York 东部标准时间 -18000000
...
302:Asia/Seoul 韩国标准时间 32400000
303:Asia/Shanghai 中国标准时间 28800000 ----------------- 💡
...
313:Asia/Tokyo 日本标准时间 32400000
...
373:Canada/Yukon 太平洋标准时间 -28800000
...
377:EET 东欧时间 7200000
378:EST5EDT 东部标准时间 -18000000
379:Egypt 东欧时间 7200000
380:Eire 格林威治时间 0
381:Etc/GMT 格林威治时间 0
382:Etc/GMT+0 格林威治时间 0
383:Etc/GMT+1 GMT-01:00 -3600000
...
386:Etc/GMT+12 GMT-12:00 -43200000
...
395:Etc/GMT-0 格林威治时间 0
396:Etc/GMT-1 GMT+01:00 3600000
...
399:Etc/GMT-12 GMT+12:00 43200000
...
410:Etc/GMT0 格林威治时间 0
411:Etc/Greenwich 格林威治时间 0
412:Etc/UCT 协调世界时间 0
413:Etc/UTC 协调世界时间 0
414:Etc/Universal 协调世界时间 0
415:Etc/Zulu 协调世界时间 0
...
445:Europe/Minsk 莫斯科标准时间 10800000
...
475:GB 格林威治时间 0
476:GB-Eire 格林威治时间 0
477:GMT 格林威治时间 0
478:GMT0 格林威治时间 0
479:Greenwich 格林威治时间 0
480:Hongkong 香港时间 28800000
481:Iceland 格林威治时间 0
...
495:Jamaica 东部标准时间 -18000000
496:Japan 日本标准时间 32400000
497:Kwajalein 马绍尔群岛时间 43200000
498:Libya 东欧时间 7200000
499:MET 中欧时间 3600000
500:MST7MDT Mountain 标准时间 -25200000
...
507:PRC 中国标准时间 28800000
508:PST8PDT 太平洋标准时间 -28800000
...
552:Poland 中欧时间 3600000
553:Portugal 西欧时间 0
554:ROK 韩国标准时间 32400000
555:Singapore 新加坡时间 28800000
556:SystemV/AST4 大西洋标准时间 -14400000
557:SystemV/AST4ADT 大西洋标准时间 -14400000
558:SystemV/CST6 中央标准时间 -21600000
559:SystemV/CST6CDT 中央标准时间 -21600000
560:SystemV/EST5 东部标准时间 -18000000
561:SystemV/EST5EDT 东部标准时间 -18000000
562:SystemV/HST10 夏威夷标准时间 -36000000
563:SystemV/MST7 Mountain 标准时间 -25200000
564:SystemV/MST7MDT Mountain 标准时间 -25200000
565:SystemV/PST8 太平洋标准时间 -28800000
566:SystemV/PST8PDT 太平洋标准时间 -28800000
567:SystemV/YST9 阿拉斯加标准时间 -32400000
568:SystemV/YST9YDT 阿拉斯加标准时间 -32400000
569:Turkey 东欧时间 7200000
570:UCT 协调世界时间 0
571:US/Alaska 阿拉斯加标准时间 -32400000
572:US/Aleutian 夏威夷标准时间 -36000000
573:US/Arizona Mountain 标准时间 -25200000
574:US/Central 中央标准时间 -21600000
575:US/East-Indiana 东部标准时间 -18000000
576:US/Eastern 东部标准时间 -18000000
577:US/Hawaii 夏威夷标准时间 -36000000
578:US/Indiana-Starke 中央标准时间 -21600000
579:US/Michigan 东部标准时间 -18000000
580:US/Mountain Mountain 标准时间 -25200000
581:US/Pacific 太平洋标准时间 -28800000
582:US/Pacific-New 太平洋标准时间 -28800000
583:US/Samoa 萨摩亚群岛标准时间 -39600000
584:UTC 协调世界时间 0
585:Universal 协调世界时间 0
586:W-SU 莫斯科标准时间 10800000
587:WET 西欧时间 0
588:Zulu 协调世界时间 0
589:EST 东部标准时间 -18000000
590:HST 夏威夷标准时间 -36000000
591:MST Mountain 标准时间 -25200000
592:ACT 中央标准时间 (北领地) 34200000
593:AET 东部标准时间 (新南威尔斯) 36000000
594:AGT 阿根廷时间 -10800000
595:ART 东欧时间 7200000
596:AST 阿拉斯加标准时间 -32400000
597:BET 巴西利亚时间 -10800000
598:BST 孟加拉时间 21600000
599:CAT 中非时间 7200000
600:CNT 纽芬兰标准时间 -12600000
601:CST 中央标准时间 -21600000
602:CTT 中国标准时间 28800000
603:EAT 东非时间 10800000
604:ECT 中欧时间 3600000
605:IET 东部标准时间 -18000000
606:IST 印度标准时间 19800000
607:JST 日本标准时间 32400000
...

zoneId的列表是jre维护的一个文本文件,路径是你JDK/JRE的安装路径。地址在.\jre\lib目录的名为tzmappings的文本文件里。

# 时区转换

有的时候你需要做带时区的时间转换,譬如:接口返回值中既要有展示北京时间,也要展示纽约时间。这个时候就要获取到纽约的时区,以北京时间为基准在其上进行带时区转换一把:

@Test
public void testChangeTimeZone() {

    // 当前时区时间
    Date date = new Date();
    showDateAndTimeZone(TimeZone.getDefault(), date);

    // 相同时刻,转换为其他时区时间
    showDateAndTimeZone(TimeZone.getTimeZone("Etc/GMT"), date);
    showDateAndTimeZone(TimeZone.getTimeZone("America/New_York"), date);
}

private void showDateAndTimeZone(TimeZone timeZone, Date date) {
    SimpleDateFormat dateFormat = new SimpleDateFormat();
    dateFormat.setTimeZone(timeZone);
    System.out.println(String.format("%10d %s", timeZone.getRawOffset(), dateFormat.format(date)));
}
  28800000 23-2-6 上午9:43
         0 23-2-6 上午1:43
 -18000000 23-2-5 下午8:43
# 默认时区

Java让我们有多种方式可以手动设置/修改默认时区:

  1. API方式:强制将时区设为北京时区TimeZone.setDefault(TimeZone.getDefault().getTimeZone("GMT+8"));
  2. JVM参数方式:-Duser.timezone=GMT+8
  3. 运维设置方式:将操作系统主机时区设置为北京时区,这是推荐方式,可以完全对开发者无感,也方便了运维统一管理

听过很多公司在阿里云、腾讯云、国内外的云主机上部署应用时,全部都是采用运维设置统一时区:中国时区,这种方式来管理的,这样对程序来说就消除了默认时区不一致的问题,对开发者友好。

问题

# Date同时表示日期和时间

java.util.Date被设计为日期 + 时间的结合体。也就是说如果只需要日期,或者只需要单纯的时间,用Date是做不到的。

@Test
public void test1() {
    System.out.println(new Date());
}

输出:
Fri Jan 22 00:25:06 CST 2021

在这里插入图片描述

这就导致语义非常的不清晰,比如说:

判断某一天是否是假期,只和日期有关,和具体时间没有关系。如果代码这样写语义只能靠注释解释,方法本身无法达到自描述的效果,也无法通过强类型去约束,因此容易出错

# 奇怪的年月日API返回值
@Test
public void test2() {
    Date date = new Date();
    System.out.println("当前日期时间:" + date);
    System.out.println("年份:" + date.getYear());
    System.out.println("月份:" + date.getMonth());
}

输出:
当前日期时间:Fri Jan 22 00:25:16 CST 2021
年份:121
月份:0

what?年份是121年,这什么鬼?月份返回0,这又是什么鬼?

  • 原来 2021 - 1900 = 121是这么来的。那么问题来了,为何是1900这个数字呢?
  • 月份,用0表示一月。月份竟然从0开始,将人看的月份当成机器用的下标了
# Date是可变的

由于设计时对Date定义不明,所以为Date提供了创建后更改的方法。这会在调用第三方代码时感到没有安全感

@Test
public void test() {
    Date currDate = new Date();
    System.out.println("当前日期是①:" + currDate);
    boolean holiday = isHoliday(currDate);
    System.out.println("是否是假期:" + holiday);

    System.out.println("当前日期是②:" + currDate);
}

/**
 * 是否是假期
 */
private static boolean isHoliday(Date date) {
    // 架设等于这一天才是假期,否则不是
    Date holiday = new Date(2021 - 1900, 10 - 1, 1);

    if (date.getTime() == holiday.getTime()) {
        return true;
    } else {
        // 模拟写代码时不注意,使坏
        date.setTime(holiday.getTime());
        return true;
    }
}

输出:
当前日期是①:Fri Jan 22 00:41:59 CST 2021
是否是假期:true
当前日期是②:Fri Oct 01 00:00:00 CST 2021

安全考虑只能给方法传递副本实例isHoliday(currDate.clone())

# 无法理喻的java.sql.Date

为了解java.util.Date日期/时间表示不清的问题,其子类java.sql.Date重新定义了规则

  • java.sql.Date:只表示日期
  • java.sql.Time:只表示时间
  • java.sql.Timestamp:表示日期 + 时间

可实现中,是将getHours()重写了,然后直接抛出错误,让java.sql.Date不能使用getHours方法。

这违背了里氏替换原则等众多设计原则: 子类功能小于父类,这是不允许的形成继承关系的,否则其实是绕过java的编译校验规则,产生无法预估的运行时异常

@Test
public void test10() {
    long currMillis = System.currentTimeMillis();

    java.util.Date date = new Date(currMillis);
    java.sql.Date sqlDate = new java.sql.Date(currMillis);
    java.sql.Time time = new Time(currMillis);
    java.sql.Timestamp timestamp = new Timestamp(currMillis);

    System.out.println("java.util.Date:" + date);
    System.out.println("java.sql.Date:" + sqlDate);
    System.out.println("java.sql.Time:" + time);
    System.out.println("java.sql.Timestamp:" + timestamp);
}
java.util.Date:Sat Jan 16 21:50:36 CST 2021
java.sql.Date:2021-01-16
java.sql.Time:21:50:36
java.sql.Timestamp:2021-01-16 21:50:36.733
# 无法处理时区

因为日期时间的特殊性,不同的国家地区在同一时刻显示的日期时间应该是不一样的,但Date做不到。因为Date实际上只是存储了一个时间戳,这在JSR 310中被认为是Instant(瞬间),而不是DateTime(日期时间)

# 线程不安全的格式化器

SimpleDateFormat

# Calendar难当大任

从JDK 1.1 开始,Java日期时间API似乎进步了些,引入了Calendar类,并且对职责进行了划分:

  • Calendar类:日期和时间字段之间转换
  • DateFormat类:格式化和解析字符串
  • Date类:只用来承载日期和时间

有了Calendar后,原有Date中的大部分方法均标记为废弃,许多职责交由Calendar代替。

Date终于单纯了些:只需要展示日期时间而无需再顾及年月日操作、格式化操作等等了。

但Calendar设计上依旧存在很多问题,难当大任!

  1. Calendar依旧是可变的,存在不安全因素,给方法传值时依然需要传入副本
  2. Calendar的API使用起来真的很不方便,而且该类在语义上也完全不符合日期/时间的含义,使用起来更显尴尬。

时间工具(Calendar)❌

依然是比较难用的,问题不少

todo https://blog.csdn.net/shi_hong_fei_hei/article/details/118947669#t11

时间工具(JSR 310)👍🏻

在2014年随着Java 8的发布引入了全新的JSR 310日期时间。JSR-310源于精品时间库joda-time打造,是整个Java 8最大亮点之一。

JSR(Java Specification Requests,Java规范提案)指的是向JCP(Java Community Process) 提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
🔗JSR:Java SEJava EEJava ME

JSR 310: Date and Time API 从开始到发布,历时7年左右的时间。这个API的设计借鉴了Joda-Time项目并且吸取了Java SE前两个API(Date和Calendar)的经验教训。可为是考虑齐全的!

JSR 310Date/Calendar说明
流畅的API难用的APIAPI设计的好坏最直接影响编程体验,前者大大大大优于后者
实例不可变实例可变对于日期时间实例,设计为可变确实不合理也不安全。都不敢放心的传递给其它函数使用
线程安全线程不安全此特性直接决定了编码方式和健壮性

JSR-310有如下常用的日期时间API(均在java.time包下,且均是线程安全的!):

  1. Clock:时钟
  2. Instant:瞬间时间
  3. LocalDate:本地日期。只有表示年月日
  4. LocalTime:本地时间,只有表示时分秒
  5. LocalDateTime:本地日期时间,LocalDate+LocalTime
  6. OffsetDateTime:有时间偏移量的日期时间(不包含基于ZoneRegion的时间偏移量)
  7. OffsetTime:有时间偏移量的时间
  8. ZonedDateTime:有时间偏移量的日期时间(包含基于ZoneRegion的时间偏移量)

Instant ── 瞬时时间

Instant表示瞬间时间,精度到毫秒。

里面有两个核心的字段

private final long seconds; // 单位为秒的时间戳
private final int nanos; // 单位为纳秒的时间戳
# 示例: 获取当前瞬间
@Test
public void testNow() {
    Instant now = Instant.now();
    // 默认没有时区
    System.out.println(now); // ⚠️UTC +0标准显示
    System.out.println(now.getEpochSecond()); // 秒
    System.out.println(now.toEpochMilli()); // 毫秒
    // 可以设定时区
    ZonedDateTime zonedNow = now.atZone(ZoneId.systemDefault());
    System.out.println(zonedNow);
    Assert.assertEquals(now.getEpochSecond(), zonedNow.toEpochSecond());
    Assert.assertEquals(now.getEpochSecond(), zonedNow.toInstant().getEpochSecond());
    Assert.assertEquals(now.toEpochMilli(), zonedNow.toInstant().toEpochMilli());
}
2023-02-06T16:10:18.251Z <-- ⚠️UTC +0标准显示
1675699818
1675699818251
2023-02-07T00:10:18.251+08:00[Asia/Shanghai]

⚠️Instant其实就是存储System.currentTimeMillis()信息,不存储时区信息,所以转换字符串时使用默认时区:以UTC +0标准显示

# 示例: 转换long
@Test
public void testOf() {
    long currentTimeMillis = System.currentTimeMillis();
    Instant epochMilli = Instant.ofEpochMilli(currentTimeMillis);
    Instant epochSecond = Instant.ofEpochSecond(currentTimeMillis / 1000);
    System.out.println(currentTimeMillis);
    System.out.println(epochMilli);
    System.out.println(epochSecond);
}
1675669697053
2023-02-06T07:48:17.053Z
2023-02-06T07:48:17Z
# 示例: toInstant

实际开发中,Instant更像一个新旧接口的中转站。很多旧接口都提供了转换成Instant的方法。

@Test
public void testToInstant() {
    new Date().toInstant();
    Calendar.getInstance().toInstant();
}

Clock ── 计时器

Clock是一个计时器(可以有不同的计时行为)。Clock内部存储了Instant、ZoneId信息。

Clock是一个抽象类,内置了几个它的实现类。这些实现类都可以通过Clock的静态方法获取。它们分别代表了Clock不同的计时方式。

在这里插入图片描述

# SystemClock ── 系统时间

内部的Instant始终与系统时间保持一致,即使当前线程曾经中断过(sleep)

@Test
public void testSystemClock() throws InterruptedException {
    Clock systemUTC = Clock.systemUTC(); // 时区: ZoneOffset.UTC
    Clock systemDefaultZone = Clock.systemDefaultZone(); // 时区: ZoneId.systemDefault()
    Instant systemUTCInstant = systemUTC.instant();
    Instant systemDefaultZoneInstant = systemDefaultZone.instant();
    showClock(systemUTC);
    showClock(systemDefaultZone);
    // sleep!
    Thread.sleep(1000L);
    showClock(systemUTC); // 变了!
    showClock(systemDefaultZone); // 变了!
    Assert.assertNotEquals(systemUTCInstant, systemUTC.instant());
    Assert.assertNotEquals(systemDefaultZoneInstant, systemDefaultZone.instant());
}
private void showClock(Clock clock) {
    String msg = String.format("%13d %s %s", clock.millis(), clock, clock.getZone());
    System.out.println(msg);
}
1675673753551 SystemClock[Z] Z
1675673753551 SystemClock[Asia/Shanghai] Asia/Shanghai
1675673754552 SystemClock[Z] Z
1675673754552 SystemClock[Asia/Shanghai] Asia/Shanghai
# FixedClock ── 固定时间

创建时需要传入Instant、ZoneId信息。内部的Instant始终与创建时传入的一致。

@Test
public void testFixedClock() throws InterruptedException {
    Instant now = Instant.now();
    Clock fixed8 = Clock.fixed(now, ZoneId.of("UTC+8")); // FixedClock
    Clock fixed4 = Clock.fixed(now, ZoneId.of("UTC+4")); // FixedClock
    showClock(fixed8);
    showClock(fixed4);
    // sleep!
    Thread.sleep(1000L);
    assertEquals(now, fixed4.instant());
    assertEquals(now, fixed8.instant());
}
1675674541563 FixedClock[2023-02-06T09:09:01.563Z,UTC+08:00] UTC+08:00
1675674541563 FixedClock[2023-02-06T09:09:01.563Z,UTC+04:00] UTC+04:00
# OffsetClock ── 偏移时间

偏移后,Clock的计时行为不变(原先fix就不同,原先随系统走就随系统走)

@Test
public void test() throws InterruptedException {
    // system
    {
        Clock systemDefaultZoneClockOffset = Clock.offset(Clock.systemDefaultZone(), Duration.of(10L, ChronoUnit.SECONDS)); // 偏移了10s
        Instant instant = systemDefaultZoneClockOffset.instant();
        showClock(systemDefaultZoneClockOffset);
        Thread.sleep(1000L);
        showClock(systemDefaultZoneClockOffset);
        Assert.assertNotEquals(instant, systemDefaultZoneClockOffset.instant());
    }
    // fixed
    {
        Clock fixedClockOffset = Clock.offset(Clock.fixed(Instant.now(), ZoneId.systemDefault()), Duration.of(10L, ChronoUnit.SECONDS)); // 偏移了10s
        Instant instant = fixedClockOffset.instant();
        showClock(fixedClockOffset);
        Thread.sleep(1000L);
        showClock(fixedClockOffset);
        Assert.assertEquals(instant, fixedClockOffset.instant());
    }
}
# TickClock ── 间隔时间

tick(滴答)

间隔一段时间计时一次

@Test
public void testTickClock() throws InterruptedException {
    {
        Clock systemDefaultZone = Clock.systemDefaultZone();
        Clock tick = Clock.tick(systemDefaultZone, Duration.ofSeconds(5L)); // 计时5s增加一次 (CPU分配不均,将间隔增长,避免报错)
        Clock fixed = Clock.fixed(tick.instant(), ZoneId.systemDefault());
        showClock(tick);
        Assert.assertEquals(fixed.instant(), tick.instant());
        Thread.sleep(1000L);
        showClock(tick);
        Assert.assertEquals(fixed.instant(), tick.instant()); // 不变
        Thread.sleep(1000L);
        showClock(tick);
        Assert.assertEquals(fixed.instant(), tick.instant()); // 不变
        Thread.sleep(4000L);
        showClock(tick);
        Assert.assertEquals(Clock.offset(fixed, Duration.ofSeconds(5L)).instant(), tick.instant()); // 变了!
    }
}
1675680445000 TickClock[SystemClock[Asia/Shanghai],PT5S] Asia/Shanghai
1675680445000 TickClock[SystemClock[Asia/Shanghai],PT5S] Asia/Shanghai
1675680445000 TickClock[SystemClock[Asia/Shanghai],PT5S] Asia/Shanghai
1675680450000 TickClock[SystemClock[Asia/Shanghai],PT5S] Asia/Shanghai

ZoneId、ZoneOffset、ZoneRegion、ZoneRules

JSR 310用ZoneId表示时区(更准确的含义是“区”),标识Instant和LocalDateTime之间进行转换的规则

ZoneId(区)是abstract(抽象)的,有两种不同的实现类型:

  • 时区(ZoneOffset,public) ── 指定相对于UTC/GMT(格林威治标准时间)的固定偏移量。转换规则就一条:偏移量运算
    e.g. UTC+8+8、…
  • 地区(ZoneRegion,default) ── 指定一个区域名,该区域名人为的绑定了一组转换规则(ZoneRules)。转换规则如:偏移量、夏节令、…
    e.g. Asia/ShanghaiAmerica/New_York、…

在这里插入图片描述

e.g.

中国横跨5个时区,可以划分5个ZoneOffset(区域偏移量),但是只有1个ZoneId(时区:因为“统一使用北京时间”这一规则)

# ZoneOffset ── 时区

ZoneOffset可以通过ZoneId.of("+8")ZoneOffset.of("+8")获取。ZoneOffset不包含时区信息

@Test
public void ofZoneOffset() {
    ZoneId zoneId = ZoneId.of("+8");
    System.out.println(zoneId);
    System.out.println(zoneId.getClass());
    System.out.println(zoneId.getRules());
    Assert.assertEquals("+08:00", zoneId.getId());
    Assert.assertEquals("java.time.ZoneOffset", zoneId.getClass().getName());

    // ZoneOffset
    Assert.assertEquals(ZoneOffset.of("+8"), zoneId);

    // ZoneRegion
    ZoneId temp = ZoneId.of("UTC+8");
    Assert.assertNotEquals(temp, zoneId); // 💡不相等。因为类就不一样,类包含的信息也不一样(ZoneOffset不包含时区)
    Assert.assertEquals("UTC+08:00", temp.getId());
    Assert.assertEquals("java.time.ZoneRegion", temp.getClass().getName());

    System.out.println("-------- 计算Instant");
    Instant now = Instant.now();
    System.out.println(now);
    System.out.println(now.atZone(temp));
    System.out.println(now.atZone(zoneId));
    Assert.assertNotEquals(now.atZone(temp), now.atZone(zoneId)); // ⚠️不一样。因为偏移量没有包含时区信息
}
+08:00
class java.time.ZoneOffset
ZoneRules[currentStandardOffset=+08:00]
-------- 计算Instant
2023-02-06T17:48:01.540Z
2023-02-07T01:48:01.540+08:00[UTC+08:00]
2023-02-07T01:48:01.540+08:00
# ZoneRegion ── 地区

ZoneRegion是default的,包外没法直接使用。只能通过ZoneId.of方法间接构造

@Test
public void ofZoneRegion() {
    ZoneId of = ZoneId.of("Etc/GMT+8");
    System.out.println(of);
    System.out.println(of.getClass());
    Assert.assertEquals("java.time.ZoneRegion", of.getClass().getName());
}
# 示例: 转换TimeZone、默认时区

在JDK 8之前,Java使用java.util.TimeZone来表示时区。而在JDK 8里分别使用了ZoneId表示时区。

在JDK 8后,TimeZone也是可以转换成ZoneId了

@Test
public void testTimeZone() {
    // JDK 1.8之前做法
    TimeZone timeZone = TimeZone.getDefault();
    // JDK 1.8之后做法
    ZoneId zoneId = ZoneId.systemDefault();

    System.out.println(timeZone);
    System.out.println(zoneId);
    System.out.println(zoneId.getClass());

    assertEquals(zoneId, timeZone.toZoneId()); // ZoneId.systemDefault源码就是这样写的!
}
sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=19,lastRule=null]
Asia/Shanghai
class java.time.ZoneRegion
# 示例: 遍历AvailableZoneIds

Java8为TimeZone添加了toZoneId方法,因此所有的TimeZone都可以转换成ZoneId!

@Test
public void testAvailableZoneIds() {
    Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
    availableZoneIds.forEach(new Consumer<String>() {
        int index = 0;
        @Override
        public void accept(String s) {
            String msg = String.format("%3d %s", index, s);
            System.out.println(msg);
            index++;
        }
    });
}

有500多个

  0 Asia/Aden
  1 America/Cuiaba
  2 Etc/GMT+9
  3 Etc/GMT+8
...
 32 CET
 33 Etc/GMT-1
 34 Etc/GMT-0
...
180 Asia/Shanghai
...
586 Europe/Athens
587 US/Pacific
588 Europe/Monaco
# 示例: 使用ZoneOffset
@Test
public void ofOffset() {
    ZoneId ofOffset = ZoneId.ofOffset("UTC", ZoneOffset.of("+8"));
    System.out.println(ofOffset);
    System.out.println(ofOffset.getClass());
    ZoneId of = ZoneId.of("UTC+8");
    System.out.println(of);
    Assert.assertEquals(ofOffset, of);
}
UTC+08:00
class java.time.ZoneRegion
# 示例: 从日期中获取
@Test
public void from() {
    ZonedDateTime now = ZonedDateTime.now();
    ZoneId from = ZoneId.from(now);
    System.out.println(from);
    Assert.assertEquals(from, now.getZone());
}
Asia/Shanghai
# ZoneRules ── 区域规则

ZoneOffset

对于ZoneOffset的规则就是自己

public final class ZoneOffset
    @Override
    public ZoneRules getRules() {
        return ZoneRules.of(this);
    }

public final class ZoneRules implements Serializable {
    public static ZoneRules of(ZoneOffset offset) {
        Objects.requireNonNull(offset, "offset");
        return new ZoneRules(offset);
    }
    private ZoneRules(ZoneOffset offset) {
        this.standardOffsets = new ZoneOffset[1];
        this.standardOffsets[0] = offset;
        this.standardTransitions = EMPTY_LONG_ARRAY;
        this.savingsInstantTransitions = EMPTY_LONG_ARRAY;
        this.savingsLocalTransitions = EMPTY_LDT_ARRAY;
        this.wallOffsets = standardOffsets;
        this.lastRules = EMPTY_LASTRULES;
    }

ZoneRegion

对ZoneRegion的规则由ZoneRulesProvider提供

final class ZoneRegion extends ZoneId implements Serializable {
    @Override
    public ZoneRules getRules() {
        // additional query for group provider when null allows for possibility
        // that the provider was updated after the ZoneId was created
        return (rules != null ? rules : ZoneRulesProvider.getRules(id, false));
    }

如果代码一直跟下去,会找到这样的语句

public TzdbZoneRulesProvider() {
    try {
        String libDir = System.getProperty("java.home") + File.separator + "lib";
        try (DataInputStream dis = new DataInputStream(
                 new BufferedInputStream(new FileInputStream(
                     new File(libDir, "tzdb.dat"))))) {
            load(dis);
        }
    } catch (Exception ex) {
        throw new ZoneRulesException("Unable to load TZDB time-zone rules", ex);
    }
}

于是我们知道,ZoneRegion的ZoneRules由${java.home}/lib/tzdb.dat提供

在这里插入图片描述

更新

由于有DST(夏令时)这种人为制定的规则,所以我们定期(或者日志有提示,或者程序报错后)需要更新该文件,以获取新规则

官方也提供了工具: https://www.oracle.com/java/technologies/javase/tzupdater-readme.html

# 示例: 遍历多规则ZoneRules
@Test
public void test() {
    ZoneId.getAvailableZoneIds().forEach(new Consumer<String>() {
        private int index = 0;
        @Override
        public void accept(String s) {
            ZoneId of = ZoneId.of(s);
            List<ZoneOffsetTransitionRule> transitionRules = of.getRules().getTransitionRules();
            if (transitionRules.size() > 1) {
                String msg = String.format("%3d: %s %s", index, of, transitionRules);
                System.out.println(msg);
            }
            index ++;
        }
    });
}

输出规则看不懂,但大为震撼

  1: America/Cuiaba [TransitionRule[Overlap -03:00 to -04:00, SUNDAY on or after FEBRUARY 15 at 00:00 WALL, standard offset -04:00], TransitionRule[Gap -04:00 to -03:00, SUNDAY on or after OCTOBER 15 at 00:00 WALL, standard offset -04:00]]
 17: Australia/Hobart [TransitionRule[Overlap +11:00 to +10:00, SUNDAY on or after APRIL 1 at 02:00 STANDARD, standard offset +10:00], TransitionRule[Gap +10:00 to +11:00, SUNDAY on or after OCTOBER 1 at 02:00 STANDARD, standard offset +10:00]]
 18: Europe/London [TransitionRule[Gap Z to +01:00, SUNDAY on or after MARCH 25 at 01:00 UTC, standard offset Z], TransitionRule[Overlap +01:00 to Z, SUNDAY on or after OCTOBER 25 at 01:00 UTC, standard offset Z]]
...
# 总结

一般我们使用系统的默认时区既可。如果有跨区域部署的应用,一般也不需要程序员操心,那是运营的事,他们按需要把系统参数(/启动参数)调成统一或者不统一既可,然后应用就会自动识别并更换时区了。

当我们需要时区转换时,也需要看需求判断使用ZoneOffset还是ZoneRegion,下面是我个人的思路

  • ZoneOffset ── 需求中给了固定的偏移量时使用
  • ZoneRegion ── 需求中只给了区域名时使用

LocalDate、LocalTime、LocalDateTime、OffsetDateTime、ZonedDateTime

鉴于Date日期、时间描述不清除的问题,JSR 310对日期、时间进行了分开表示:LocalDate、LocalTime、LocalDateTime

对本地时间和带时区的时间进行了分开管理:LocalXXX(本地)、ZonedXXX(带有时区)

在这里插入图片描述

它们三个没有相互继承的关系,但是都是TemporalAccessor接口的实现。因此,在设计方法时,如果仅需要基础信息,可以统一将它们定义为TemporalAccessor接口

在这里插入图片描述

# 示例: 创建、打印
@Test
public void test8() {
    // 本地日期/时间
    System.out.println("================本地时间================");
    System.out.println(LocalDate.now());
    System.out.println(LocalTime.now());
    System.out.println(LocalDateTime.now());

    // 时区时间
    System.out.println("================带时区的时间ZonedDateTime================");
    System.out.println(ZonedDateTime.now()); // 使用系统时区
    System.out.println(ZonedDateTime.now(ZoneId.of("America/New_York"))); // 自己指定时区
    System.out.println(ZonedDateTime.now(Clock.systemUTC())); // 自己指定时区
    System.out.println("================带时区的时间OffsetDateTime================");
    System.out.println(OffsetDateTime.now()); // 使用系统时区
    System.out.println(OffsetDateTime.now(ZoneId.of("America/New_York"))); // 自己指定时区
    System.out.println(OffsetDateTime.now(Clock.systemUTC())); // 自己指定时区
}
================本地时间================
2021-01-17
09:18:40.703
2021-01-17T09:18:40.703
================带时区的时间ZonedDateTime================
2021-01-17T09:18:40.704+08:00[Asia/Shanghai]
2021-01-16T20:18:40.706-05:00[America/New_York]
2021-01-17T01:18:40.709Z
================带时区的时间OffsetDateTime================
2021-01-17T09:18:40.710+08:00
2021-01-16T20:18:40.710-05:00
2021-01-17T01:18:40.710Z
# LocalDate、LocalTime、LocalDateTime

在这里插入图片描述

LocalDateTime是一个不可变的日期-时间对象,它表示一个日期时间(ISO-8601日历系统中不带时区的日期时间),通常被视为年-月-日-小时-分钟-秒。还可以访问其他日期和时间字段,如day-of-year、day-of-week和week-of-year等等,它的精度能达纳秒级别。

💡 ISO-8601日系统是现今世界上绝大部分国家/地区使用的,这就是我们国人所说的公历,有闰年的特性

该类不存储时区,所以适合日期的描述,比如用于生日、deadline等等。但是请记住,如果没有偏移量/时区等附加信息,一个时间是不能表示时间线上的某一时刻的。

构造
@Test
public void test2() {
    System.out.println("当前时区的本地时间:" + LocalDateTime.now());
    System.out.println("当前时区的本地时间:" + LocalDateTime.of(LocalDate.now(), LocalTime.now()));

    System.out.println("纽约时区的本地时间:" + LocalDateTime.now(ZoneId.of("America/New_York"))); // 💡构造传入了ZoneId,并不是说LocalDateTime和时区有关了,而是告诉说这个Local指的是纽约
}

输出:
当前时区的本地时间:2021-01-17T17:00:41.446
当前时区的本地时间:2021-01-17T17:00:41.447
纽约时区的本地时间:2021-01-17T04:00:41.450
计算
@Test
public void test3() {
    LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault());
    System.out.println("计算前:" + now);

    // 加3天
    LocalDateTime after = now.plusDays(3);
    // 减4个小时
    after = after.plusHours(-3); // 效果同now.minusDays(3);
    System.out.println("计算后:" + after);

    // 计算时间差
    Period period = Period.between(now.toLocalDate(), after.toLocalDate());
    System.out.println("相差天数:" + period.getDays());
    Duration duration = Duration.between(now.toLocalTime(), after.toLocalTime());
    System.out.println("相差小时数:" + duration.toHours());
}

输出:
计算前:2021-01-17T17:10:15.381
计算后:2021-01-20T14:10:15.381
相差天数:3
相差小时数:-3
格式化
@Test
public void test4() {
    LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault());
    // System.out.println("格式化输出:" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(now));
    System.out.println("格式化输出(本地化输出,中文环境):" + DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT).format(now));

    String dateTimeStrParam = "2021-01-17 18:00:00";
    System.out.println("解析后输出:" + LocalDateTime.parse(dateTimeStrParam, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.US)));
}

输出:
格式化输出(本地化输出,中文环境):21-1-17 下午5:15
解析后输出:2021-01-17T18:00
# OffsetDateTime

在这里插入图片描述

OffsetDateTime是一个带有偏移量的日期时间类型。存储有精确到纳秒的日期时间,以及偏移量(ISO-8601日历系统中与UTC偏移量有关的日期时间)。可以简单理解为 OffsetDateTime = LocalDateTime + ZoneOffset。

OffsetDateTime、ZonedDateTime和Instant它们三都能在时间线上以纳秒精度存储一个瞬间(请注意:LocalDateTime是不行的),也可理解我某个时刻。OffsetDateTime和Instant可用于模型的字段类型,因为它们都表示瞬间值并且还不可变,所以适合网络传输或者数据库持久化

构造
@Test
public void test6() {
    System.out.println("当前位置偏移量的本地时间:" + OffsetDateTime.now());
    System.out.println("偏移量-4(纽约)的本地时间::" + OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.of("-4")));

    System.out.println("纽约时区的本地时间:" + OffsetDateTime.now(ZoneId.of("America/New_York")));
}

输出:
当前位置偏移量的本地时间:2021-01-17T19:02:06.328+08:00
偏移量-4(纽约)的本地时间::2021-01-17T19:02:06.329-04:00
纽约时区的本地时间:2021-01-17T06:02:06.330-05:00
计算

格式化
@Test
public void test7() {
    OffsetDateTime now = OffsetDateTime.now(ZoneId.systemDefault());
    System.out.println("格式化输出(本地化输出,中文环境):" + DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT).format(now));

    String dateTimeStrParam = "2021-01-17T18:00:00+07:00";
    System.out.println("解析后输出:" + OffsetDateTime.parse(dateTimeStrParam));
}

输出:
格式化输出(本地化输出,中文环境):21-1-17 下午7:06
解析后输出:2021-01-17T18:00+07:00
转换:LocalDateTime -> OffsetDateTime
@Test
public void test8() {
    LocalDateTime localDateTime = LocalDateTime.of(2021, 01, 17, 18, 00, 00);
    System.out.println("当前时区(北京)时间为:" + localDateTime);

    // 转换为偏移量为 -4的OffsetDateTime时间
    // 1、-4地方的晚上18点
    System.out.println("-4偏移量地方的晚上18点:" + OffsetDateTime.of(localDateTime, ZoneOffset.ofHours(-4)));
    System.out.println("-4偏移量地方的晚上18点(方式二):" + localDateTime.atOffset(ZoneOffset.ofHours(-4)));
    // 2、北京时间晚上18:00 对应的-4地方的时间点
    System.out.println("当前地区对应的-4地方的时间:" + OffsetDateTime.ofInstant(localDateTime.toInstant(ZoneOffset.ofHours(8)), ZoneOffset.ofHours(-4)));
}

输出:
当前时区(北京)时间为:2021-01-17T18:00
-4偏移量地方的晚上18点:2021-01-17T18:00-04:00
-4偏移量地方的晚上18点(方式二):2021-01-17T18:00-04:00
当前地区对应的-4地方的时间:2021-01-17T06:00-04:00

通过此例值得注意的是:LocalDateTime.atOffset()/atZone()只是增加了偏移量/时区,本地时间是并没有改变的。若想实现本地时间到其它偏移量的对应的时间只能通过其ofInstant()系列构造方法。

转换:OffsetDateTime -> LocalDateTime
@Test
public void test81() {
    OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.ofHours(-4));
    System.out.println("-4偏移量时间为:" + offsetDateTime);

    // 转为LocalDateTime 注意:时间还是未变的哦
    System.out.println("LocalDateTime的表示形式:" + offsetDateTime.toLocalDateTime());
}

输出:
-4偏移量时间为:2021-01-17T19:33:28.139-04:00
LocalDateTime的表示形式:2021-01-17T19:33:28.139
# ZonedDateTime

在这里插入图片描述

ZonedDateTime存储所有的日期和时间字段,精度为纳秒,以及一个时区,带有用于处理不明确的本地日期时间的时区偏移量(ISO-8601国际标准日历系统中带有时区的日期时间)

ZonedDateTime不适合网络传输/持久化,因为即使同一个ZoneId时区,不同地方获取到瞬时值也有可能不一样

构造
@Test
public void test9() {
    System.out.println("当前位置偏移量的本地时间:" + ZonedDateTime.now());
    System.out.println("纽约时区的本地时间:" + ZonedDateTime.of(LocalDateTime.now(), ZoneId.of("America/New_York")));

    System.out.println("北京实现对应的纽约时区的本地时间:" + ZonedDateTime.now(ZoneId.of("America/New_York")));
}

输出:
当前位置偏移量的本地时间:2021-01-17T19:25:10.520+08:00[Asia/Shanghai]
纽约时区的本地时间:2021-01-17T19:25:10.521-05:00[America/New_York]
北京实现对应的纽约时区的本地时间:2021-01-17T06:25:10.528-05:00[America/New_York]
计算

格式化

转换:LocalDateTime -> ZonedDateTime

ZonedDateTime可简单认为是LocalDateTime和ZoneId的组合。

@Test
public void test10() {
    LocalDateTime localDateTime = LocalDateTime.of(2021, 01, 17, 18, 00, 00);
    System.out.println("当前时区(北京)时间为:" + localDateTime);

    // 转换为偏移量为 -4的OffsetDateTime时间
    // 1、-4地方的晚上18点
    System.out.println("纽约时区晚上18点:" + ZonedDateTime.of(localDateTime, ZoneId.of("America/New_York")));
    System.out.println("纽约时区晚上18点(方式二):" + localDateTime.atZone(ZoneId.of("America/New_York")));
    // 2、北京时间晚上18:00 对应的-4地方的时间点
    System.out.println("北京地区此时间对应的纽约的时间:" + ZonedDateTime.ofInstant(localDateTime.toInstant(ZoneOffset.ofHours(8)), ZoneOffset.ofHours(-4)));
    System.out.println("北京地区此时间对应的纽约的时间:" + ZonedDateTime.ofInstant(localDateTime, ZoneOffset.ofHours(8), ZoneOffset.ofHours(-4)));
}

输出:
当前时区(北京)时间为:2021-01-17T18:00
纽约时区晚上18点:2021-01-17T18:00-05:00[America/New_York]
纽约时区晚上18点(方式二):2021-01-17T18:00-05:00[America/New_York]
北京地区此时间对应的纽约的时间:2021-01-17T06:00-04:00
北京地区此时间对应的纽约的时间:2021-01-17T06:00-04:00
转换:OffsetDateTime -> ZonedDateTime
  • toZonedDateTime() ── 同一时刻(Instant),相同区域,转换为ZonedDateTime
  • atZoneSameInstant(ZoneId) ── 同一时刻(Instant),指定区域,转换为ZonedDateTime
  • atZoneSimilarLocal(ZoneId) ── 没想到应用场景
 /**
 * 💡下面展示:2021-03-14T02:00-05:00 ~ 2021-11-07T02:00-05:00 这段时间内,对应纽约夏节令的情况
 *                                                                      LocalDateTime对应个数
 *                                                                      ↓
 * 2021-03-14T01:58-05:00 <==> 2021-03-14T01:58-05:00[America/New_York] 1
 * 2021-03-14T01:59-05:00 <==> 2021-03-14T01:59-05:00[America/New_York] 1
 *                             2021-03-14T02:00                         0 <- ⚠️Note1
 *                             2021-03-14T02:00                         0
 *                             ...
 *                             2021-03-14T02:59                         0
 * 2021-03-14T02:00-05:00 <==> 2021-03-14T03:00-04:00[America/New_York] 1
 * 2021-03-14T02:01-05:00 <==> 2021-03-14T03:01-04:00[America/New_York] 1
 * ...
 * 2021-03-14T02:59-05:00 <==> 2021-03-14T03:59-04:00[America/New_York] 1
 * 2021-03-14T03:00-05:00 <==> 2021-03-14T04:00-04:00[America/New_York] 1 <- ⚠️Note2
 * 2021-03-14T03:01-05:00 <==> 2021-03-14T04:01-04:00[America/New_York] 1
 * ...
 * 2021-11-06T23:59-05:00 <==> 2021-11-07T00:59-04:00[America/New_York] 1
 * 2021-11-07T00:00-05:00 <==> 2021-11-07T01:00-04:00[America/New_York] 2 <- ⚠️Note3
 * 2021-11-07T00:01-05:00 <==> 2021-11-07T01:01-04:00[America/New_York] 2
 * ...
 * 2021-11-07T00:58-05:00 <==> 2021-11-07T01:58-04:00[America/New_York] 2
 * 2021-11-07T00:59-05:00 <==> 2021-11-07T01:59-04:00[America/New_York] 2
 * 2021-11-07T01:00-05:00 <==> 2021-11-07T01:00-05:00[America/New_York] 2 <- ⚠️Note3
 * 2021-11-07T01:01-05:00 <==> 2021-11-07T01:01-05:00[America/New_York] 2
 * ...
 * 2021-11-07T01:59-05:00 <==> 2021-11-07T01:59-05:00[America/New_York] 2
 * 2021-11-07T02:00-05:00 <==> 2021-11-07T00:00-05:00[America/New_York] 1
 *
 * 描述ZonedDateTime.ofLocal(LocalDateTime localDateTime, ZoneId zone, ZoneOffset preferredOffset)的工作流程:
 *  设zone=America/New_York
 *  情况
 *  1. 若收到LocalDateTime=2021-03-14T02:00(Note1),这个LocalDateTime在纽约夏节令中是不存在的。
 *      于是,会将OffsetZone=-05:00赋予这个LocalDateTime。并将它们整体转换为OffsetZone=-04:00(夏节令)
 *  2. 若收到LocalDateTime=2021-03-14T04:00(Note2),这个LocalDateTime在纽约夏节令中只有一个对应: 2021-03-14T04:00-04:00
 *  3. 若收到LocalDateTime=2021-11-07T01:00(Note3),这个LocalDateTime在纽约夏节令中有两个对应: 2021-11-07T01:00-04:00、2021-11-07T01:00-05:00
 *      于是,若preferredOffset有合法指定(-04:00或者-05:00)则使用。否则,使用更大的那个(这样Instant会更小)
 */
@Test
public void testOfLocal() {
    ZoneId of = ZoneId.of("America/New_York");
    ZoneOffset offset = ZoneOffset.of("-5");
    ZonedDateTime note1 = ZonedDateTime.ofLocal(LocalDateTime.parse("2021-03-14T02:00"), of, offset);
    ZonedDateTime note2 = ZonedDateTime.ofLocal(LocalDateTime.parse("2021-03-14T04:00"), of, offset);
    ZonedDateTime note3 = ZonedDateTime.ofLocal(LocalDateTime.parse("2021-11-07T01:00"), of, offset);

    showDateTime(note1);
    showDateTime(note2);
    showDateTime(note3);
}

输出:
1615705200000:America/New_York 2021-03-14T03:00-04:00[America/New_York]
1615708800000:America/New_York 2021-03-14T04:00-04:00[America/New_York]
1636264800000:America/New_York 2021-11-07T01:00-05:00[America/New_York]

@Test
public void testCharge() {
    // CST开始
    showCharge("2021-03-14T01:59-05:00");
    showCharge("2021-03-14T02:00-05:00");
    showCharge("2021-03-14T02:01-05:00");
    // ...
    showCharge("2021-03-14T02:59-05:00");
    showCharge("2021-03-14T03:00-05:00");
    showCharge("2021-03-14T03:30-05:00");
    // CST结束
    showCharge("2021-11-06T23:59-05:00");
    showCharge("2021-11-07T00:00-05:00");
    showCharge("2021-11-07T00:01-05:00");
    // ...
    showCharge("2021-11-07T00:59-05:00");
    showCharge("2021-11-07T01:00-05:00");
    showCharge("2021-11-07T01:01-05:00");
}
private void showCharge(String offsetDateTime) {

    System.out.println("----------"+offsetDateTime);

    OffsetDateTime parse = OffsetDateTime.parse(offsetDateTime);
    showDateTime(parse);

    // ⚠️保持Instant的转换是安全的,只要确定ZoneId后,就能相互转换
    System.out.println("~~SameInstant转换");

    ZonedDateTime toZonedDateTime = parse.toZonedDateTime(); // 💡时区 to 地区 with “保持Instant一致,使用时区Id作为地区Id”
    showDateTime(toZonedDateTime);
    assertEquals(toZonedDateTime, ZonedDateTime.of(parse.toLocalDateTime(), parse.getOffset()));
    assertEquals(toZonedDateTime, ZonedDateTime.ofLocal(parse.toLocalDateTime(), parse.getOffset(), null));
    assertEquals(toZonedDateTime, ZonedDateTime.ofInstant(parse.toLocalDateTime(), parse.getOffset(), parse.getOffset())); // LocalDateTime + Offset = Instant

    // 美国纽约时区
    ZoneId of = ZoneId.of("America/New_York"); // 💡这时是夏节令

    ZonedDateTime atZoneSameInstant = parse.atZoneSameInstant(of); // 💡时区 to 地区 with “保持Instant一致”
    showDateTime(atZoneSameInstant);
    assertEquals(atZoneSameInstant, ZonedDateTime.ofInstant(parse.toLocalDateTime(), parse.getOffset(), of)); // LocalDateTime + Offset = Instant
    assertEquals(atZoneSameInstant.toInstant(), parse.toInstant());

    // ⚠️保持local一致的转换是危险的,因为Instant、ZoneId均可能变化,如果错误使用可能导致数据/含义变化
    System.out.println("~~SimilarLocal转换");

    ZonedDateTime atZoneSimilarLocal = parse.atZoneSimilarLocal(of); // 💡时区 to 地区 with “保持LocalDateTime含义一致(但不一定一致!)”
    showDateTime(atZoneSimilarLocal);
    assertEquals(atZoneSimilarLocal, ZonedDateTime.ofLocal(parse.toLocalDateTime(), of, parse.getOffset())); // 💡第三个参数preferredOffset用于指明夏节令结束时的歧义
}
----------2021-03-14T01:59-05:00
1615705140000:-05:00 2021-03-14T01:59-05:00
~~SameInstant转换
1615705140000:-05:00 2021-03-14T01:59-05:00
1615705140000:America/New_York 2021-03-14T01:59-05:00[America/New_York]
~~SimilarLocal转换
1615705140000:America/New_York 2021-03-14T01:59-05:00[America/New_York]
----------2021-03-14T02:00-05:00
1615705200000:-05:00 2021-03-14T02:00-05:00
~~SameInstant转换
1615705200000:-05:00 2021-03-14T02:00-05:00
1615705200000:America/New_York 2021-03-14T03:00-04:00[America/New_York]
~~SimilarLocal转换
1615705200000:America/New_York 2021-03-14T03:00-04:00[America/New_York]
----------2021-03-14T02:01-05:00
1615705260000:-05:00 2021-03-14T02:01-05:00
~~SameInstant转换
1615705260000:-05:00 2021-03-14T02:01-05:00
1615705260000:America/New_York 2021-03-14T03:01-04:00[America/New_York]
~~SimilarLocal转换
1615705260000:America/New_York 2021-03-14T03:01-04:00[America/New_York]
----------2021-03-14T02:59-05:00
1615708740000:-05:00 2021-03-14T02:59-05:00
~~SameInstant转换
1615708740000:-05:00 2021-03-14T02:59-05:00
1615708740000:America/New_York 2021-03-14T03:59-04:00[America/New_York]
~~SimilarLocal转换
1615708740000:America/New_York 2021-03-14T03:59-04:00[America/New_York]
----------2021-03-14T03:00-05:00
1615708800000:-05:00 2021-03-14T03:00-05:00
~~SameInstant转换
1615708800000:-05:00 2021-03-14T03:00-05:00
1615708800000:America/New_York 2021-03-14T04:00-04:00[America/New_York]
~~SimilarLocal转换
1615705200000:America/New_York 2021-03-14T03:00-04:00[America/New_York]
----------2021-03-14T03:30-05:00
1615710600000:-05:00 2021-03-14T03:30-05:00
~~SameInstant转换
1615710600000:-05:00 2021-03-14T03:30-05:00
1615710600000:America/New_York 2021-03-14T04:30-04:00[America/New_York]
~~SimilarLocal转换
1615707000000:America/New_York 2021-03-14T03:30-04:00[America/New_York]
----------2021-11-06T23:59-05:00
1636261140000:-05:00 2021-11-06T23:59-05:00
~~SameInstant转换
1636261140000:-05:00 2021-11-06T23:59-05:00
1636261140000:America/New_York 2021-11-07T00:59-04:00[America/New_York]
~~SimilarLocal转换
1636257540000:America/New_York 2021-11-06T23:59-04:00[America/New_York]
----------2021-11-07T00:00-05:00
1636261200000:-05:00 2021-11-07T00:00-05:00
~~SameInstant转换
1636261200000:-05:00 2021-11-07T00:00-05:00
1636261200000:America/New_York 2021-11-07T01:00-04:00[America/New_York]
~~SimilarLocal转换
1636257600000:America/New_York 2021-11-07T00:00-04:00[America/New_York]
----------2021-11-07T00:01-05:00
1636261260000:-05:00 2021-11-07T00:01-05:00
~~SameInstant转换
1636261260000:-05:00 2021-11-07T00:01-05:00
1636261260000:America/New_York 2021-11-07T01:01-04:00[America/New_York]
~~SimilarLocal转换
1636257660000:America/New_York 2021-11-07T00:01-04:00[America/New_York]
----------2021-11-07T00:59-05:00
1636264740000:-05:00 2021-11-07T00:59-05:00
~~SameInstant转换
1636264740000:-05:00 2021-11-07T00:59-05:00
1636264740000:America/New_York 2021-11-07T01:59-04:00[America/New_York]
~~SimilarLocal转换
1636261140000:America/New_York 2021-11-07T00:59-04:00[America/New_York]
----------2021-11-07T01:00-05:00
1636264800000:-05:00 2021-11-07T01:00-05:00
~~SameInstant转换
1636264800000:-05:00 2021-11-07T01:00-05:00
1636264800000:America/New_York 2021-11-07T01:00-05:00[America/New_York]
~~SimilarLocal转换
1636264800000:America/New_York 2021-11-07T01:00-05:00[America/New_York]
----------2021-11-07T01:01-05:00
1636264860000:-05:00 2021-11-07T01:01-05:00
~~SameInstant转换
1636264860000:-05:00 2021-11-07T01:01-05:00
1636264860000:America/New_York 2021-11-07T01:01-05:00[America/New_York]
~~SimilarLocal转换
1636264860000:America/New_York 2021-11-07T01:01-05:00[America/New_York]

# OffsetDateTime、ZonedDateTime的区别
  • OffsetDateTime = LocalDateTime + 偏移量ZoneOffset;ZonedDateTime = LocalDateTime + 时区ZoneId
  • OffsetDateTime可以随意设置偏移值,但ZonedDateTime无法自由设置偏移值,因为此值是由时区ZoneId控制的
  • OffsetDateTime无法支持夏令时等规则,但ZonedDateTime可以很好的处理夏令时调整
  • OffsetDateTime得益于不变性一般用于数据库存储、网络通信;而ZonedDateTime得益于其时区特性,一般在指定时区里显示时间非常方便,无需认为干预规则
  • OffsetDateTime代表一个瞬时值,而ZonedDateTime的值是不稳定的,需要在某个瞬时根据当时的规则计算出来偏移量从而确定实际值

总的来说,OffsetDateTime和ZonedDateTime的区别主要在于ZoneOffset和ZoneId的区别。如果你只是用来传递数据,请使用OffsetDateTime,若你想在特定时区里做时间显示那么请务必使用ZonedDateTime

# 示例: 解释

一个独立的日期时间类型字符串如2021-05-05T18:00-04:00它是没有任何意义的,因为没有时区无法确定它代表那个瞬间,这是理论当然也适合JSR 310类型喽。

遇到一个日期时间格式字符串,要解析它一般有这两种情况:

  1. 不带时区/偏移量的字符串:要么不理它说转换不了,要么就约定一个时区(一般用系统默认时区),使用LocalDateTime来解析
@Test
public void testParse() {
    // localDateTime
    {
        String dateTimeStrParam = "2021-05-05T18:00";
        LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStrParam);
        showDateTime(localDateTime);
    }
    // OffsetDateTime
    {
        String dateTimeStrParam = "2021-05-05T18:00-04:00";
        OffsetDateTime offsetDateTime = OffsetDateTime.parse(dateTimeStrParam);
        showDateTime(offsetDateTime);
    }
    // ZonedDateTime
    {
        String dateTimeStrParam = "2021-05-05T18:00-05:00[America/New_York]"; // ⚠️因为夏令时,纽约所在的时区(-5)被调高了一小时(-5变成了-4)
        ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateTimeStrParam);
        showDateTime(zonedDateTime);
        System.out.println(zonedDateTime.getOffset());
    }
    {
        String dateTimeStrParam = "2021-05-05T18:00+33:00[America/New_York]"; // 💡偏移量貌似可以瞎写
        ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateTimeStrParam);
        showDateTime(zonedDateTime);
        System.out.println(zonedDateTime.getOffset());
        for (ZoneOffsetTransitionRule transitionRule : zonedDateTime.getZone().getRules().getTransitionRules()) {
            System.out.println(transitionRule);
        }
    }
}
private void showDateTime(TemporalAccessor temporal) {
    Instant instant = null;
    ZoneId zoneId = null;
    if(temporal instanceof LocalDateTime) {
        // LocalDateTime仅仅是对年月日、时分秒的描述。没有存储Instant、zoneId
    } else {
        instant = Instant.from(temporal);
        zoneId = ZoneId.from(temporal);
    }
    String msg = String.format("%13d:%s %s", instant==null?0:instant.toEpochMilli(), zoneId==null?"??????":zoneId, temporal);
    System.out.println(msg);
}
            0:?????? 2021-05-05T18:00
1620252000000:-04:00 2021-05-05T18:00-04:00
1620252000000:America/New_York 2021-05-05T18:00-04:00[America/New_York]
-04:00
1620252000000:America/New_York 2021-05-05T18:00-04:00[America/New_York]
-04:00
TransitionRule[Gap -05:00 to -04:00, SUNDAY on or after MARCH 8 at 02:00 WALL, standard offset -05:00]
TransitionRule[Overlap -04:00 to -05:00, SUNDAY on or after NOVEMBER 1 at 02:00 WALL, standard offset -05:00]
# 最佳实践
  • 描述日期时间 ── LocalDateTime、LocalDate、LocalTime
  • 描述、存储时刻 ── OffsetDateTime
  • 显示日期时间 ── ZonedDateTime
  • 切换时区(OffsetDateTime to ZonedDateTime) ── toZonedDateTime()atZoneSameInstant(ZoneId)
  • 切换时区(ZonedDateTime to OffsetDateTime) ── toOffsetDateTime()withZoneSameInstant(ZoneId)

DateTimeFormatter ── 格式化

DateTimeFormatter是线程安全且final不可变的,并且内置了非常多的格式化模版实例

格式化器示例
ofLocalizedDate(dateStyle)'2021-01-03'
ofLocalizedTime(timeStyle)'10:15:30'
ofLocalizedDateTime(dateTimeStyle)'3 Jun 2021 11:05:30'
ISO_LOCAL_DATE'2021-12-03'
ISO_LOCAL_TIME'10:15:30'
ISO_LOCAL_DATE_TIME'2021-12-03T10:15:30'
ISO_OFFSET_DATE_TIME'2021-12-03T10:15:30+01:00'
ISO_ZONED_DATE_TIME'2021-12-03T10:15:30+01:00[Europe/Paris]'
@Test
public void test13() {
    System.out.println(DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.now()));
    System.out.println(DateTimeFormatter.ISO_LOCAL_TIME.format(LocalTime.now()));
    System.out.println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()));
}

输出:
2021-01-17
22:43:21.398
2021-01-17T22:43:21.4

若想自定义模式pattern,和Date一样它也可以自己指定7任意的pattern 日期/时间模式。

@Test
public void test14() {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("第Q季度 yyyy-MM-dd HH:mm:ss", Locale.US);

    // 格式化输出
    System.out.println(formatter.format(LocalDateTime.now()));

    // 解析
    String dateTimeStrParam = "第1季度 2021-01-17 22:51:32";
    LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStrParam, formatter);
    System.out.println("解析后的结果:" + localDateTime);
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骆言

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值