Java的API提供了不少有用的組件,能幫助你構建複雜的應用。不過,Java API也不老是完美的。咱們相信大多數有經驗的程序員都會贊同Java 8以前的庫對日期和時間的支持就很是不理想。然而,你也不用太擔憂:Java 8中引入全新的日期和時間API就是要解決這一問題。java
在Java 1.0中,對日期和時間的支持只能依賴java.util.Date類。正如類名所表達的,這個類沒法表示日期,只能以毫秒的精度表示時間。更糟糕的是它的易用性,因爲某些緣由未知的設計決策,這個類的易用性被深深地損害了,好比:年份的起始選擇是1900年,月份的起始從0開始。這意味着,若是你想要用Date表示Java 8的發佈日期,即2014年3月18日,須要建立下面這樣的Date實例:git
Date date = new Date(114, 2, 18);
複製代碼
它的打印輸出效果爲:程序員
Tue Mar 18 00:00:00 CST 2014
複製代碼
看起來不那麼直觀,不是嗎?此外,甚至Date類的toString方法返回的字符串也容易誤導人。github
隨着Java 1.0退出歷史舞臺,Date類的種種問題和限制幾乎一掃而光,但很明顯,這些歷史舊帳若是不犧牲前向兼容性是沒法解決的。因此,在Java 1.1中,Date類中的不少方法被廢棄了,取而代之的是java.util.Calendar類。很不幸,Calendar類也有相似的問題和設計缺陷,致使使用這些方法寫出的代碼很是容易出錯。好比,月份依舊是從0開始計算(不過,至少Calendar類拿掉了由1900年開始計算年份這一設計)。更糟的是,同時存在Date和Calendar這兩個類,也增長了程序員的困惑。到底該使用哪個類呢?此外,有的特性只在某一個類有提供,好比用於以語言無關方式格式化和解析日期或時間的DateFormat方法就只在Date類裏有。數據庫
DateFormat方法也有它本身的問題。好比,它不是線程安全的。這意味着兩個線程若是嘗試使用同一個formatter解析日期,你可能會獲得沒法預期的結果。編程
最後,Date和Calendar類都是能夠變的。能把2014年3月18日修改爲4月18日意味着什麼呢?這種設計會將你拖入維護的噩夢,接下來的一章,咱們會討論函數式編程,你在該章中會了解到更多的細節。安全
這一章中,咱們會一塊兒探索新的日期和時間API所提供的新特性。咱們從最基本的用例入手,好比建立同時適合人與機器的日期和時間,逐漸轉入到日期和時間API更高級的一些應用,好比操縱、解析、打印輸出日期時間對象,使用不一樣的時區和年曆。app
讓咱們從探索如何建立簡單的日期和時間間隔入手。java.time包中提供了不少新的類能夠幫你解決問題,它們是LocalDate、LocalTime、Instant、Duration和Period。函數式編程
開始使用新的日期和時間API時,你最早碰到的多是LocalDate類。該類的實例是一個不可變對象,它只提供了簡單的日期,並不含當天的時間信息。另外,它也不附帶任何與時區相關的信息。函數
你能夠經過靜態工廠方法of建立一個LocalDate實例。LocalDate實例提供了多種方法來讀取經常使用的值,好比年份、月份、星期幾等,以下所示。
LocalDate localDate = LocalDate.of(2014, 3, 18);
int year = localDate.getYear();
Month month = localDate.getMonth();
int day = localDate.getDayOfMonth();
DayOfWeek dow = localDate.getDayOfWeek();
int len = localDate.lengthOfMonth();
boolean leap = localDate.isLeapYear();
System.out.println(String.format("year:%s\nmonth:%s\nday:%s\ndow:%s\nlen:%s\nleap:%s", year, month, day, dow, len, leap));
複製代碼
打印結果:
year:2014
month:MARCH
day:18
dow:TUESDAY
len:31
leap:false
複製代碼
你還可使用工廠方法從系統時鐘中獲取當前的日期:
LocalDate today = LocalDate.now();
複製代碼
接下來剩餘的部分會探討全部日期-時間類,這些類都提供了相似的工廠方法。你還能夠經過傳遞一個TemporalField參數給get方法拿到一樣的信息。TemporalField是一個接口,它定義瞭如何訪問temporal對象某個字段的值。ChronoField枚舉實現了這一接口,因此你能夠很方便地使用get方法獲得枚舉元素的值,以下所示。
int year = localDate.get(ChronoField.YEAR);
int month = localDate.get(ChronoField.MONTH_OF_YEAR);
int day = localDate.get(ChronoField.DAY_OF_MONTH);
複製代碼
相似地,一天中的時間,好比13:45:20,可使用LocalTime類表示。你可使用of重載的兩個工廠方法建立LocalTime的實例。第一個重載函數接收小時和分鐘,第二個重載函數同時還接收秒。同LocalDate同樣,LocalTime類也提供了一些getter方法訪問這些變量的值,以下所示。
LocalTime localTime = LocalTime.of(13, 45, 20);
int hour = localTime.getHour();
int minute = localTime.getMinute();
int second = localTime.getSecond();
System.out.println(String.format("hour:%s\nminute:%s\nsecond:%s", hour, minute, second));
複製代碼
打印結果:
hour:13
minute:45
second:20
複製代碼
LocalDate和LocalTime均可以經過解析表明它們的字符串建立。使用靜態方法parse,你能夠實現這一目的:
LocalDate date = LocalDate.parse("2018-11-17");
LocalTime time = LocalTime.parse("21:27:58");
複製代碼
你能夠向parse方法傳遞一個DateTimeFormatter。該類的實例定義瞭如何格式化一個日期或者時間對象。正如咱們以前所介紹的,它是替換老版java.util.DateFormat的推薦替代品。這個咱們後面將會討論到。同時,也請注意,一旦傳遞的字符串參數沒法被解析爲合法的LocalDate或LocalTime對象,這兩個parse方法都會拋出一個繼承自RuntimeException的DateTimeParseException異常。
這個複合類名叫LocalDateTime,是LocalDate和LocalTime的合體。它同時表示了日期和時間,但不帶有時區信息,你能夠直接建立,也能夠經過合併日期和時間對象構造,以下所示。
// 2018-11-17T21:31:50
LocalTime time = LocalTime.of(21, 31, 50);
LocalDate date = LocalDate.of(2018, 11, 17);
LocalDateTime dt1 = LocalDateTime.of(2018, Month.NOVEMBER, 17, 21, 31, 50);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(21, 11, 17);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
複製代碼
注意,經過它們各自的atTime或者atDate方法,向LocalDate傳遞一個時間對象,或者向LocalTime傳遞一個日期對象的方式,你能夠建立一個LocalDateTime對象。你也可使用toLocalDate或者toLocalTime方法,從LocalDateTime中提取LocalDate或者LocalTime組件:
LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();
複製代碼
做爲人,咱們習慣於以星期幾、幾號、幾點、幾分這樣的方式理解日期和時間。毫無疑問,這種方式對於計算機而言並不容易理解。從計算機的角度來看,建模時間最天然的格式是表示一個持續時間段上某個點的單一大整型數。這也是新的java.time.Instant類對時間建模的方式,基本上它是以Unix元年時間(傳統的設定爲UTC時區1970年1月1日午夜時分)開始所經歷的秒數進行計算。
你能夠經過向靜態工廠方法ofEpochSecond傳遞一個表明秒數的值建立一個該類的實例。靜態工廠方法ofEpochSecond還有一個加強的重載版本,它接收第二個以納秒爲單位的參數值,對傳入做爲秒數的參數進行調整。重載的版本會調整納秒參數,確保保存的納秒分片在0到999 999999之間。這意味着下面這些對ofEpochSecond工廠方法的調用會返回幾乎一樣的Instant對象:
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
// 2 秒以後再加上100萬納秒(1秒)
Instant.ofEpochSecond(2, 1_000_000_000);
// 4秒以前的100萬納秒(1秒)
Instant.ofEpochSecond(4, -1_000_000_000);
複製代碼
正如你已經在LocalDate及其餘爲便於閱讀而設計的日期-時間類中所看到的那樣,Instant類也支持靜態工廠方法now,它可以幫你獲取當前時刻的時間戳。咱們想要特別強調一點,Instant的設計初衷是爲了便於機器使用。它包含的是由秒及納秒所構成的數字。因此,它沒法處理那些咱們很是容易理解的時間單位。好比下面這段語句:
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);
複製代碼
它會拋出下面這樣的異常:
Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth
複製代碼
可是你能夠經過Duration和Period類使用Instant,接下來咱們會對這部份內容進行介紹。
目前爲止,你看到的全部類都實現了Temporal接口,Temporal接口定義瞭如何讀取和操縱爲時間建模的對象的值。以前的介紹中,咱們已經瞭解了建立Temporal實例的幾種方法。很天然地你會想到,咱們須要建立兩個Temporal對象之間的duration。Duration類的靜態工廠方法between就是爲這個目的而設計的。你能夠建立兩個LocalTimes對象、兩個LocalDateTimes對象,或者兩個Instant對象之間的duration,以下所示:
LocalTime time1 = LocalTime.of(21, 50, 10);
LocalTime time2 = LocalTime.of(22, 50, 10);
LocalDateTime dateTime1 = LocalDateTime.of(2018, 11, 17, 21, 50, 10);
LocalDateTime dateTime2 = LocalDateTime.of(2018, 11, 17, 23, 50, 10);
Instant instant1 = Instant.ofEpochSecond(1000 * 60 * 2);
Instant instant2 = Instant.ofEpochSecond(1000 * 60 * 3);
Duration d1 = Duration.between(time1, time2);
Duration d2 = Duration.between(dateTime1, dateTime2);
Duration d3 = Duration.between(instant1, instant2);
// PT1H 相差1小時
System.out.println("d1:" + d1);
// PT2H 相差2小時
System.out.println("d2:" + d2);
// PT16H40M 相差16小時40分鐘
System.out.println("d3:" + d3);
複製代碼
因爲LocalDateTime和Instant是爲不一樣的目的而設計的,一個是爲了便於人閱讀使用,另外一個是爲了便於機器處理,因此你不能將兩者混用。若是你試圖在這兩類對象之間建立duration,會觸發一個DateTimeException異常。此外,因爲Duration類主要用於以秒和納秒衡量時間的長短,你不能僅向between方法傳遞一個LocalDate對象作參數。
若是你須要以年、月或者日的方式對多個時間單位建模,可使用Period類。使用該類的工廠方法between,你可使用獲得兩個LocalDate之間的時長,以下所示:
Period period = Period.between(LocalDate.of(2018, 11, 7), LocalDate.of(2018, 11, 17));
// P10D 相差10天
System.out.println("Period between:" + period);
複製代碼
最後,Duration和Period類都提供了不少很是方便的工廠類,直接建立對應的實例;換句話說,就像下面這段代碼那樣,再也不是隻能以兩個temporal對象的差值的方式來定義它們的對象。
Duration threeMinutes = Duration.ofMinutes(3);
Duration fourMinutes = Duration.of(4, ChronoUnit.MINUTES);
Period tenDay = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
複製代碼
Duration類和Period類共享了不少類似的方法,有興趣的能夠參考官網的文檔。
截至目前,咱們介紹的這些日期時間對象都是不可修改的,這是爲了更好地支持函數式編程,確保線程安全,保持領域模式一致性而作出的重大設計決定。固然,新的日期和時間API也提供了一些便利的方法來建立這些對象的可變版本。好比,你可能但願在已有的LocalDate實例上增長3天。除此以外,咱們還會介紹如何依據指定的模式,好比dd/MM/yyyy,建立日期-時間格式器,以及如何使用這種格式器解析和輸出日期。
若是你已經有一個LocalDate對象,想要建立它的一個修改版,最直接也最簡單的方法是使用withAttribute方法。withAttribute方法會建立對象的一個副本,並按照須要修改它的屬性。注意,下面的這段代碼中全部的方法都返回一個修改了屬性的對象。它們都不會修改原來的對象!
// 2018-11-17
LocalDate date1 = LocalDate.of(2018, 11, 17);
// 2019-11-17
LocalDate date2 = date1.withYear(2019);
// 2019-11-25
LocalDate date3 = date2.withDayOfMonth(25);
// 2019-09-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);
複製代碼
它們都聲明於Temporal接口,全部的日期和時間API類都實現這兩個方法,它們定義了單點的時間,好比LocalDate、LocalTime、LocalDateTime以及Instant。更確切地說,使用get和with方法,咱們能夠將Temporal對象值的讀取和修改區分開。若是Temporal對象不支持請求訪問的字段,它會拋出一個UnsupportedTemporalTypeException異常,好比試圖訪問Instant對象的ChronoField.MONTH_OF_YEAR字段,或者LocalDate對象的ChronoField.NANO_OF_SECOND字段時都會拋出這樣的異常。
它甚至能以聲明的方式操縱LocalDate對象。好比,你能夠像下面這段代碼那樣加上或者減去一段時間。
// 2018-11-17
LocalDate date1 = LocalDate.of(2018, 11, 17);
// 2018-11-24
LocalDate date2 = date1.plusWeeks(1);
// 2015-11-24
LocalDate date3 = date2.minusYears(3);
// 2016-05-24
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);
複製代碼
與咱們剛纔介紹的get和with方法相似最後一行使用的plus方法也是通用方法,它和minus方法都聲明於Temporal接口中。經過這些方法,對TemporalUnit對象加上或者減去一個數字,咱們能很是方便地將Temporal對象前溯或者回滾至某個時間段,經過ChronoUnit枚舉咱們能夠很是方便地實現TemporalUnit接口。
大概你已經猜到,像LocalDate、LocalTime、LocalDateTime以及Instant這樣表示時 間點的日期時間類提供了大量通用的方法,咱們目前所使用的只有一小部分,有興趣的能夠去看官網文檔。
截至目前,你所看到的全部日期操做都是相對比較直接的。有的時候,你須要進行一些更加複雜的操做,好比,將日期調整到下個週日、下個工做日,或者是本月的最後一天。這時,你可使用重載版本的with方法,向其傳遞一個提供了更多定製化選擇的TemporalAdjuster對象,更加靈活地處理日期。對於最多見的用例, 日期和時間API已經提供了大量預約義的TemporalAdjuster。你能夠經過TemporalAdjuster類的靜態工廠方法訪問它們,以下所示。
// 2018-11-17
LocalDate date1 = LocalDate.of(2018, 11, 17);
// 2018-11-19
LocalDate date2 = date1.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
// 2018-11-30
LocalDate date3 = date2.with(TemporalAdjusters.lastDayOfMonth());
複製代碼
正如咱們看到的,使用TemporalAdjuster咱們能夠進行更加複雜的日期操做,並且這些方法的名稱也很是直觀,方法名基本就是問題陳述。此外,即便你沒有找到符合你要求的預約義的TemporalAdjuster,建立你本身的TemporalAdjuster也並不是難事。實際上,TemporalAdjuster接口只聲明瞭單一的一個方法(這使得它成爲了一個函數式接口),定義以下。
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
複製代碼
這意味着TemporalAdjuster接口的實現須要定義如何將一個Temporal對象轉換爲另外一個Temporal對象。你能夠把它當作一個UnaryOperator。
你可能但願對你的日期時間對象進行的另一個通用操做是,依據你的業務領域以不一樣的格式打印輸出這些日期和時間對象。相似地,你可能也須要將那些格式的字符串轉換爲實際的日期對象。接下來的一節,咱們會演示新的日期和時間API提供那些機制是如何完成這些任務的。
處理日期和時間對象時,格式化以及解析日期時間對象是另外一個很是重要的功能。新的java.time.format包就是特別爲這個目的而設計的。這個包中,最重要的類是DateTimeFormatter。建立格式器最簡單的方法是經過它的靜態工廠方法以及常量。像BASIC_ISO_DATE和ISO_LOCAL_DATE 這樣的常量是DateTimeFormatter 類的預約義實例。全部的DateTimeFormatter實例都能用於以必定的格式建立表明特定日期或時間的字符串。好比,下面的這個例子中,咱們使用了兩個不一樣的格式器生成了字符串:
LocalDate date1 = LocalDate.of(2018, 11, 17);
// 20181117
String s1 = date1.format(DateTimeFormatter.BASIC_ISO_DATE);
// 2018-11-17
String s2 = date1.format(DateTimeFormatter.ISO_LOCAL_DATE);
複製代碼
你也能夠經過解析表明日期或時間的字符串從新建立該日期對象。全部的日期和時間API都提供了表示時間點或者時間段的工廠方法,你可使用工廠方法parse達到重創該日期對象的目的:
LocalDate date2 = LocalDate.parse("20181117", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date3 = LocalDate.parse("2018-11-17", DateTimeFormatter.ISO_LOCAL_DATE);
複製代碼
和老的java.util.DateFormat相比較,全部的DateTimeFormatter實例都是線程安全的。因此,你可以以單例模式建立格式器實例,就像DateTimeFormatter所定義的那些常量,並能在多個線程間共享這些實例。DateTimeFormatter類還支持一個靜態工廠方法,它能夠按照某個特定的模式建立格式器,代碼清單以下。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
// 17/11/2018
String formattedDate = date1.format(formatter);
LocalDate date4 = LocalDate.parse(formattedDate, formatter);
複製代碼
這段代碼中,LocalDate的formate方法使用指定的模式生成了一個表明該日期的字符串。緊接着,靜態的parse方法使用一樣的格式器解析了剛纔生成的字符串,並重建了該日期對象。ofPattern方法也提供了一個重載的版本,使用它你能夠建立某個Locale的格式器,代碼清單以下所示。
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
LocalDate date5 = LocalDate.of(2018, 11, 16);
// 16. novembre 2018
String formattedDate2 = date5.format(italianFormatter);
// 2018-11-16
LocalDate date6 = LocalDate.parse(formattedDate2, italianFormatter);
複製代碼
最後,若是你還須要更加細粒度的控制,DateTimeFormatterBuilder類還提供了更復雜的格式器,你能夠選擇恰當的方法,一步一步地構造本身的格式器。另外,它還提供了很是強大的解析功能,好比區分大小寫的解析、柔性解析(容許解析器使用啓發式的機制去解析輸入,不精確地匹配指定的模式)、填充, 以及在格式器中指定可選節。
好比, 你能夠經過DateTimeFormatterBuilder本身編程實現咱們在上面代碼中使用的italianFormatter,代碼清單以下。
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(". ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.YEAR)
.parseCaseInsensitive()
.toFormatter(Locale.ITALIAN);
LocalDate now = LocalDate.now();
// 17. novembre 2018
String s1 = now.format(italianFormatter);
複製代碼
目前爲止,你已經學習瞭如何建立、操縱、格式化以及解析時間點和時間段,可是你還不瞭解如何處理日期和時間之間的微妙關係。好比,你可能須要處理不一樣的時區,或者因爲不一樣的歷法系統帶來的差別。接下來的一節,咱們會探究如何使用新的日期和時間API解決這些問題。
以前你看到的日期和時間的種類都不包含時區信息。時區的處理是新版日期和時間API新增長的重要功能,使用新版日期和時間API時區的處理被極大地簡化了。新的java.time.ZoneId類是老版java.util.TimeZone的替代品。它的設計目標就是要讓你無需爲時區處理的複雜和繁瑣而操心,好比處理日光時(Daylight Saving Time,DST)這種問題。跟其餘日期和時間類同樣,ZoneId類也是沒法修改的。
時區是按照必定的規則將區域劃分紅的標準時間相同的區間。在ZoneRules這個類中包含了40個這樣的實例。你能夠簡單地經過調用ZoneId的getRules()獲得指定時區的規則。每一個特定的ZoneId對象都由一個地區ID標識,好比:
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
複製代碼
地區ID都爲「{區域}/{城市}」的格式,這些地區集合的設定都由英特網編號分配機構(IANA)的時區數據庫提供。你能夠經過Java 8的新方法toZoneId將一個老的時區對象轉換爲ZoneId:
ZoneId zoneId = TimeZone.getDefault().toZoneId();
複製代碼
一旦獲得一個ZoneId對象,你就能夠將它與LocalDate、LocalDateTime或者是Instant對象整合起來,構造爲一個ZonedDateTime實例,它表明了相對於指定時區的時間點,代碼清單以下所示。
LocalDate date = LocalDate.of(2018, 11, 17);
ZonedDateTime zdt1 = date.atStartOfDay(shanghaiZone);
LocalDateTime dateTime = LocalDateTime.of(2018, 11, 27, 18, 13, 15);
ZonedDateTime zdt2 = dateTime.atZone(shanghaiZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(shanghaiZone);
複製代碼
經過ZoneId,你還能夠將LocalDateTime轉換爲Instant:
LocalDateTime dateTime = LocalDateTime.of(2018, 11, 17, 18, 45);
Instant instantFromDateTime = dateTime.toInstant(shanghaiZone);
複製代碼
你也能夠經過反向的方式獲得LocalDateTime對象:
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, shanghaiZone);
複製代碼
另外一種比較通用的表達時區的方式是利用當前時區和UTC/格林尼治的固定誤差。好比,基於這個理論,你能夠說「紐約落後於倫敦5小時」。這種狀況下,你可使用ZoneOffset類,它是ZoneId的一個子類,表示的是當前時間和倫敦格林尼治子午線時間的差別:
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
複製代碼
「-05:00」的誤差實際上對應的是美國東部標準時間。注意,使用這種方式定義的ZoneOffset並未考慮任何日光時的影響,因此在大多數狀況下,不推薦使用。因爲ZoneOffset也是ZoneId,因此你能夠像上面的代碼那樣使用它。你甚至還能夠建立這樣的OffsetDateTime,它使用ISO-8601的歷法系統,以相對於UTC/格林尼治時間的誤差方式表示日期時間。
LocalDateTime dateTime = LocalDateTime.of(2018, 11, 17, 18, 45);
OffsetDateTime offsetDateTime = OffsetDateTime.of(dateTime, newYorkOffset);
複製代碼
能夠說《Java8實戰》的讀書筆記相關的已經寫完了,這本書後面還有最後一部分超越Java8,這一部分相關的章節都是跟函數式編程的思考與技巧相關,以及Java之後的將來等等。《Java8實戰》這本書真的寫的太好了並且這本書徹底能夠看成一本關於Java8使用的工具書,隨時能夠翻開看看,看看關於Java8的特性是如何使用,該如何去避免一些坑,該如何使用Stream和Lambda表達式去簡化你的代碼。
Gitee:chap12
Github:chap12