在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套件內。

常用的LocalDateLocalTimeLocalDateTime

LocalDateLocalTime都位於java.time套件下,這兩個應該是學習java.time套件最先用到類別。它們代表著純粹地(本地端的)日期與時間,沒有時區的影響。另外如果要同時使用到日期與時間的話,可以使用LocalDateTime這個類別,它直接把LocalDateLocalTime整合在一起了。

如果有用到時區,可以使用java.time套件下的ZonedDateTime類別,這在稍候會有比較詳細的介紹。

InstantDurationPeriod

java.time套件下的InstantDurationPeriod類別分別定義了一個瞬間的時間點、一個時間區段和一個時間週期。雖然都是時間,但概念有點不太相同。

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方法,用法將在介紹建立LocalDateLocalTime時提到。

Duration

Duration類別用來定義一個時間區段,譬如說:「從甲地到乙地,開車開了20分鐘。」這就是一個時間區段的描述。如果要用Duration類別產生出符合上述句子的物件,可以寫成程式如下:

final Duration duration = Duration.parse("PT20M");

或是使用Duration類別提供的ofofXXXX方法,如下:

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物件

也可以使用Durationbetween來計算兩個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物件可用來定義InstantLocalDateTime物件如何互相轉換。產生ZoneId物件可以使用ZoneId類別所提供的systemDefaultofofXXXX方法,舉例如下:

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,但它只專注在時間的位移上,而沒有地區與標準時間的概念。如果在使用ZoneIdof方法時,只傳入時間的位移,那麼產生出來的物件將會是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類別提供的systemUTCsystemDefaultZone或是system方法來取得不同時區的Clock物件實體。Clock的實際應用將在建立LocalDateLocalTime物件時一同介紹。ClockZoneID可能很容易被混淆在一起,簡單來說,Clock所表示的是一個已調整好時區的「時鐘」,因此可以藉由直接查看這個「時鐘」來得知現在的時間(時鐘是一直在跑的,只有觀察它時才會得到該觀察時間點的結果);而ZoneID只表示一個時區,或者說是一個時間位移,並不能用來查看時間,但ZoneID物件可以用來調整Clock物件的時區,或是其它已知時間的時區。

建立LocalDateLocalTimeLocalDateTime物件

由於LocalDateLocalTimeLocalDateTime物件的建構子都被宣告為private,因此無法直接使用new來實體化出它們。如果要建立出它們的物件,必須透過它們的類別方法來達成,常用的幾種方法以下分別一一介紹。

now

now是類別方法,可以使用現在系統的時間來建立出LocalDateLocalTimeLocalDateTime物件。參數可以代入Clock物件來指定時區,但若不傳入任何參數,則會預設代入Clock.systemDefaultZone(),使用作業系統預設的時區。

舉個例子,取得目前系統的日期與時間,並顯示出來:

final LocalDateTime currentPoint = LocalDateTime.now(); //直接使用LocalDateTime類別來取得日期與時間
System.out.println(currentPoint);

印出結果為:

2015-04-03T00:51:03.688

如果要指定時區,可以在呼叫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是類別方法,可以直接傳入年、月、日、小時、分鐘、秒等時間數值來產生LocalDateLocalTimeLocalDateTime物件。這裡要注意的是LocalDateLocalTime這兩個類別都已經定義好它們運用的領域(文章一開始有提到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是類別方法,可以直接傳入字串來產生LocalDateLocalTimeLocalDateTime物件。傳入的字串格式可由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"));

活用LocalDateLocalTimeLocalDateTime物件

取得年、月、日、小時、分鐘、秒、星期

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並沒有被改變!

LocalDateLocalTimeLocalDateTime物件也和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提供了nextnextOrSamepreviouspreviousOrSame方法,可以快速找出某一時間點往前或是往後距離最近的星期日子。

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套件下,已有內建減少時間精確度的相關實作,就不必再由設計師自行手動調整了!

只有LocalTimeLocalDateTime物件有truncatedTo方法可以使用,使用方式如下:

final LocalDateTime hourDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS); //捨棄比小時小的單位

ZonedDateTime的建立與使用

前面提到的LocalDateLocalTimeLocalDateTime都沒有時區的概念,如果有要使用到時區的話,應該要使用ZonedDateTime這個類別。ZonedDateTime物件的建立方式和LocalDateTime挺像的,只不過ZonedDateTime需要在建立時多代入ZoneId物件來指定時間的時區。

舉個例子,取得目前系統的日期與時間,並顯示出來:

final ZonedDateTime currentPoint = ZonedDateTime.now(); //同ZonedDateTime.now(Clock.systemDefaultZone())。直接使用ZonedDateTime類別來取得日期與時間,由於Clock物件已有時區資訊,故不用再代入時區
System.out.println(currentPoint);

印出結果為:

2015-04-07T13:25:12.794+08:00[Asia/Taipei]

除了使用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裡的JapaneseChronologyJapaneseDateJapaneseEra(日本年號列舉)類別,程式如下:

final JapaneseChronology japaneseChronology = JapaneseChronology.INSTANCE;
final JapaneseDate japaneseDate = japaneseChronology.dateNow();
System.out.println(japaneseDate);

印出結果為:

Japanese Heisei 27-04-07

YearMonthMonthDay

有時我們只會需要年月(如信用卡的有效日期)或是月日(如生日)等資料,java.time套件裡也有這樣的類別存在,分別就是YearMonthMonthDay。如下:

final MonthDay birthday = MonthDay.of(8, 10);
final YearMonth creditCard = YearMonth.of(2015, 4);

由於YearMonthMonthDay所含的資訊比較少,因此若要轉成LocalDate物件,還需要再提供更多的資訊,如以下程式:

final LocalDate birthdayLocalDate = birthday.atYear(1993);
final LocalDate creditCardLocalDate = creditCard.atDay(5);

農曆(Chinese Calendar)

原先以為這次Java 8的Date-Time API更新應該會支援農曆,但很可惜的還是沒有。如果想在Java上使用農曆的話,可以參考底下這篇文章: