在Java 8推出新的日期與時間API之前可能會使用Long型態直接紀錄Unix Timestamp,或是使用java.util.Date來紀錄一個日期時間。從Java 8開始推出更方便的java.time系列API,可以只記錄日期、只記錄時間、紀錄一段時間長度、處理各種時區問題。本文章將介紹這些好用的API,以及提供各種操作範例。
Java 8之前
常見方案
在Java 8之前的時間常見的大概有兩種選擇,一個是使用Long儲存Unix Timestamp(又稱POSIX Time或是Epoch Time),通常我們使用System.currentTimeMillis()
即可獲得毫秒級別的Unix Timestamp。
另一種方法是使用java.util.Date來儲存時間,Date內部儲存的也是Unix Timestamp,可以透過getTime()
方法獲得。如果需要將Date變成人類方便閱讀的字串就會搭配java.text.SimpleDateFormat來使用。
缺點
這種方案帶來的缺點主要有兩個,一個是不容易區分時區,尤其Date物件上看不出時區,通常都是採用作業系統的時區。另一個是Date物件儲存的是Unix Timestamp,不容易
- 可變性:Date可以透過setTime()方法改變內容,因此從外部將參數設為final也沒辦法保證Immutability,在Multi-Thread或是Functional運算情況下容易不小心變更內容導致運算出錯。
- 時區:雖然Unix Timestamp是UTC+0的時區,但是java.util.Date本身沒有時區概念,因此各種轉換下容易出錯。
- 精度:java.util.Date儲存的是毫秒級別的Unix Timestamp,沒辦法更精細。
- 不能只記錄日期:java.util.Date不能只儲存日期或是只儲存時間,使用起來不夠彈性。在JPA的Entity中還要另外使用TemporalType來標注對應的是資料庫的Date、Time、Timestamp等。
Java 8之後
從Java 8開始新增了java.time
套件,還有字串處理的java.time.format
。將原本的Date變成LocalDateTime
,然後新增只記錄日期的LocalDate
與只記錄時間的LocalTime
。
前幾個Class使用Local開頭是因為這代表當地時間,對應的還有紀錄時區的ZonedDateTime,以及紀錄偏移量的OffsetDateTime。前者可以紀錄特定地理時區,例如「Asia/Tokyo」,後者用來記錄一個時區的偏移量,例如跟東京時間相同的UTC+9。
Java 8還加上了代表一個時間戳的Instant
,代表一段時間長度的Duration
。而且現在的Spring Data JPA與Hibernate也支援Local開頭的日期與時間,可自動轉為Date、Time、DateTime等型態,可以取代原本的Date來作為Entity Class內的欄位型態。
以下提供各種操作範例。
建立LocalDateTime
以下提供直接建立,以及使用ISO 8601格式建立指定時間的方式,如果需要使用自訂的格式建立LocalDateTime,請參考下方「字串轉為LocalDate」
import java.time.LocalDateTime;
// 以現在時間建立LocalDateTime
LocalDateTime.now();
// 使用ISO 8601格式的字串建立LocalDateTime
LocalDateTime.parse("2023-03-12T08:35:00");
LocalDateTime轉換為日期與時間
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.time.LocalTime;
// 以現在時間建立
LocalDateTime dateTime = LocalDateTime.now();
// 轉為只有日期
LocalDate date = dateTime.toLocalDate();
// 轉為只有時間
LocalTime date = dateTime.toLocalTime();
Unix Time轉換為LocalDateTime
import java.time.LocalDateTime;
import java.time.Instant;
import java.time.TimeZone;
// 輸入單位為秒
long ts = 1678550618L;
LocalDateTime time = LocalDateTime.ofInstant(
Instant.ofEpochSecond(ts), TimeZone.getDefault().toZoneId());
// 輸入單位為毫秒
long ts = 1678550618900L;
LocalDateTime time = LocalDateTime.ofInstant(
Instant.ofEpochMilli(ts), TimeZone.getDefault().toZoneId());
LocalDateTime轉換為Unix Time
import java.time.LocalDateTime;
import java.time.ZoneId;
// 輸出單位為毫秒
LocalDateTime date = LocalDateTime.now();
long time = ZonedDateTime.of(date, ZoneId.systemDefault()).toInstant().toEpochMilli();
字串轉為LocalDate
這篇範例的程式碼比較多,改用var
來宣告看起來比較清爽。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
// 範例一
var str = "2023-03-12";
var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
var date = LocalDate.parse(str, formatter);
// 範例二
var str = "2023年03月12日";
var formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
var date = LocalDate.parse(str, formatter);
// 範例三
var str = "Create by https://klab.tw 2023/03/12";
var formatter = DateTimeFormatter.ofPattern("'Create by https://klab.tw' yyyy/MM/dd");
var date = LocalDate.parse(str, formatter);
本篇文章最後有提供DateTimeFormatter的參數教學。
字串轉為LocalDateTime
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
String str = "2023-03-12 15:00";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
LocalDateTime date = LocalDateTime.parse(str, formatter);
本篇文章最後有提供DateTimeFormatter的參數教學。
LocalDate轉為字串
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
LocalDate date = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
String str = date.format(formatter);
// 使用LocalDateTime也可以只輸出年月日
LocalDateTime dateTime = LocalDateTime.now();
String str = date.format(dateTime);
本篇文章最後有提供DateTimeFormatter的參數教學。
計算時間長度
這邊以計算程式的執行時間作為範例。
// https://klab.tw/2023/03/java-8-date-time-api-introduction/
import java.time.Instant;
import java.time.Duration;
// 紀錄開始時間
Instant begin = Instant.now();
// 做一些費時的事情
// 紀錄結束時間
Instant end = Instant.now();
// 將兩個時間轉為時間長度
Duration duration = Duration.between(begin, end);
// 輸出秒數
long s = duration.toSeconds();
// 輸出毫秒
long ms = duration.toMillis();
// 輸出奈秒
long ns = duration.toNanos();
日期與時間的加減
前面有提到Java 8的日期與時間是不可變的(Immutable),因此對日期時間進行加減運算的時候不會改變原本儲存的物件,而是會回傳一個新的日期時間物件。
// https://klab.tw/2023/03/java-8-date-time-api-introduction/
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
LocalDateTime dateTime = LocalDateTime.now();
// 方法一
dateTime.plusYears(1); // 增加一年
dateTime.plusMonths(1); // 增加一個月
dateTime.plusWeeks(1); // 增加一個星期
dateTime.plusDays(1); // 增加一天
dateTime.plusHours(1); // 增加一小時
dateTime.plusMinutes(1); // 增加一分鐘
dateTime.plusSeconds(1); // 增加一秒
dateTime.plusNanos(1); // 增加一奈秒
// 方法二
dateTime.plus(1, ChronoUnit.YEARS); // 增加一年
dateTime.plus(1, ChronoUnit.MONTHS); // 增加一個月
dateTime.plus(1, ChronoUnit.WEEKS); // 增加一個星期
dateTime.plus(1, ChronoUnit.DAYS); // 增加一天
dateTime.plus(1, ChronoUnit.HALF_DAYS); // 增加半天(12小時)
dateTime.plus(1, ChronoUnit.HOURS); // 增加一小時
dateTime.plus(1, ChronoUnit.MINUTES); // 增加一分鐘
dateTime.plus(1, ChronoUnit.SECONDS); // 增加一秒
dateTime.plus(1, ChronoUnit.MILLIS); // 增加一毫秒
dateTime.plus(1, ChronoUnit.NANOS); // 增加一奈秒
以上每個plusXXX方法都有對應的minusXXX方法,plus()方法也有對應的minus()方法,因此可以在plus函數內帶入負數,或是直接使用minus函數來做時間的減法。
還記得前面提過Duration是用來記錄一段時間的長度,因此這邊的plus與minus參數也可以是Duration。
DateTimeFormatter參數
Java 8的DateTimeFormatter
的參數跟舊版的SimpleDateFormat
幾乎一樣,以下提供簡易範例與說明,需要更詳細資料可以參考Java Doc DateTimeFormatter的說明。
以下表單的「範例」以「2023-03-09T08:01:05」為例。
參數 | 說明 | 範例 |
---|---|---|
y | 年 | 2023 |
yyyy | 年(同上) | 2023 |
yy | 年(兩位數) | 23 |
Y | 基於星期的年份 | 2023 |
u | 基於ISO星期的年份 | 2023 |
M | 月(不補0) | 3 |
MM | 月(補0) | 03 |
d | 日(不補0) | 9 |
dd | 日(補0) | 09 |
h | 12小時制的時(不補0) | 8 |
hh | 12小時制的時(補0) | 08 |
H | 24小時制的時(不補0) | 8 |
HH | 24小時制的時(補0) | 08 |
m | 分(不補0) | 1 |
mm | 分(補0) | 01 |
s | 秒(不補0) | 5 |
ss | 秒(補0) | 05 |
S | 秒小數點後1位 | 0 |
SS | 秒小數點後2位,最高到9位 | 00 |
a | 上午、下午(會轉為當地語言) | 上午 |