Java中的时间处理
Java中的时间处理
时间很简单也很复杂, 机器时间很简单, 简单到一个长整数表示的秒数.
人的时间很复杂, 涉及到各种历法, 语言, 时区的不同表示, 不同的时间标准.
编程上更涉及到数据库, 后端, 前端, 各种不同的类型转化. 本文以时间为序, 首先介绍了Java世界中的时间表示的演进, 从java.util.Date, 到JodaTime到java.time.
最终花了最大量的篇幅介绍java.time, 期间会介绍为了表示时间而引入的区域化表示(Locale), 日历(Calendar),时区(TimeZone).
开始正文之前, 先了解一个常识, 即时间戳的字符表示IOS8601标准,
最常见的表示是 DDDD分隔符TTTT, 而分隔符是就T.
此标准是时间文本表示的标准, Instance的默认表示, json中也常用, clojure中#inst reader mark中也是.
典型的例子: 2004-05-03T17:30:08+08:00表示东八区时间2004年5月3日下午5点30分8秒.
2004-05-03T17:30:08.000Z,表示标准格林尼治时间2004年5月3日下午5点30分8秒. Z的意思是零(Zero)时区.
java.util.Date
来自java1.0, 诞生于1995年
是对UNIX中的时间戳(从1970.1.1)的毫秒的简单封装, 没有时区的概念
设置了默认时区以后, 日期的字符串表示会发生对应变化, 但是对应的 #inst 其实是不会变的.
@(def date (java.util.Date.))
;; => #inst "2022-04-18T01:09:49.216-00:00"
(.setTime date 822391200000);; => nil
date
;; => #inst "1996-01-23T10:00:00.000-00:00"
(java.util.TimeZone/setDefault (java.util.TimeZone/getTimeZone "UTC"))
(.toString date)
;; => "Tue Jan 23 10:00:00 UTC 1996"
(java.util.TimeZone/setDefault (java.util.TimeZone/getTimeZone "Europe/Berlin"))
(.toString date)
;; => "Tue Jan 23 11:00:00 CET 1996"
clj-time/joda-time
鉴于1.0版本中j.u.Date设计的不合理性, 1.1版本中把大半函数标记为deprecated, 并且增加了j.u.Calendar 类.
在2002年(日韩世界杯)这一年, 巴西第五次捧杯. Joda诞生, 是当时最好的Java时间库, 也是2010年clj-time的基础.
Joda设计上, 很好的区分了机器时间和人类时间, 机器时间非常简单, 就是一个不断增加的整数, 人类时间就复杂的多, 考虑时区, 日期更变线, 适应各种日历(公历, 阴历, etc.)
后来这一设计也被很好的延续到了java.time的设计中, clj-time和joda-time更多的是作为有意义过渡, 而不是当前的最优解. 有了解就好, 不建议直接使用.
org.joda.time.Instant
机器时间, 后来也沿用成为了java.time中的Instant类
org.joda.time.DateTime
人用的时间, 后来发展为一些列的时间表示
java.time/clojure.java-time
Oracle也很快意识到时间是JDK的核心组件, 吸纳了Joda的作者Stephen Colebourne入伙, 通过JSR-310, 为JDK在 1.8 版本中增加了java.time.
clojure社区用一个新的库 java-time(区别于java.time)来封装java.time. java.time 在带来强大功能的同时, 也带来了一定的复杂性, 相比于以前单一的
j.u.Date 类, 我们引入了一些列的新时间相关类型:
时间的单位
(import '[java.time.temporal ChronoUnit])
(->> (clojure.reflect/reflect ChronoUnit)
:members
(filter #(= #{:enum :public :static :final} (:flags %)))
(mapv :name))
;; => [MILLIS MINUTES MICROS HALF_DAYS MILLENNIA YEARS DECADES DAYS CENTURIES WEEKS HOURS ERAS SECONDS MONTHS NANOS FOREVER]
java.time中时间类对照表
这个表总结了时间相关的各种类/以及枚举类型的表示范围 [年月日时分秒时区]
以及他们的字符串表示
Class or Enum Year Month Day Hours Minutes Seconds\* Zone Offset Zone ID toString Output
---------------- ------ ------- ------ ------- --------- ----------- ------------- --------- ---------------------------------------------
Instant Y 2013-08-20T15:16:26.355Z
LocalDate Y Y Y 8/20/2013
LocalDateTime Y Y Y Y Y Y 2013-08-20T08:16:26.937
ZonedDateTime Y Y Y Y Y Y Y Y 2013-08-21T00:16:26.941+09:00\[Asia/Tokyo\]
LocalTime Y Y Y 16:26.9
MonthDay Y Y --08-20
Year Y 2013
YearMonth Y Y 2013-08
Month Y AUGUST
OffsetDateTime Y Y Y Y Y Y Y 2013-08-20T08:16:26.954-07:00
OffsetTime Y Y Y Y 08:16:26.957-07:00
Duration \*\* \*\* \*\* Y PT20H (20 hours)
Period Y Y Y **\*** **\*** P10D (10 days)
时间相关的类的使用场景
要表示某人的生日, LocalDate足够了. 如果要进一步追溯出生时间, LocalDateTime则更适用.
需要注意的是Local开头的时间和日期由于不带时区属性, 其实是无法确定具体时间的, 好比说"早上8点下陨石雨", 我们不知道是美国还是中国的8点.
鉴于这种情况,Instant/ZonedDateTime/OffsetDateTime就用用场了.
比如说要和国外同事约定会议时间, 我们需要指定发起会议的具体时区+时间
如果是需要比较时间先后的时间戳, Instant非常合适.
实现简单(就是一个大整数). 但是如果要对应到可读的时间格式, 日月年星期闰年闰月日期变更线等逻辑, OffsetDateTime和ZonedDateTime就排上用场了.
时间间隔Duration和Period
在我学习的过程中, Instant, Duration, Period是比较容易感觉迷惑的
Instant 精确到纳秒ns的机器时间表示
Duration 只存储到秒(或者纳秒)的时间差, 不包含日期, 小时, 分钟之类的单位, 是Instant的好伙伴
(import '[java.time Instant Duration]) (let [t1 (Instant/EPOCH) t2 (Instant/now) d (Duration/between t1 t2)] [d (.toNanos d)]) ;; => [#object[java.time.Duration 0x10a52344 "PT459959H2M12.2068494S"] ;; 1655852532206849400]Period 则提供了丰富的时间函数, 来计算某段时间的年月日,时分秒. 主要用于人类可以理解的时间范围. 比如说计算某个人的年龄
(import '[java.time Month LocalDate Period] '[java.time.temporal ChronoUnit]) (let [birthday (LocalDate/of 1990 Month/APRIL 1) today (LocalDate/now) period (Period/between birthday today)] (format "此人%d岁又%d月零%d天, 一共%d天" (.getYears period) (.getMonths period) (.getDays period) (.between (ChronoUnit/DAYS) birthday today)))
Month & DayofWeek
是两个枚举类型, 人类可读时间的基本单位, 鉴于语言地域的不同, 我们提供给人的可读格式要相应变化, 所以在这里我们也一并了解下Locale类的用法
(import [java.time DayOfWeek Month]
[java.time.format TextStyle]
[java.util Locale])
(.plus DayOfWeek/MONDAY 3)
;; => #object[java.time.DayOfWeek 0x4924f4d0 "THURSDAY"]
@(def monday (DayOfWeek/MONDAY))
;; => #object[java.time.DayOfWeek 0x5ae9dc27 "MONDAY"]
@(def ^:dynamic *locale* (Locale/getDefault))
;; => #object[java.util.Locale 0x1742fee3 "en_CN"]
;; Locale 的格式如下
;; languageCode_countryCode
;; 英文语言_中国地区
(Locale/CHINESE)
;; => #object[java.util.Locale 0x38fcfb5a "zh"]
(Locale/CHINA)
;; => #object[java.util.Locale 0x7ed8cd66 "zh_CN"]
(Locale/ENGLISH)
;; => #object[java.util.Locale 0x7b1dfda5 "en"]
(Locale/US)
;; => #object[java.util.Locale 0x6d5ceca9 "en_US"]
(Locale/forLanguageTag "en-US")
;; => #object[java.util.Locale 0x6d5ceca9 "en_US"]
(.toLanguageTag (Locale/forLanguageTag "en-US"))
;; => "en-US"
(binding [*locale* (Locale/CHINA)]
[(.getDisplayName monday TextStyle/FULL *locale*)
(.getDisplayName monday TextStyle/NARROW *locale*)
(.getDisplayName monday TextStyle/SHORT *locale*) ])
;; => ["星期一" "一" "周一"]
(binding [*locale* (Locale/US)]
[(.getDisplayName monday TextStyle/FULL *locale*)
(.getDisplayName monday TextStyle/NARROW *locale*)
(.getDisplayName monday TextStyle/SHORT *locale*) ])
;; => ["Monday" "M" "Mon"]
@(def month Month/AUGUST)
;; => #object[java.time.Month 0x156f73f9 "AUGUST"]
(binding [*locale* (Locale/US)]
[(.getDisplayName month TextStyle/FULL *locale*)
(.getDisplayName month TextStyle/NARROW *locale*)
(.getDisplayName month TextStyle/SHORT *locale*) ])
;; => ["August" "A" "Aug"]
(binding [*locale* (Locale/CHINA)]
[(.getDisplayName month TextStyle/FULL *locale*)
(.getDisplayName month TextStyle/NARROW *locale*)
(.getDisplayName month TextStyle/SHORT *locale*) ])
;; => ["八月" "8" "8月"]
LocalDate, YearMonth, MonthDay and Year
本地日期, 但凡是Local开头的, 统统不考虑时区(时差)
(import [java.time LocalDate Year YearMonth MonthDay Month DayOfWeek]
[java.time.temporal TemporalAdjusters])
@(def date (LocalDate/of 2000, Month/NOVEMBER, 20))
;; => #object[java.time.LocalDate 0x46322eb9 "2000-11-20"]
@(def next-wed (.with date (TemporalAdjusters/next DayOfWeek/WEDNESDAY)))
;; => #object[java.time.LocalDate 0x343b1627 "2000-11-22"]
@(def year-month (YearMonth/now))
;; => #object[java.time.YearMonth 0x7818dc79 "2022-04"]
(.lengthOfMonth year-month)
;; => 30
@(def year-month2 (YearMonth/of 2021 Month/FEBRUARY))
;; => #object[java.time.YearMonth 0x5d010d5e "2021-02"]
(.lengthOfMonth year-month2)
;; => 28
(def month-day (MonthDay/of Month/FEBRUARY 29))
;; => #object[java.time.MonthDay 0x7044b8ab "--02-29"]
(.isValidYear month-day 2021)
;; => false
(.isLeap (Year/of 2021))
;; => false
LocalTime
本地时间
(import [java.time LocalTime LocalDateTime Instant ZoneId])
@(def this-sec (LocalTime/now))
;; => #object[java.time.LocalTime 0x28b53109 "16:05:52.570370"]
(format "%d:%d:%d" (.getHour this-sec) (.getMinute this-sec) (.getSecond this-sec))
;; => "16:6:47"
@(def date-time (LocalDateTime/now))
;; => #object[java.time.LocalDateTime 0x612f0070 "2022-04-24T17:15:34.855218"]
(LocalDateTime/of 1994, Month/APRIL, 15, 11, 30)
;; => #object[java.time.LocalDateTime 0x33133bea "1994-04-15T11:30"]
(LocalDateTime/ofInstant (Instant/now) (ZoneId/systemDefault))
;; => #object[java.time.LocalDateTime 0x1d444024 "2022-04-24T17:19:41.901675"]
(.plusMonths (LocalDateTime/now) 6)
;; => #object[java.time.LocalDateTime 0x4d11b3f4 "2022-10-24T17:20:34.461093"]
(.minusMonths (LocalDateTime/now) 6)
;; => #object[java.time.LocalDateTime 0x19397ada "2021-10-24T17:20:51.273925"]
ZonedDateTime, OffsetDateTime & OffsetTime
地球是圆的, 所以有时区, 所以我们的计时需要有时区和时差, 所以Java Time中有时区ID(ZoneId)和时区时差(ZoneOffset)
(import [java.time ZonedDateTime LocalDateTime ZoneId])
;; 系统中一共有601个时区id, 之所以有这么多,是因为虽然时区只有24个
;; 但是大城市,国家多,一个东八区, 光国内就有北京, 上海, 香港, 乌鲁木齐等多个id
(count (ZoneId/getAvailableZoneIds))
;; => 601
;; 头10个id的样子
@(def zone-ids (take 10 (ZoneId/getAvailableZoneIds)))
;; => ("Asia/Aden" "America/Cuiaba" "Etc/GMT+9" "Etc/GMT+8" "Africa/Nairobi" "America/Marigot" "Asia/Aqtau" "Pacific/Kwajalein" "America/El_Salvador" "Asia/Pontianak")
@(def dt (LocalDateTime/now))
;; => #object[java.time.LocalDateTime 0x2fce20f6 "2022-04-24T17:22:57.174683"]
(->> zone-ids
(mapv (fn [id-str]
[id-str (ZoneId/of id-str)]))
(mapv (fn [[id-str id]]
[id-str (.atZone dt id)]))
(mapv (fn [[id-str zdt]]
[id-str (.getOffset zdt)]))
(mapv (fn [[id-str offset]]
[(format "%25s" id-str)
(format "%10s" offset)
(format "%10s"
(/ (.getTotalSeconds offset)
(* 60 60)))])))
;; => [[" Asia/Aden" " +03:00" " 3"]
;; [" America/Cuiaba" " -04:00" " -4"]
;; [" Etc/GMT+9" " -09:00" " -9"]
;; [" Etc/GMT+8" " -08:00" " -8"]
;; [" Africa/Nairobi" " +03:00" " 3"]
;; [" America/Marigot" " -04:00" " -4"]
;; [" Asia/Aqtau" " +05:00" " 5"]
;; [" Pacific/Kwajalein" " +12:00" " 12"]
;; [" America/El_Salvador" " -06:00" " -6"]
;; [" Asia/Pontianak" " +07:00" " 7"]]
;; ZonedDatetime 就是localDatetime + ZoneID
;; 假如说我北京 2022/30 9:30起飞
;; 起飞, 飞行14个小时到达美国纽约
;; 我们使用ZonedDateTime来模拟一下这个过程
(let [leaving (LocalDateTime/of 2020 Month/JUNE 30 9 30)
leaving-zoneID (ZoneId/of "Asia/Shanghai")
departure-time (ZonedDateTime/of leaving leaving-zoneID)
arriving-zoneID (ZoneId/of "America/Los_Angeles")
arriving-time (.plusHours
(.withZoneSameInstant departure-time arriving-zoneID)
15)]
[departure-time arriving-time])
;; => [#object[java.time.ZonedDateTime 0x57361e8a "2020-06-30T09:30+08:00[Asia/Shanghai]"]
;; #object[java.time.ZonedDateTime 0x1fc7f3 "2020-06-30T09:30-07:00[America/Los_Angeles]"]]
OffSet DateTime 和zoned DateTime非常类似, 只不过时区的指定方式不是用ID,
二十直接指定和UTC时间的时差(offset) 我国就处于东八区 +8:00
(import '[java.time OffsetDateTime ZoneOffset LocalDateTime])
(let [datetime (LocalDateTime/of 2020 Month/JUNE 20 0 0)
offset (ZoneOffset/of "+08:00")]
(OffsetDateTime/of datetime offset))
;; => #object[java.time.OffsetDateTime 0x23fb2692 "2020-06-20T09:59+08:00"]
Instant
计算机时间, 以从1970年1月1号零点开始的秒数
Instant的toString函数以ISO-8601标准输出字符串, 即:
2013-05-30T23:38:23.085Z
(import '[java.time Instant]
'[java.time.temporal ChronoUnit])
;; 提供了一些列的方便函数,
;; 时间增减
(.plus (Instant/now) 1 ChronoUnit/HOURS)
;; => #object[java.time.Instant 0x2a2707d1 "2022-06-22T08:33:15.587093300Z"]
;; 从1970年到现在过去了多少个小时
(.until (Instant/ofEpochSecond 0)
(Instant/now)
ChronoUnit/HOURS)
The Instant class does not work with human units of time, such as years, months, or days. If you want to perform calculations in those units, you
can convert an Instant to another class, such as LocalDateTime or ZonedDateTime, by binding the Instant with a time zone. You can then
access the value in the desired units. The following code converts an Instant to a LocalDateTime object using the ofInstant method and the
default time zone, and then prints out the date and time in a more readable form:
;; Instant是机器时间, 如果要人能读得懂的月日年, 需要转化成带时区的时间
(let [now (Instant/now)
local-datetime (LocalDateTime/ofInstant now (ZoneId/systemDefault))]
(format "%s %d %d at %d:%d%n"
(.getMonth local-datetime)
(.getDayOfMonth local-datetime)
(.getYear local-datetime)
(.getHour local-datetime)
(.getMinute local-datetime)
))
;; => "JUNE 22 2022 at 9:40\r\n"
Parsing 和 Formatting
Java1.x中的工具 DateTimeFormatter 依旧可以用使用, 而且可以自由扩展
(import '[java.time.format DateTimeFormatter]
'[java.time.LocalDate])
(LocalDate/parse "20200911" DateTimeFormatter/BASIC_ISO_DATE)
;; => #object[java.time.LocalDate 0x3010993e "2020-09-11"]
;; 自定义formatter
(let [in "2022/01/01"
formatter (DateTimeFormatter/ofPattern "yyyy/mm/DD")]
(LocalDate/parse in formatter))
;; => #object[java.time.LocalDate 0x6a214f56 "2022-01-01"]