當時是 2018 年吧,筆者在前公司從零開發新產品,至今對於日期時間一律都是使用「java.util.Date」類別來處理。若想做增減,還要搭配 Apache 提供的 DateUtils 來完成。
後來才在網路上得知如「LocalDateTime」這種 Java 8 推出的類別。這兩天就來介紹它們。除了了解操作方式,也要知道如何在 Spring Boot 的 controller 接收它們。
此篇亦轉載到個人部落格。
在建立 Java 的 Date
物件時,透過建構子能傳入 long
整數,直接定義它的時間。這個整數,我們稱之為時間戳(timestamp)。這個 timestamp,是從 UTC 時間 1970-01-01 00:00:00
開始計算,通常以秒(second)或毫秒(millisecond)為單位,不因時區而有差別。而 Date
的建構子是接收毫秒。
Unix Time Stamp 這個網站可看到目前的 timestamp,也能將其與日期時間互換。
附帶一提,在表示時區時,常會看到 GMT 或 UTC 的字眼。這兩者測量時間的方式不同,但由於差距不到一秒,因此在生活中混用是沒有什麼大問題。
Ref:雜智社GMT?UTC?到底有什麼差別:時間的故事下集
Date
所儲存的是 timestamp,同時代表了日期與時間。若某些情境只需要其中一個,便會形成多餘資料。例如想要紀錄出生年月日是 1996-01-01
,則 Date
物件會被表示成類似 1996-01-01 00:00:00
的樣子,明明我們不需要時分秒的。
Date
可透過呼叫如 setYear
、setHour
等方法,來改變該物件所代表的 timestamp。若該物件被傳遞到其他程式,意外地遭到修改,這其實是不安全的。
Date
本身帶有如 getYear
、getMonth
等方法,來單獨取出年、月等資訊,但由於被 deprecated 了,我們就不討論。
透過 Calendar
可以幫忙做這件事,用法如下。
var calendar = Calendar.getInstance();
// calendar.setTime(new Date()); // optional
System.out.println(calendar.get(Calendar.MONTH)); // 7
System.out.println(calendar.get(Calendar.DAY_OF_MONTH)); // 25
System.out.println(calendar.get(Calendar.HOUR)); // 11
System.out.println(calendar.get(Calendar.MINUTE)); // 49
Calendar
並未提供如 getYear
之類的方法,而是呼叫 get
再自行傳入參數,較麻煩。
還有一點是月份值是從 0 開始。意即在 8 月呼叫 calendar.get(Calendar.MONTH)
,反而會得到 7。
Date
本身沒有沒有時區的概念,畢竟儲存的 timestamp,不因時區而有差異。即便呼叫 toString
的結果會因時區而有不同,但那是從電腦系統取得時區的緣故。
若要進行時區處理,需透過 SimpleDateFormat
的輔助。
var now = new Date();
// 目前時區
var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(now)); // 2023-08-25 11:22:39
// 變換時區
sdf.setTimeZone(TimeZone.getTimeZone("America/NewYork"));
System.out.println(sdf.format(now)); // 2023-08-25 03:22:39
SimpleDateFormat
可以將日期時間的字串與 Date
物件互相轉換。然而在呼叫相關的 format
與 parse
方法時,會有執行緒不安全的問題。
下面整理一些解法:
SimpleDateFormat
物件。但這會產生額外的效能開銷。synchronized
關鍵字,限制同時間只有一個執行緒可用。然而在 server 上會導致 request 被阻塞。Ref:
一文告訴你Java日期時間API到底有多爛
SimpleDateFormat線程不安全及解決辦法
看完了 java.util.Date
的缺點,接下來讓我們認識 Java 8 新推出的日期時間套件。它們位於 java.time
的 package 中,概念上都是對 timestamp 值進行封裝,並提供一些方法呼叫。
代表日期,只儲存了年、月、日。
var date1 = LocalDate.now();
System.out.println(date1); // 2023-08-25
var date2 = LocalDate.of(2018, 8, 1);
date2 = date2.plusDays(10); // 增加 10 天
System.out.println(date2.getYear()); // 2018
System.out.println(date2.getMonth()); // AUGUST
System.out.println(date2.getMonthValue()); // 8
System.out.println(date2.getDayOfMonth()); // 11
要注意的是,該物件儲存的日期是不可變的(immutable),因此進行日期的增減後,要將回傳值接起來。
代表時間,除了儲存時、分、秒,也能儲存奈秒。
var time1 = LocalTime.now();
System.out.println(time1); // 12:39:53.561241100
var time2 = LocalTime.of(12, 30, 15);
time2 = time2.minusSeconds(10); // 減少 10 秒
System.out.println(time2.getHour()); // 12
System.out.println(time2.getMinute()); // 30
System.out.println(time2.getSecond()); // 5
封裝了上述的 LocalDate
和 LocalTime
。
var dateTime1 = LocalDateTime.now();
System.out.println(dateTime1); // 2023-08-25T12:53:55.822241500
var dateTime2 = LocalDateTime.of(2023, 8, 24, 12, 30, 15);
dateTime2 = dateTime2.plusDays(180).minusMinutes(5);
System.out.println(dateTime2.getYear()); // 2024
System.out.println(dateTime2.getMonthValue()); // 2
System.out.println(dateTime2.getDayOfMonth()); // 20
System.out.println(dateTime2.getMinute()); // 25
DateTimeFormatter
可用來進行日期時間物件,與字串之間的互換。而且不必像舊的 SimpleDateFormat
那樣,還要處理 ParseException
這個 checked exception。
var formatter = DateTimeFormatter.ofPattern("yyyyMMdd HHmmss");
var dateTime = LocalDateTime.parse("20230824 123015", formatter);
System.out.println(dateTime); // 2023-08-24T12:30:15
System.out.println(formatter.format(dateTime)); // 20230824 123015
Ref:Java 8 新增的時間系列用法
下一篇文章會介紹具有時區概念的「ZonedDateTime」。以及如何在 Spring Boot 類別以 request body 接收這些時間日期類別。
今日文章到此結束!
最後推廣一下自己的部落格,我是「新手工程師的程式教室」的作者,請多指教