在Java 8之前,日期與時間的API(java.util.Date
)一直存在著幾個問題。以往的Date
物件同時包含了日期以及時間(如時、分、秒)的資訊,因此如果只是要使用到日期的資訊,通常還需要將Date
物件的時間設定在00:00:00或者是其他固定的時刻,避免在運算時會有一些差異。同樣地,如果只是要使用到時間的資訊,也需要將Date
物件的日期設定在固定的日子,這些都讓Date
物件在使用變得不太方便。此外,Date
物件所使用的年、月數值也十分不直覺,前者是以1900年為基準來增加或是減少的年數,後者則是以0~11來表示1~12月。
核心概念
Java 8之後,新增了一組日期和時間相關的API,都包含在java.time
這個Package內。相較於之前的java.util.Date,java.time
所提供的類別與物件,擁有以下幾點特性:
- 物件的值不會被改變:
java.time
套件底下的物件都是Immutable Object,設計師只能夠通過一些類別或是物件方法來產生新的不同數值的物件。也因此這些物件都可以同時在不同的執行緒下被使用(thread-safe)。 - 領域驅動設計(Domain-driven design):
java.time
套件將日期與時間依據不同的使用案例,區分成不同的類別,不像以前的java.util.Date
是同時結合了時間與日期和時區的大雜燴。 - 支援多種曆法:
java.time
套件除了能支援國際通用的ISO 8601日期與時間的表示方式外,還支援一些non-ISO的曆法(如日本常會使用紀年年號),都放置在java.time.chrono
套件內。
常用的LocalDate
、LocalTime
與LocalDateTime
LocalDate
和LocalTime
都位於java.time
套件下,這兩個應該是學習java.time
套件最先用到類別。它們代表著純粹地(本地端的)日期與時間,沒有時區的影響。另外如果要同時使用到日期與時間的話,可以使用LocalDateTime
這個類別,它直接把LocalDate
和LocalTime
整合在一起了。
如果有用到時區,可以使用java.time
套件下的ZonedDateTime
類別,這在稍候會有比較詳細的介紹。
Instant
、Duration
與Period
java.time
套件下的Instant
、Duration
和Period
類別分別定義了一個瞬間的時間點、一個時間區段和一個時間週期。雖然都是時間,但概念有點不太相同。
Instant
Instant
類別用來定義一個瞬間的時間點,譬如說:「現在UTC的時間是西元2015年4月3日凌晨1點整。」這就是一個瞬間時間點的描述。Instant
只能夠表示UTC的時間,並沒有時區概念。如果要用Instant
類別產生出符合上述句子的物件,可以寫成程式如下:
final Instant instant = Instant.parse("2015-04-03T00:00:00Z");
也可以使用Instant
類別的ofEpochMilli
或是ofEpochSecond
方法來代入UTC的「西元2015年4月3日凌晨1點整」與「西元1970年1月1日凌晨0點整」(Epoch Time)所差距的秒數,單位分別為毫秒與秒。這個差距的數值通常為64位元的長整數(long)型態,就是我們常說與常用的「時間戳記(Timestamp)」啦!
UTC的「西元2015年4月3日凌晨1點整」與「西元1970年1月1日凌晨0點整」共相差了1428019200000毫秒,因此也可以寫成以下程式來產生Instant
物件:
final Instant instant = Instant.ofEpochMilli(1428019200000L);
如果要取得目前的時間點(或時間戳記),可以使用Instant
類別的now
方法,用法將在介紹建立LocalDate
與LocalTime
時提到。
Duration
Duration
類別用來定義一個時間區段,譬如說:「從甲地到乙地,開車開了20分鐘。」這就是一個時間區段的描述。如果要用Duration
類別產生出符合上述句子的物件,可以寫成程式如下:
final Duration duration = Duration.parse("PT20M");
或是使用Duration
類別提供的of
、ofXXXX
方法,如下:
final Duration duration = Duration.ofMinutes(20);
Duration
物件也可以和Instant
物件一同使用,進行時間的加減運算,比如說要計算在UTC的「西元2015年4月3日凌晨1點整」時,「從甲地到乙地,開車開了20分鐘」,那麼到達乙地的時間點為何?
final Instant instant = Instant.parse("2015-04-03T00:00:00Z");
final Duration duration = Duration.parse("PT20M");
final Instant newInstant = instant.plus(duration); //會再產生出新的「2015-04-03T00:20:00Z」之Instant物件
也可以使用Duration
的between
來計算兩個Instant
物件的時間差,如下:
final Instant instant1 = Instant.parse("2015-04-03T00:00:00Z");
final Instant instant2 = Instant.parse("2015-04-03T00:20:00Z");
final Duration duration12 = Duration.between(instant1, instant2); //注意參數的順序!
Period
Period
類別用來定義一個時間週期,有點類似Duration
,比較不同的是,Duration
最小的單位可到奈秒(Nanosecond),而Period
最小的單位則是天(day)。兩者的用法其實差不多,只是在使用到年、月等需要曆法支持的單位時,應該使用Period
類別,因為一年或是一個月的時間區段不是固定的!以ISO 8601的日期表示法來說,一年可能有365天或是366天、一個月則可能有28~31天,此時如果使用Duration
類別會有很多問題。
ZoneId
ZoneId
類別與時區有關,ZoneId
物件可用來定義Instant
和LocalDateTime
物件如何互相轉換。產生ZoneId
物件可以使用ZoneId
類別所提供的systemDefault
、of
、ofXXXX
方法,舉例如下:
final ZoneId zoneidDefault = ZoneId.systemDefault(); //系統預設時區
final ZoneId zoneidPlus8 = ZoneId.of("UTC+8"); //UTC時間+8
ZoneId
物件可以配合LocalDateTime
類別所提供的ofInstant
方法來將Instant
物件轉成LocalDateTime
物件,作法如下:
final LocalDateTime nowLocalDateTime = LocalDateTime.ofInstant(nowInstant, zoneidPlus8);
為什麼需要將Instant
轉成LocalDateTime
?那是因為Instant
物件只有單純時間的概念,雖然可以知道該點時間與「西元1970年1月1日凌晨0點整」(Epoch Time)所差距的秒數,但因Instant
本身缺乏日期/曆法的功能,因此無法直接用它來取得我們常會需要的年、月、日等獨立訊息(使用Instant
物件的toString
方法會自動使用DateTimeFormatter
將其轉為ISO 8601的日期與時間格式之字串)。但是透過LocalDateTime
類別,可以將其輕易轉成我們熟知的曆法格式,並能輕易地取出各時間單位的值。只不過在這裡,由於LocalDateTime
本身並不含有時區資訊,因此若要將LocalDateTime
轉回Instant
物件,也還是需要提供一個時區給它。
透過LocalDateTime
物件的toInstant
方法,可以將現有的LocalDateTime
物件轉為Instant
物件,參數部份要代入ZoneOffset
物件,而不是ZoneId
物件。ZoneOffset
物件繼承ZoneId
,但它只專注在時間的位移上,而沒有地區與標準時間的概念。如果在使用ZoneId
的of
方法時,只傳入時間的位移,那麼產生出來的物件將會是ZoneOffset
物件。當然,ZoneOffset
類別本身也是有提供of
或是ofXXXX
方法來建立ZoneOffset
物件。建立ZoneOffset
物件的方式如下:
final ZoneOffset zoneid8hr1 = (ZoneOffset)ZoneId.of("+8");
final ZoneOffset zoneid8hr2 = ZoneOffset.of("+8");
final ZoneOffset zoneid8hr3 = ZoneOffset.ofHours(8);
將LocalDateTime
物件轉為Instant
物件的方式如下:
final Instant nowLocalDateTimeToInstant = nowLocalDateTime.toInstant(zoneid8hr1);
Clock
java.time.Clock
物件可用來找出目前指定時區的時間點,使用Clock
類別提供的systemUTC
、systemDefaultZone
或是system
方法來取得不同時區的Clock
物件實體。Clock
的實際應用將在建立LocalDate
與LocalTime
物件時一同介紹。Clock
和ZoneID
可能很容易被混淆在一起,簡單來說,Clock
所表示的是一個已調整好時區的「時鐘」,因此可以藉由直接查看這個「時鐘」來得知現在的時間(時鐘是一直在跑的,只有觀察它時才會得到該觀察時間點的結果);而ZoneID
只表示一個時區,或者說是一個時間位移,並不能用來查看時間,但ZoneID
物件可以用來調整Clock
物件的時區,或是其它已知時間的時區。
建立LocalDate
、LocalTime
與LocalDateTime
物件
由於LocalDate
、LocalTime
和LocalDateTime
物件的建構子都被宣告為private
,因此無法直接使用new
來實體化出它們。如果要建立出它們的物件,必須透過它們的類別方法來達成,常用的幾種方法以下分別一一介紹。
now
now
是類別方法,可以使用現在系統的時間來建立出LocalDate
、LocalTime
和LocalDateTime
物件。參數可以代入Clock
物件來指定時區,但若不傳入任何參數,則會預設代入Clock.systemDefaultZone()
,使用作業系統預設的時區。
舉個例子,取得目前系統的日期與時間,並顯示出來:
final LocalDateTime currentPoint = LocalDateTime.now(); //直接使用LocalDateTime類別來取得日期與時間
System.out.println(currentPoint);
印出結果為:
如果要指定時區,可以在呼叫now
方法時自行傳入Clock
物件。作法如下:
final LocalDateTime currentPointUTC = LocalDateTime.now(Clock.systemUTC());
final LocalDateTime currentPointDefault = LocalDateTime.now(Clock.systemDefaultZone()); //同LocalDateTime.now();
final LocalDateTime currentPointPlus8 = LocalDateTime.now(Clock.system(ZoneId.of("+8")));
of
of
是類別方法,可以直接傳入年、月、日、小時、分鐘、秒等時間數值來產生LocalDate
、LocalTime
和LocalDateTime
物件。這裡要注意的是LocalDate
和LocalTime
這兩個類別都已經定義好它們運用的領域(文章一開始有提到Domain-driven design),因此,LocalDate
是無法使用of
方法指定時間的,LocalTime
也無法使用of
方法指定日期,如果要同時使用of
方法指定日期與時間,需得用LocalDateTime
。
舉個例子,建立出「2015年4月5日下午12點30分30秒」的物件:
final LocalDateTime qingming = LocalDateTime.of(2015, 4, 5, 12, 30, 30, 30);
final LocalDate qingmingDate = LocalDate.of(2015, 4, 5); //同qingming.toLocalDate()
final LocalTime qingmingTime = LocalTime.of(12, 30, 30); //同qingming.toLocalTime()
parse
parse
是類別方法,可以直接傳入字串來產生LocalDate
、LocalTime
和LocalDateTime
物件。傳入的字串格式可由DateTimeFormatter
來決定,預設是使用DateTimeFormatter.ISO_LOCAL_DATE_TIME
,用法如下:
final LocalDateTime qingmingParsed = LocalDateTime.parse("2015-04-05T12:30:30");
使用DateTimeFormatter
類別提供的ofPattern
方法,可以自訂日期與時間字串格式化的方式,至於Pattern的寫法可以參考DateTimeFormatter
的Java文件。上面程式,可以使用自訂的DateTimeFormatter
,改寫如下:
final LocalDateTime qingmingParsed = LocalDateTime.parse("2015/04/05 12:30:30", DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));
活用LocalDate
、LocalTime
與LocalDateTime
物件
取得年、月、日、小時、分鐘、秒、星期
java.time
套件提供的取得日期與時間之功能,不像以往的java.util.Date
會用一些奇怪的數值來表示,像是用0表示1月,用70表示西元1970年,java.time
套件將這些數值都改成大家習慣的方式的表示了,有專有名詞的單位(如:月、星期)還用了列舉來表示,方便了很多。
final LocalDateTime currentDateTime = LocalDateTime.now();
final int year = currentDateTime.getYear();
final int month = currentDateTime.getMonthValue();
final Month m = currentDateTime.getMonth();
final int day = currentDateTime.getDayOfMonth();
final DayOfWeek w = currentDateTime.getDayOfWeek();
final int hour = currentDateTime.getHour();
final int minute = currentDateTime.getMinute();
final int second = currentDateTime.getSecond();
修改日期或時間
以往的java.util.Date
有提供setter
方法讓設計師可以直接修改Date
物件所代表的日期與時間,這個情形在java.time
套件下並不會出現,前面有提到過,java.time
套件底下的物件都是Immutable Object,一旦被建立後,就只能讀取,不能再被改變。雖然是說不能被改變,卻也並不是說java.time
套件下的物件建立完成後,如果要修改它的值只能再從頭使用類別方法產生一個新的物件出來。這些Immutable Object還是有提供一些方法來指定新的數值,只是會產生出新的物件。
final LocalDate currentDate = LocalDate.now();
final LocalDate thisMonth = currentDate.withDayOfMonth(1); //將日期指定為該月1號。注意這裡currentDate並沒有被改變!
LocalDate
、LocalTime
與LocalDateTime
物件也和Instant
等物件一樣可以做一些時間的運算。例如,計算出下個禮拜的時間,程式如下:
final LocalDateTime nextWeekDateTime = LocalDateTime.now().plusWeeks(1);
final LocalDateTime next7DaysDateTime = LocalDateTime.now().plusDays(7);
時間點的轉換
TemporalAdjusters
提供了許多類別方法來從某一時間點下跳到其他的時間點,不需要設計師自己多寫程式來實作。以下舉幾個例子:
取得目前這個月的最後一天
西曆每個月的天數都不同,在以前,如果要取得該月的最後一天是幾號,還需要去判斷其是否為2月,若是,還要判斷其是否為閏年;若不是,還要判斷其為大月還是小月,十分麻煩。有了TemporalAdjusters
後,程式就可以這樣寫:
final LocalDateTime lastDayOfMonthDateTime = LocalDateTime.now().with(TemporalAdjusters.lastDayOfMonth());
取得距離目前最近的星期三
TemporalAdjusters
提供了next
和nextOrSame
與previous
和previousOrSame
方法,可以快速找出某一時間點往前或是往後距離最近的星期日子。
final LocalDateTime previousWednesdayDateTime = LocalDateTime.now().with(TemporalAdjusters.previousOrSame(DayOfWeek.WEDNESDAY));
final LocalDateTime nextWednesdayDateTime = LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.WEDNESDAY));
減少時間精確度(Truncation)
在進行時間運算的時候,常常會因為時間的精確度而造成一些誤差。例如在以往使用Date
物件做日期計算的時候,都必須特意將時間設為00:00:00或者是其他固定的時刻。在java.time
套件下,已有內建減少時間精確度的相關實作,就不必再由設計師自行手動調整了!
只有LocalTime
與LocalDateTime
物件有truncatedTo
方法可以使用,使用方式如下:
final LocalDateTime hourDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS); //捨棄比小時小的單位
ZonedDateTime
的建立與使用
前面提到的LocalDate
、LocalTime
與LocalDateTime
都沒有時區的概念,如果有要使用到時區的話,應該要使用ZonedDateTime
這個類別。ZonedDateTime
物件的建立方式和LocalDateTime
挺像的,只不過ZonedDateTime
需要在建立時多代入ZoneId
物件來指定時間的時區。
舉個例子,取得目前系統的日期與時間,並顯示出來:
final ZonedDateTime currentPoint = ZonedDateTime.now(); //同ZonedDateTime.now(Clock.systemDefaultZone())。直接使用ZonedDateTime類別來取得日期與時間,由於Clock物件已有時區資訊,故不用再代入時區
System.out.println(currentPoint);
印出結果為:
除了使用ZonedDateTime
類別來建立ZonedDateTime
物件之外,也可以直接用LocalDateTime
物件的atZone
方法,參數傳入ZoneId
物件,就能轉成ZonedDateTime
物件,如下:
final LocalDateTime currentDateTime = LocalDateTime.now();
final ZonedDateTime zonedCurrentDateTime = currentDateTime.atZone(ZoneId.of("+8"));
ZonedDateTime
物件因為已經包含了時區資訊,因此可以直接使用toInstant
方法,不用代入任何時區參數,就可以轉成Instant
物件。
final Instant zonedCurrentInstant = ZonedDateTime.now().toInstant();
年表/曆法(Chronology)
java.time
套件除了能支援國際通用的ISO 8601日期與時間的表示方式外,還支援一些non-ISO的曆法,相關API都在java.time.chrono
套件內。舉例來說,如果要使用日本昭和或是平成年號來紀年的話,可以使用java.time.chrono
裡的JapaneseChronology
、JapaneseDate
與JapaneseEra
(日本年號列舉)類別,程式如下:
final JapaneseChronology japaneseChronology = JapaneseChronology.INSTANCE;
final JapaneseDate japaneseDate = japaneseChronology.dateNow();
System.out.println(japaneseDate);
印出結果為:
YearMonth
與MonthDay
有時我們只會需要年月(如信用卡的有效日期)或是月日(如生日)等資料,java.time
套件裡也有這樣的類別存在,分別就是YearMonth
和MonthDay
。如下:
final MonthDay birthday = MonthDay.of(8, 10);
final YearMonth creditCard = YearMonth.of(2015, 4);
由於YearMonth
和MonthDay
所含的資訊比較少,因此若要轉成LocalDate
物件,還需要再提供更多的資訊,如以下程式:
final LocalDate birthdayLocalDate = birthday.atYear(1993);
final LocalDate creditCardLocalDate = creditCard.atDay(5);
農曆(Chinese Calendar)
原先以為這次Java 8的Date-Time API更新應該會支援農曆,但很可惜的還是沒有。如果想在Java上使用農曆的話,可以參考底下這篇文章: