详解 Java 日期与时间修改

Java
219
0
0
2023-12-11

详解 Java 日期与时间修改

最近在家办工接到的一项工作是和时区有关的,据用户反馈,由于美国的 Puerto Rico 州不使用 夏令时 ,在其他州施行夏令时时,这个州的用户选不到适合自己的时区,导致时间无法正确显示。最终笔者为软件添加了太平洋标准时区解决了这个问题。

时区、夏令时、标准时间…日期和时间是计算机处理的重要数据,在绝大多数软件程序中,我们都要和日期和时间打交道。本篇文章我们将系统地学习 Java 对日期和时间的处理。(本文参考了 廖雪峰 Java 教程-日期和时间(点击链接查看) 文章中的资料,事实上,笔者并不认为本文比廖大佬的文章更好,有时间的读者可以直接阅读原教程。)

一、时区

​地球人都知道,我们地球是自西向东自转的,所以东边会比西边早看到太阳,东边的时间也总比西边的快。如果全球采用统一的时间,比如都用北京时间,会产生什么问题呢?

当正午十二点的太阳照射到北京时,身处地球另一面的纽约还是漆黑一片。对于纽约来说,日常作息时间就成了晚上九点开始上班,因为那时太阳刚刚升起;所有纽约人都上班到第二天早上六点下班,因为那时太阳刚刚落下。

虽然对于长期居住在一个地方的人来说,他可以适应自己本地的作息时间,但当他去其他地方旅游或是与其他地方的人交流时,就必须查询当地的作息时间,这会带来很大的不便。

详解 Java 日期与时间修改

于是,在 1879 年,加拿大铁路工程师弗莱明首次提出全世界按统一标准划分“时区”。1884 年华盛顿子午线国际会议正式通过采纳这种时区划分,称为世界标准时制度。

时区划分的初衷是 尽量使中午贴近太阳上中天的时间 ,从此以后,各地的时间经过换算,都能统一地早上六点起床,中午十二点午餐,晚上六点下班。

详解 Java 日期与时间修改

全球共分为 24 个时区,所以每个时区占 15˚ 经度。 理论时区 以能被 15 整除的经线为中心,向东西两侧延伸 7.5˚。国际规定经过英国格林威治天文台的那一条经线为 0˚ 经线,这条经线也被称作 本初子午线 。选择格林威治既是因为当初“日不落帝国”的强大,也是由于格林威治常年提供准确的航海观测数据,19 世纪晚期,72% 的世界贸易都依靠以格林威治作为本初子午线的航海图表。

为了避开国界线,有的时区的形状并不规则,而是比较大的国家以国家内部行政分界线为时区界线,这是 实际时区 ,也称为 法定时区

详解 Java 日期与时间修改

身处地球的不同地区,时间可能是不同的,所以光靠时间我们无法确定一个时刻,要确定一个时刻必须要带上时区。

详解 Java 日期与时间修改

表示时区有两种常见的写法,最常见的是 GMT ,它的全称是 Greenwich Mean Time,意思是格林威治标准时间,世界各地根据东西偏移量计算时区。比如,北京位于 东八区 ,记做 GMT+8,纽约位于西五区,记做 GMT-5。

还有一种写法是 UTC ,它的全称是 Coordinated Universal Time,意思是协调世界时,如果时间以 UTC 表示,则在时间后面直接加上一个“Z”(不加空格),“Z”是协调世界时中 0 时区的标志。比如,“09:30 UTC” 写作 “09:30Z” 或是 “0930Z”。“14:45:15 UTC” 写作 “14:45:15Z” 或 “144515Z”。因为在北约音标字母中用 “Zulu” 表示 “Z”,所以 UTC 时间也被称做祖鲁时间。

GMT 和 UTC 基本一样,只不过 UTC 使用更加精确的原子钟计时,每隔几年会有一个闰秒。但我们无需关注两者的差别,计算机在联网时会自动与时间服务器同步时间。

计算不同时区的时间差很简单,我们平时常用的北京时间位于东八区,即:GMT+8,它的值是在 GMT 的基础上增加了 8 小时,纽约位于西五区,即:GMT-5,它的值是在 GMT 的基础上减少了 5 小时。所以北京时间通常比纽约时间快 13 个小时。

我们现在知道,每往西越过一个时区,时间便提前一小时。据此我们来思考一个有趣的问题:如果我们一直往西,以每小时一个时区的速度前进,时间是否会静止呢?

1.比如我们从北京出发,此时时间是 2020-2-11 8:00 GMT+8

2.当我们花费一个小时,走到 东七区 时,时间是 2020-2-11 8:00 GMT+7

3.当我们走到本初子午线时,时间是 2020-2-11 8:00 GMT

4.当我们走到西五区时,时间是 2020-2-11 8:00 GMT-5

……

我们都知道地球是个球体,当我们绕地球一圈回到北京时,如果时间还是 2020-2-11 8:00 GMT+8,岂不是时间真的静止了?进一步思考,如果我们以半小时一个时区的速度向西前进,岂不是时间还会倒流?

常识告诉我们,时间是不可能静止也不可能倒流的。那么这里的问题出在哪里呢?问题就出在东西时区的交界处。上文说到,地球分为 24 个时区,包括标准时区、东一区~东十二区、西一区~西十二区。实际上,东十二区和西十二区是同一时区。

从 0˚ 经线开始,每往西跨一个时区时间便减少 1 小时,每往东跨一个时区便增加 1 小时。如此一来,到了另一端 180˚ 经线时,就会有 24 小时的落差,为了平衡这一落差,人们规定由西向东越过此线日期需减少一天,由东向西越过此线时日期需增加一天。故而这一条线被称之为 国际日期变更线 ,也叫 换日线 ,它位于本初子午线的另一面。和时区界限类似,为了避开国界线,换日线并不与 180˚ 经线重合,换日线实际上是不规则的。

详解 Java 日期与时间修改

如果我们接着走下去:

5.当我们走到东 / 西十二区时,时间是 2020-2-11 8:00 GMT±12

6.我们越过国际换日线,日期增加一天,时间是 2020-2-12 8:00 GMT±12

7.当我们走到东十一区时,时间是 2020-2-12 8:00 GMT+11

8.当我们回到北京时,时间是 2020-2-12 8:00 GMT+8

此时,我们的环球之旅刚好用了 24 小时。

再来看一下如果我们以每半小时一个时区的速度向西行走,时间为什么不会逆流:

1.我们还是从北京出发,此时时间是 2020-2-11 8:00 GMT+8

2.当我们花费半小时,走到东七区时,时间是 2020-2-11 7:30 GMT+7

3.当我们走到本初子午线时,时间是 2020-2-11 4:00 GMT

4.当我们走到西五区时,时间是 2020-2-11 1:30 GMT-5

5.当我们走到东 / 西十二区时,时间是 2020-2-10 22:00 GMT±12

6.我们越过国际换日线,日期增加一天,时间是 2020-2-11 22:00 GMT±12

7.当我们走到东十一区时,时间是 2020-2-11 21:30 GMT+11

8.当我们回到北京时,时间是 2020-2-11 20:00 GMT+8

此时,我们的环球之旅刚好用了 12 小时。

二、夏令时

​由于夏季和冬季白昼时间不一致,部分国家施行了夏令时制度,目的是让人们根据白昼时间来调整作息。

施行夏令时使得人们可以尽量在白天工作,从而减少照明,节省电能。但夏令时也带来了很多的不便,如夏令时开始和结束时,人们不得不调整睡眠时间;夏令时也使得时间计算变得复杂,在夏令时结束的当天,某些时间会出现两次,容易造成交通、生产、会议安排等时间的混乱。中国曾经施行过一段时间夏令时,在 1992 年就被废除了,而美国大部分地区现在还在使用夏令时。

美国使用夏令时时,纽约时间按照西四区计算,即:GMT-4。这段时间北京时间比纽约时间快 12 个小时,夏令时结束后,纽约时间又恢复到西五区 GMT-5。

由于各国规定有所差异,所以夏令时计算非常复杂。当我们需要计算夏令时时,应尽量使用 Java 库提供的类,避免自己计算夏令时。

三、旧 API

Java 标准库提供了两套关于时间和日期的 API:

  • 旧 API :位于 java.util 包中,里面主要有 Date、Calendar、TimeZone 类
  • 新 API :位于 java.time 包中,里面主要有 LocalDateTime、ZonedDateTime、ZoneId 类

有两套 API 的原因是旧 API 在设计时没有考虑好时区问题,常量设计也有些不合理,导致使用起来不够方便。新 API 很好地解决了这些问题。我们在开发时,除非维护老代码,其他时候都应该尽量使用新 API。


3.1. Date

Date 类用于存储日期和时间,查看其源码可以发现,它保存了一个 long 类型的时间戳。时间戳是指格林威治时间从 1970 年 1 月 1 日零点到此刻经历的秒数或毫秒数。

详解 Java 日期与时间修改

Date 的基本用法如下:

详解 Java 日期与时间修改

Date 在使用时有几个缺点:

  • 每次获取年份、月份都需要转换
  • 只能获取当前时区的时间,无法设置时区
  • 无法加减日期和时间
  • 无法计算某个月的第几个星期几


3.2. SimpleDateFormat

默认输出的时间 字符串 的格式通常不能满足我们的要求,所以我们需要用 SimpleDateFormat 来格式化输出,它使用一些预定义的字符串表示格式化,较常用的字符串有:

  • y:年
  • M:月
  • d:日
  • H:小时
  • m:分钟
  • s:秒
  • S:毫秒
  • a:上午 / 下午
  • E:星期
  • z:时区

附:Java 官网文档中给出的预定义字符串表格

详解 Java 日期与时间修改

SimpleDateFormat 的使用:

详解 Java 日期与时间修改

这里的时区信息输出为 CST,表示 China Standard Time,也就是中国标准时间。

SimpleDateFormat 会根据预定义字符的长度打印不同长度的信息。以 M 为例:

  • M:输出 2
  • MM:输出 02
  • MMM:输出 2月
  • MMMM:输出 二月

如果预定义字符串的长度短于需要输出的信息,这时 Java 会输出 能包含全部信息的最短字符串,也就是说 Java 不会丢弃任何信息,如上例中只用了一个 y,仍然输出了 2020,并不会只输出一个 2。

我们来发挥一下极客精神,探索一下预定义字符串过长 Java 会怎么处理:

详解 Java 日期与时间修改

本例中,每个预定义字符的长度都为 10,可以看到,系统对年、日、时、分、秒、毫秒的处理是用前置 0 补足位数,对月份、上午 / 下午、星期、时区的处理是输出全中文。

SimpleDateFormat 可以设置时区,我们可以用 SimpleDateFormat 把 Date 获取的时间转换为其他时区显示出来:

详解 Java 日期与时间修改

3.3. Calendar

旧 API 中,为了加减日期和时间,Java 提供了 Calendar 类。

Calendar 的基本使用:

详解 Java 日期与时间修改

Calendar 修复了 Date 获取年份时必须 + 1900 的问题,但月份仍然使用 0~11 表示 1~12 月,星期采用 1~7 表示周日~周六。虽然咱们程序员都从 0 开始计数,但日期和时间一般都是要展示给用户看的,每次显示时都要转换实在是太不方便了,这也是需要新 API 的原因之一。

Calendar 提供的日期和时间的加减功能使用如下:

详解 Java 日期与时间修改

使用日期加减时有一点需要特别注意,我们来看一个例子:

详解 Java 日期与时间修改

我们将 12 月 31 日减去 1 个月,再加上 1 个月,日期变成了 12 月 30 日!这是因为 11 月 没有 31 日,所以 12 月 31 日减去 1 个月时, Calendar 会自动将日期调整到 11 月 30 日,再加 1 个月,就变成了 12 月 30 日。也就是说 Calendar 加减时,会根据月份自动调整日期。

上文介绍 Date 时我们说到,单靠 Date 和 SimpleDateFormat 只能把本地时区的时间用其他时区显示出来,无法自由的实现时区的转换,比如我们身在中国,无法把纽约时间 GMT-5 转换为东京时间 GMT+9。但 Calendar 是可以设置时区的,所以我们现在有了一种间接转换任意时区的方法:

详解 Java 日期与时间修改

实际转换过程为:Calendar 保存的 纽约时间先转换成 Date 保存的 北京时间,再用 SimpleDateFormat 将 Date 转换成 东京时间展示出来。

上例中还可以看到,Calendar 使用 set 方法设置指定时间,除了此例中的一次性全部指定的方式外,也可以单个指定:

详解 Java 日期与时间修改

四、新 API

由于旧 API 存在的诸多不便,从 Java 8 开始,jaca.time 包提供了一套新的日期和时间的 API。主要有 LocalDateTime、ZonedDateTime、Instant、ZoneId、Duration、DateTimeFormatter。

新 API 不仅使用更方便,而且修正了 旧 API 中不合理的常量设计:

  • 新 API 中,Month 取值范围变成:1~12,表示 1~12月
  • 新 API 中,Week 取值范围变成:1~7,表示周一~周日
4.1. LocalDateTime

LocalDateTime 用来代替 Date 和 Calendar,LocalDateTime 的基本用法如下:

详解 Java 日期与时间修改

LocalDateTime 使用 now() 函数获取当前日期和时间,输出时严格按照 ISO 8601 格式打印。ISO 8601 是国际标准化组织的日期和时间的表示方法,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》,ISO 8601 规定使用 T 分隔日期和时间。标准格式如下:

  • 日期:yyyy-MM-dd
  • 时间:HH:mm:ss
  • 带毫秒的时间:HH:mm:ss.SSS
  • 日期和时间:yyyy-MM-dd’T’HH:mm:ss
  • 带毫秒的日期和时间:yyyy-MM-dd’T’HH:mm:ss.SSS

我们可以通过 parse() 函数解析一个符合 ISO 8601 格式的字符串,创建出 LocalDateTime:

详解 Java 日期与时间修改

除此之外,我们还可以通过 of() 函数指定日期和时间创建 LocalDateTime:

详解 Java 日期与时间修改

LocalDateTime 存储了当前的日期信息和时间信息,如果我们只需要当前日期或当前时间,可以使用 LocalDate 和 LocalTime:

详解 Java 日期与时间修改

同 LocalDateTime 类一样,LocalDate 和 LocalTime 类也可以通过 now()、parse()、of() 方法创建。

LocalDateTime 在加减日期时,可以采用简洁的链式调用:

详解 Java 日期与时间修改

和 Calendar 一样,LocalDateTime 在加减时,仍然会自动调整日期:

详解 Java 日期与时间修改

与 Calendar 不同的是,LocalDateTime 是不可变类,如此例中调用 minusMonths(1) 和 plusMonths(1) 方法后,dt 的值并没有改变,这个函数返回的是一个调整后的新值,我们将这个新值赋值给了 dt2。

对应 Calendar 的 set() 方法,LocalDateTime 调整时间使用 withXxx() 方法:

  • 调整年:withYear()
  • 调整月:withMonth()
  • 调整日:withDayOfMonth()
  • 调整时:withHour()
  • 调整分:withMinute()
  • 调整秒:withSecond()

详解 Java 日期与时间修改

LocalDateTime 还有一个 with() 方法允许我们做更复杂的运算:

详解 Java 日期与时间修改

要比较两个日期的先后,可以使用 LocalDateTime 的 isBefore()、isAfter() 方法:

详解 Java 日期与时间修改

4.2. ZonedDateTime

LocalDateTime 和 Date 类一样,总是表示本地时区的时间,如果要表示带时区的时间,需要使用 ZonedDateTime,它相当于 LocalDateTime + ZoneId。LocalDateTime 提供的方法,如 now()、of()、plusDays() 等,ZonedDateTime 也都提供。

ZonedDateTime 的使用:

详解 Java 日期与时间修改

ZonedDateTime 通过 now() 函数获取当前时区的时间,通过 now(ZoneId zone) 函数获取指定时区的时间。这样获取到的两个时间虽然时区不同,但表示的都是同一时刻(毫秒数不同是由于执行代码会花费一点时间)。

通过给 LocalDateTime 设置 ZoneId,也可以创建出 ZonedDateTime:

详解 Java 日期与时间修改

通过这种方式创建的 ZonedDateTime 日期和时间一样,但时区不同,所以表示的是两个不同时刻。

ZonedDateTime 可以通过 toLocalDateTime() 函数转换成 LocalDateTime:

详解 Java 日期与时间修改

我们看到,ZonedDateTime 转换成 LocalDateTime 时,不会自动切换成本地时区的时间,而是直接丢弃时区信息。

由于 ZonedDateTime 自带时区信息,所以在涉及时区转换时使用 ZonedDateTime 非常方便。如上文中提到的将纽约时间转换成的东京时间,使用 ZonedDateTime 实现如下:

详解 Java 日期与时间修改

4.3. DateTimeFormatter

上文已经说到,DateTimeFormatter 是用来代替 SimpleDateFormat 的。与 SimpleDateFormat 相比,DateTimeFormatter 的一个明显优势在于它是 线程安全 的。SimpleDateFormat 由于不是线程安全的,使用时只能在方法内部创建新的局部变量,而 DateTimeFormatter 可以只创建一个实例。

DataTimeFormat 预定义的字符串和 SimpleDateFormat 一模一样,来看下 DateTimeFormatter 的基本使用:

详解 Java 日期与时间修改

还记得 LocalDateTime 的 parse() 方法吗?我们查看一下它的源码:

详解 Java 日期与时间修改

从源码中我们看到,parse() 方法可以传入两个参数,第二个参数就是一个 DateTimeFormatter,也就是说不仅 ISO 8601 标准格式的字符串可以被解析,我们完全可以自定义被解析的字符串格式。

详解 Java 日期与时间修改

DataTimeFormatter 的 ofPattern() 方法还可以传入一个 Locale 参数,这个参数的作用是使用当地的习惯来格式化时间:

详解 Java 日期与时间修改


4.4. Instant

在新 API 中,使用 Instant 表示时间戳,它类似于 System.currentTimeMillis()。Instant 使用如下:

详解 Java 日期与时间修改

给 Instant 加上一个时区,就可以创建出 ZonedDateTime:

详解 Java 日期与时间修改

五、新旧 API 的转换

旧 API 转新 API 可以通过 toInstant() 方法转换为 Instant,再由 Instant 转换成 ZonedDateTime:

详解 Java 日期与时间修改

新 API 转旧 API 时,需要借助 long 类型时间戳实现:

详解 Java 日期与时间修改

以上,就是 Java 日期和时间的全部内容了,有任何收获或疑问欢迎在评论区与大家一起讨论交流。