Dating Java8系列之新的日期和時間

翎野君/文

 

圖片

 

圖片

舊的日期時間

 

在Java 1.0中,對日期和時間的支持只能依賴java.util.Date類。正如類名所表達的,這個類無法表示日期,只能以毫秒的精度表示時間。更糟糕的是它的易用性,由於某些原因和未知的設計決策,這個類的易用性被深深地損害了,比如:年份的起始選擇是1900年,月份的起始從0開始。這意味着,如果你想要用Date表示Java 8的發佈日期,即2014年3月18日,需要創建下面這樣的Date實例:

Date date = new Date(114, 2, 18);

它的打印輸出效果爲:

Tue Mar 18 00:00:00 CET 2014

看起來不那麼直觀,不是嗎?此外,甚至Date類的toString方法返回的字符串也容易誤導人。以我們的例子而言,它的返回值中甚至還包含了JVM的默認時區CET,即中歐時間(Central Europe Time)。但這並不表示Date類是一個支持時區的日期時間API。

隨着Java 1.0退出歷史舞臺,Date類的種種問題和限制幾乎一掃而光,但很明顯,這些歷史舊賬如果不犧牲前向兼容性是無法解決的。所以,在Java1.1中,Date類中的很多方法被廢棄了,取而代之的是java.util.Calendar類。很不幸,Calendar類也有類似的問題和設計缺陷,導致使用這些方法寫出的代碼非常容易出錯。比如,月份依舊是從0開始計算(不過,至少Calendar類拿掉了由1900年開始計算年份這一設計)。更糟的是,同時存在Date和Calendar這兩個類,也增加了程序員的在選擇上的困難。到底該使用哪一個類呢?此外,有的特性只在某一個類有提供,比如用於以語言無關方式格式化和解析日期或時間的DateFormat方法就只在Date類裏有。

DateFormat方法也有它自己的問題。比如,它不是線程安全的。這意味着兩個線程如果使用同一個formatter解析日期,你可能會得到無法預期的結果。

 

圖片

新的日期和時間

 

LocalDate

LocalDate類的實例是一個不可變對象,它只提供了簡單的日期,並不含當天的時間信息。另外,它也不攜帶任何與時區相關的信息。

LocalDate localDate1 = LocalDate.now();System.out.println(localDate1);LocalDate localDate2 = LocalDate.of(2019, 06, 06);LocalDate localDate3 = LocalDate.of(2019, Month.JUNE, 06);System.out.println(localDate2 + " " + localDate3);System.out.println(localDate2.getDayOfYear());System.out.println(localDate2.getDayOfWeek());System.out.println(localDate2.getDayOfMonth());System.out.println(localDate2.getMonth());System.out.println(localDate2.get(ChronoField.DAY_OF_WEEK));System.out.println(localDate2.get(ChronoField.DAY_OF_YEAR));

LocalTime

類似地,一天中的時間,比如13:45:20,可以使用LocalTime類表示。你可以使用of重載的兩個工廠方法創建LocalTime的實例。第一個重載函數接收小時和分 ,第二個重載函數同時還接收秒。同LocalDate一樣,LocalTime類也提供了一些getter方法訪問這些變量的值。

LocalTime localTime1 = LocalTime.now();System.out.println(localTime1);LocalTime localTime2 = LocalTime.of(12, 02);LocalTime localTime3 = LocalTime.of(12, 02, 03);LocalTime localTime4 = LocalTime.of(12, 02, 03, 900_000_000);System.out.println(localTime2 + " " + localTime3 + " " + localTime4);System.out.println(localTime4.getHour());System.out.println(localTime4.getMinute());System.out.println(localTime4.getSecond());

LocalDateTime

這個複合類名叫LocalDateTime,是LocalDate和LocalTime的合體。它同時表示了日期和時間,但不帶有時區信息,你可以直接創建,也可以通過合併日期和時間對象進行構造。

LocalDateTime localDateTime1 = LocalDateTime.now();System.out.println(localDateTime1);LocalDateTime localDateTime2 = LocalDateTime.of(2000, Month.AUGUST, 22, 04, 05, 06);System.out.println(localDateTime2);System.out.println(LocalDateTime.of(localDate1, localTime1));System.out.println(localDate2.atTime(localTime2));System.out.println(localTime3.atDate(localDate3));System.out.println(localTime3.get(ChronoField.HOUR_OF_DAY));System.out.println(localTime3.get(ChronoField.MINUTE_OF_HOUR));

Instant

作爲人,我們習慣於以星期幾、幾號、幾點、幾分這樣的方式理解日期和時間。毫無疑問,這種方式對於計算機而言並不容易理解。從計算機的角度來看,建模時間最自然的格式是表示一個持續時間段上某個點的單一大整型數。這也是新的java.time.Instant類對時間建模的方式,基本上它是以Unix元年時間(傳統的設定爲UTC時區1970年1月1日午夜時分)開始所經歷的秒數進行計算。

// InstantInstant instant = Instant.now();System.out.println(instant);// 時間戳,秒,毫秒System.out.println(instant.toEpochMilli());System.out.println(instant.getEpochSecond());// Date、Calendar、SystemDate date = new Date();System.out.println(date.getTime());System.out.println(Calendar.getInstance().getTimeInMillis());System.out.println(System.currentTimeMillis());System.out.println(instant.toEpochMilli() + " " + instant.getNano());

Duration和Period

Duration類的靜態工廠方法between就是爲獲取兩個時間的間隔。可以創建兩個LocalTime對象、兩個LocalDateTime。

由於LocalDateTime和Instant是爲不同的目的而設計的,一個是爲了便於人閱讀使用, 另一個是爲了便於機器處理,所以你不能將二者混用。如果試圖在這兩類對象之間創建duration,會觸發一個DateTimeException異常。此外,由於Duration類主要用於以秒和納秒衡量時間的長短,你不能僅向between方法傳遞一個LocalDate對象做參數。

如果你需要以年、月或者日的方式對多個時間單位建模,可以使用Period類。使用該類的工廠方法between,你可以使用得到兩個LocalDate之間的時長。

圖片

// Duration 用在LocalTime、LocalDateTimeDuration duration1 = Duration.between(localTime1, localTime2);System.out.println(duration1);System.out.println(Duration.between(localDateTime1, localDateTime2));System.out.println(Duration.ofDays(2));System.out.println(Duration.ofHours(5));System.out.println(Duration.ofMinutes(100));System.out.println(Duration.of(2, ChronoUnit.MINUTES));System.out.println(Duration.of(2, ChronoUnit.SECONDS));
// Period用在LocalDate上面Period period = Period.between(localDate1, localDate2);System.out.println(period);System.out.println(Period.ofYears(2));

 

圖片

操縱日期和時間

 

簡單的操縱對象

如果你已經有一個LocalDate對象,想要創建它的一個修改版,最直接也最簡單的方法是使用withAttribute方法。withAttribute方法會創建對象的一個副本,並按照需要修改它的屬性。注意,下面的這段代碼中所有的方法都返回一個修改了屬性的對象。它們都不會修改原來的對象!

LocalDate date1 = LocalDate.of(2014, 3, 18);LocalDate date2 = date1.withYear(2011);LocalDate date3 = date2.withDayOfMonth(25);LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);

像LocalDate、LocalTime、LocalDateTime以及Instant這樣表示時間點的日期和時間類提供了大量通用的方法

圖片

使用TemporalAdjuster

截至目前,你所看到的所有日期操作都是相對比較直接的。有的時候,你需要進行一些更加複雜的操作,比如,將日期調整到下個週日、下個工作日,或者是本月的最後一天。這時,你可以使用重載版本的with方法,向其傳遞一個提供了更多定製化選擇的TemporalAdjuster對象,更加靈活地處理日期。對於最常見的用例,日期和時間API已經提供了大量預定義的TemporalAdjuster。你可以通過TemporalAdjuster類的靜態工廠方法訪問它們。

import static java.time.temporal.TemporalAdjusters.*;LocalDate date1 = LocalDate.of(2014, 3, 18);LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));LocalDate date3 = date2.with(lastDayOfMonth());

使用TemporalAdjuster我們可以進行更加複雜的日期操作,而且這些方法的名稱也非常直觀,方法名基本就是問題陳述。此外,即使你沒有找到符合你要求的預定義TemporalAdjuster,創建你自己的TemporalAdjuster也並非難事。

實際上,TemporalAdjuster接口只聲明瞭單一的一個方法這使得它成爲了一個函數式接口),定義如下。

圖片

// 使用TemporalAdjuster中預定義的方法快速操縱和修改日期System.out.println(localDate.with(TemporalAdjusters.lastDayOfYear()));System.out.println(localDate.with(TemporalAdjusters.firstDayOfNextMonth()));System.out.println(localDate.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)));System.out.println(localDate.with(TemporalAdjusters.firstInMonth(DayOfWeek.TUESDAY)));// 函數式接口LocalDate firstMonthOfYear = localDate.with((temporal) -> localDate.withMonth(1));LocalDate lastMonthOfYear = localDate.with((temporal) -> localDate.withMonth(12));System.out.println(firstMonthOfYear + " " + lastMonthOfYear);

 

圖片

解析日期對象

 

解析日期時間

處理日期和時間對象時,格式化以及解析日期和時間對象是另一個非常重要的功能。新的java.time.format包就是特別爲這個目的而設計的。這個包中,最重要的類是DateTimeFormatter。創建格式器最簡單的方法是通過它的靜態工廠方法以及常量。像BASIC_ISO_DATE和ISO_LOCAL_DATE這樣的常量是DateTimeFormatter類的預定義實例。所有的DateTimeFormatter實例都能用於以一定的格式創建代表特定日期或時間的字符串。比如,下面的這個例子中,我們使用了兩個不同的格式器生成了字符串:

LocalDate ld4 = LocalDate.of(2019, 06, 06);System.out.println(ld1.format(DateTimeFormatter.BASIC_ISO_DATE));LocalTime lt3 = LocalTime.now();System.out.println(lt3.format(DateTimeFormatter.ISO_LOCAL_TIME));System.out.println(LocalDate.parse("20190606", DateTimeFormatter.BASIC_ISO_DATE));System.out.println(LocalTime.parse(“12:02:03”, DateTimeFormatter.ISO_LOCAL_TIME));

和老的java.util.DateFormat相比較,所有的DateTimeFormatter實例都是線程安全的。所以,你能夠以單例模式創建格式器實例,就像DateTimeFormatter所定義的那些常量,並能在多個線程間共享這些實例。DateTimeFormatter類還支持一個靜態工廠方法,它可以按照某個特定的模式創建格式器,代碼清單如下。

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");String ld4Str = ld4.format(dateTimeFormatter);System.out.println(ld4Str);System.out.println(LocalDate.parse(ld4Str, dateTimeFormatter));

 時區

時區是按照一定的規則將區域劃分成的標準時間相同的區間。在ZoneRules這個類中包含了40個這樣的實例。你可以簡單地通過調用ZoneId的getRules()得到指定時區的規則。每個特定的ZoneId對象都由一個地區ID標識,比如:

ZoneId zoneId = ZoneId.of("Asia/Tokyo");

地區ID都爲“{區域}/{城市}”的格式,這些地區集合的設定都由因特網編號分配機構(IANA)的時區數據庫提供。你可以通過Java8的新方法toZoneId將一個老的時區對象轉換爲ZoneId:

ZoneId zoneId = TimeZone.getDefault().toZoneId();

一旦得到一個ZoneId對象你就可以將它與LocalDate、LocalDateTime或者是Instant對象整合起來,構造爲一個ZonedDateTime實例,它代表了相對於指定時區的時間點。

 

圖片

// 不同時區ZoneId zoneId = ZoneId.of("Asia/Tokyo");ZoneId defaultZoneId = TimeZone.getDefault().toZoneId();LocalDateTime localDateTime2 = LocalDateTime.now();// 標記這個是哪個時區的時間System.out.println(localDateTime2);System.out.println(" !! " + localDateTime2.atZone(zoneId));// 把時間戳轉成指定時區的時間System.out.println(Instant.now().atZone(zoneId));

 通過ZoneId,你還可以將LocalDateTime轉換爲Instant:

LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45); Instant instantFromDateTime = dateTime.toInstant(romeZone);

你也可以通過反向的方式得到LocalDateTime對象: 

Instant instant = Instant.now();LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);

另一種比較通用的表達時區的方式是利用當前時區和UTC/格林尼治時間的固定偏差計算時區。比如,基於這個理論,你可以說“北京早於倫敦8小時”。這種情況下,你可以使用ZoneOffset類,它是ZoneId的一個子類,表示的是當前時間和倫敦格林尼治子午線時間的差異:

ZoneOffset beijing = ZoneOffset.of("+08:00");OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDateTime.now(), beijing);System.out.println(offsetDateTime);

 

圖片

總結

 

  • 新版的日期和時間API中,日期-時間對象是不可變的。

  • 新的API提供了兩種不同的時間表示方式,有效地區分了運行時人和機器的不同需求。

  • 可以用絕對或者相對的方式操作日期和時間,操作的結果總是返回一個新的實例,老的日期時間對象不會發生變化。

  • TemporalAdjuster能夠用更精細的方式操縱日期,不再侷限於一次只能改變它的一個值,並且你還可按照需求定義自己的日期轉換器。

  • 現在可以按照特定的格式需求,定義自己的格式器,打印輸出或者解析日期時間對象。這些格式器可以通過模板創建,也可以自己編程創建,並且它們都是線程安全的。

  • 可以用相對於某個地區/位置的方式,或者以與UTC/格林尼治時間的絕對偏差的方式表示時區,並將其應用到日期-時間對象上,對其進行本地化。

 

作者:翎野君
博客:https://www.cnblogs.com/lingyejun/

 

本篇文章如有幫助到您,請給「翎野君」點個贊,感謝您的支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章