在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」為例。

參數說明範例
y2023
yyyy年(同上)2023
yy年(兩位數)23
Y基於星期的年份2023
u基於ISO星期的年份2023
M月(不補0)3
MM月(補0)03
d日(不補0)9
dd日(補0)09
h12小時制的時(不補0)8
hh12小時制的時(補0)08
H24小時制的時(不補0)8
HH24小時制的時(補0)08
m分(不補0)1
mm分(補0)01
s秒(不補0)5
ss秒(補0)05
S秒小數點後1位0
SS秒小數點後2位,最高到9位00
a上午、下午(會轉為當地語言)上午