伴隨lambda表達式、streams以及一系列小優化,Java 8 推出了全新的日期時間API,在教程中咱們將經過一些簡單的實例來學習如何使用新API。Java處理日期、日曆和時間的方式一直爲社區所詬病,將 java.util.Date設定爲可變類型,以及SimpleDateFormat的非線程安全使其應用很是受限。Java也意識到須要一個更好的 API來知足社區中已經習慣了使用JodaTime API的人們。全新API的衆多好處之一就是,明確了日期時間概念,例如:瞬時(instant)、 長短(duration)、日期、時間、時區和週期。同時繼承了Joda庫按人類語言和計算機各自解析的時間處理方式。不一樣於老版本,新API基於ISO 標準日曆系統,java.time包下的全部類都是不可變類型並且線程安全。下面是新版API中java.time包裏的一些關鍵類: html
新API還引入了ZoneOffSet和ZoneId類,使得解決時區問題更爲簡便。解析、格式化時間的DateTimeFormatter類也所有從新設 計。注意,這篇文章是我在一年前Java 8即將發佈時寫的,如下示例代碼中的時間都是那一年的,當運行這些例子時會返回你當前的時間。 java
常有人問我學習一個新庫的最好方式是什麼?個人答案是在實際項目中使用它。項目中有不少真正的需求驅使開發者去發掘並學習新庫。簡單得說就是任務驅 動學習探 索。這對Java 8新日期時間API也不例外。我建立了20個基於任務的實例來學習Java 8的新特性。從最簡單建立當天的日期開始,而後建立時間及時區,接着模擬一個日期提醒應用中的任務——計算重要日期的到期天數,例如生日、記念日、帳單 日、保費到期日、信用卡過時日等。 編程
Java 8 中的 LocalDate 用於表示當天日期。和java.util.Date不一樣,它只有日期,不包含時間。當你僅須要表示日期時就用這個類。 安全
LocalDate today = LocalDate.now(); System.out.println("Today's Local date : " + today);
Output Today's Local date : 2014-01-14
上面的代碼建立了當天的日期,不含時間信息。打印出的日期格式很是友好,不像老的Date類打印出一堆沒有格式化的信息。 多線程
LocalDate類提供了獲取年、月、日的快捷方法,其實例還包含不少其它的日期屬性。經過調用這些方法就能夠很方便的獲得須要的日期信息,不用像之前同樣須要依賴java.util.Calendar類了。 app
LocalDate today = LocalDate.now(); int year = today.getYear(); int month = today.getMonthValue(); int day = today.getDayOfMonth(); System.out.printf("Year : %d Month : %d day : %d t %n", year, month, day);
Output Today's Local date : 2014-01-14 Year : 2014 Month : 1 day : 14
看到了吧,在Java 8 中獲得年、月、日信息是這麼簡單直觀,想用就用,沒什麼須要記的。對比看看之前Java是怎麼處理年月日信息的吧。 ide
在 第一個例子裏,咱們經過靜態工廠方法now()很是容易地建立了當天日期,你還能夠調用另外一個有用的工廠方法LocalDate.of()建立任意日期, 該方法須要傳入年、月、日作參數,返回對應的LocalDate實例。這個方法的好處是沒再犯老API的設計錯誤,好比年度起始於1900,月份是從0開 始等等。日期所見即所得,就像下面這個例子表示了1月14日,沒有任何隱藏機關。 工具
LocalDate dateOfBirth = LocalDate.of(2010, 01, 14); System.out.println("Your Date of birth is : " + dateOfBirth);
Output : Your Date of birth is : 2010-01-14
能夠看到建立的日期徹底符合預期,與你寫入的2010年1月14日徹底一致。 單元測試
現 實生活中有一類時間處理就是判斷兩個日期是否相等。你經常會檢查今天是否是個特殊的日子,好比生日、記念日或非交易日。這時就須要把指定的日期與某個特定 日期作比較,例如判斷這一天是不是假期。下面這個例子會幫助你用Java 8的方式去解決,你確定已經想到了,LocalDate重載了equal方法,請看下面的例子: 學習
LocalDate date1 = LocalDate.of(2014, 01, 14); if(date1.equals(today)){ System.out.printf("Today %s and date1 %s are same date %n", today, date1); }
Output today 2014-01-14 and date1 2014-01-14 are same date
這個例子中咱們比較的兩個日期相同。注意,若是比較的日期是字符型的,須要先解析成日期對象再做判斷。對比Java老的日期比較方式,你會感到清風拂面。
Java 中另外一個日期時間的處理就是檢查相似每個月帳單、結婚記念日、EMI日或保險繳費日這些週期性事件。若是你在電子商務網站工做,那麼必定會有一個模塊用來在 聖誕節、感恩節這種節日時向客戶發送問候郵件。Java中如何檢查這些節日或其它週期性事件呢?答案就是MonthDay類。這個類組合了月份和日,去掉 了年,這意味着你能夠用它判斷每一年都會發生事件。和這個類類似的還有一個YearMonth類。這些類也都是不可變而且線程安全的值類型。下面咱們經過 MonthDay來檢查週期性事件:
LocalDate dateOfBirth = LocalDate.of(2010, 01, 14); MonthDay birthday = MonthDay.of(dateOfBirth.getMonth(), dateOfBirth.getDayOfMonth()); MonthDay currentMonthDay = MonthDay.from(today); if(currentMonthDay.equals(birthday)){ System.out.println("Many Many happy returns of the day !!"); }else{ System.out.println("Sorry, today is not your birthday"); }
Output: Many Many happy returns of the day !!
只要當天的日期和生日匹配,不管是哪一年都會打印出祝賀信息。你能夠把程序整合進系統時鐘,看看生日時是否會受到提醒,或者寫一個單元測試來檢測代碼是否運行正確。
與Java 8獲取日期的例子很像,獲取時間使用的是LocalTime類,一個只有時間沒有日期的LocalDate近親。能夠調用靜態工廠方法now()來獲取當前時間。默認的格式是hh:mm:ss:nnn。對比一下Java 8以前獲取當前時間的方式。
LocalTime time = LocalTime.now(); System.out.println("local time now : " + time);
Output local time now : 16:33:33.369 // in hour, minutes, seconds, nano seconds
能夠看到當前時間就只包含時間信息,沒有日期。
經過增長小時、分、秒來計算未來的時間很常見。Java 8除了不變類型和線程安全的好處以外,還提供了更好的plusHours()方法替換add(),而且是兼容的。注意,這些方法返回一個全新的LocalTime實例,因爲其不可變性,返回後必定要用變量賦值。
LocalTime time = LocalTime.now(); LocalTime newTime = time.plusHours(2); // adding two hours System.out.println("Time after 2 hours : " + newTime);
Output : Time after 2 hours : 18:33:33.369
能夠看到,新的時間在當前時間16:33:33.369的基礎上增長了2個小時。和舊版Java的增減時間的處理方式對比一下,看看哪一種更好。
和上個例子計算兩小時之後的時間相似,這個例子會計算一週後的日期。LocalDate日期不包含時間信息,它的plus()方法用來增長天、周、月,ChronoUnit類聲明瞭這些時間單位。因爲LocalDate也是不變類型,返回後必定要用變量賦值。
LocalDate nextWeek = today.plus(1, ChronoUnit.WEEKS); System.out.println("Today is : " + today); System.out.println("Date after 1 week : " + nextWeek);
Output: Today is : 2014-01-14 Date after 1 week : 2014-01-21
能夠看到新日期離當天日期是7天,也就是一週。你能夠用一樣的方法增長1個月、1年、1小時、1分鐘甚至一個世紀,更多選項能夠查看Java 8 API中的ChronoUnit類。
繼續上面的例子,上個例子中咱們經過LocalDate的plus()方法增長天數、週數或月數,這個例子咱們利用minus()方法計算一年前的日期。
LocalDate previousYear = today.minus(1, ChronoUnit.YEARS); System.out.println("Date before 1 year : " + previousYear); LocalDate nextYear = today.plus(1, YEARS); System.out.println("Date after 1 year : " + nextYear);
Output: Date before 1 year : 2013-01-14 Date after 1 year : 2015-01-14
例子結果獲得了兩個日期,一個2013年、一個2015年、分別是2014年的前一年和後一年。
Java 8增長了一個Clock時鐘類用於獲取當時的時間戳,或當前時區下的日期時間信息。之前用到System.currentTimeInMillis()和TimeZone.getDefault()的地方均可用Clock替換。
// Returns the current time based on your system clock and set to UTC. Clock clock = Clock.systemUTC(); System.out.println("Clock : " + clock); // Returns time based on system clock zone Clock defaultClock = Clock.systemDefaultZone(); System.out.println("Clock : " + clock);
Output: Clock : SystemClock[Z] Clock : SystemClock[Z]還能夠針對clock時鐘作比較,像下面這個例子:
public class MyClass { private Clock clock; // dependency inject ... public void process(LocalDate eventDate) { if (eventDate.isBefore(LocalDate.now(clock)) { ... } } }
這種方式在不一樣時區下處理日期時會很是管用。
另外一個工做中常見的操做就是如何判斷給定的一個日期是大於某天仍是小於某天?在Java 8中,LocalDate類有兩類方法isBefore()和isAfter()用於比較日期。調用isBefore()方法時,若是給定日期小於當前日期則返回true。
LocalDate tomorrow = LocalDate.of(2014, 1, 15); if(tommorow.isAfter(today)){ System.out.println("Tomorrow comes after today"); } LocalDate yesterday = today.minus(1, DAYS); if(yesterday.isBefore(today)){ System.out.println("Yesterday is day before today"); }
Output: Tomorrow comes after today Yesterday is day before today
在Java 8中比較日期很是方便,不須要使用額外的Calendar類來作這些基礎工做了。
Java 8不只分離了日期和時間,也把時區分離出來了。如今有一系列單獨的類如ZoneId來處理特定時區,ZoneDateTime類來表示某時區下的時間。這在Java 8之前都是 GregorianCalendar類來作的。下面這個例子展現瞭如何把本時區的時間轉換成另外一個時區的時間。
// Date and time with timezone in Java 8 ZoneId america = ZoneId.of("America/New_York"); LocalDateTime localtDateAndTime = LocalDateTime.now(); ZonedDateTime dateAndTimeInNewYork = ZonedDateTime.of(localtDateAndTime, america ); System.out.println("Current date and time in a particular timezone : " + dateAndTimeInNewYork);
Output : Current date and time in a particular timezone : 2014-01-14T16:33:33.373-05:00[America/New_York]和 之前使用GMT的方式轉換本地時間對比一下。注意,在Java 8之前,必定要緊緊記住時區的名稱,否則就會拋出下面的異常:
Exception in thread "main" java.time.zone.ZoneRulesException: Unknown time-zone ID: ASIA/Tokyo at java.time.zone.ZoneRulesProvider.getProvider(ZoneRulesProvider.java:272) at java.time.zone.ZoneRulesProvider.getRules(ZoneRulesProvider.java:227) at java.time.ZoneRegion.ofId(ZoneRegion.java:120) at java.time.ZoneId.of(ZoneId.java:403) at java.time.ZoneId.of(ZoneId.java:351)
與 MonthDay檢查重複事件的例子類似,YearMonth是另外一個組合類,用於表示信用卡到期日、FD到期日、期貨期權到期日等。還能夠用這個類獲得 當月共有多少天,YearMonth實例的lengthOfMonth()方法能夠返回當月的天數,在判斷2月有28天仍是29天時很是有用。
YearMonth currentYearMonth = YearMonth.now(); System.out.printf("Days in month year %s: %d%n", currentYearMonth, currentYearMonth.lengthOfMonth()); YearMonth creditCardExpiry = YearMonth.of(2018, Month.FEBRUARY); System.out.printf("Your credit card expires on %s %n", creditCardExpiry);
Output: Days in month year 2014-01: 31 Your credit card expires on 2018-02
根據上述數據,你能夠提醒客戶信用卡快要到期了,我的認爲這個類很是有用。
LocalDate類有一個很實用的方法isLeapYear()判斷該實例是不是一個閏年,若是你仍是想從新發明輪子,這有一個代碼示例,純Java邏輯編寫的判斷閏年的程序。
if(today.isLeapYear()){ System.out.println("This year is Leap year"); }else { System.out.println("2014 is not a Leap year"); }
Output: 2014 is not a Leap year
你能夠多寫幾個日期來驗證是不是閏年,最好是寫JUnit單元測試作判斷。
有一個常見日期操做是計算兩個日期之間的天數、週數或月數。在Java 8中能夠用java.time.Period類來作計算。下面這個例子中,咱們計算了當天和未來某一天之間的月數。
LocalDate java8Release = LocalDate.of(2014, Month.MARCH, 14); Period periodToNextJavaRelease = Period.between(today, java8Release); System.out.println("Months left between today and Java 8 release : " + periodToNextJavaRelease.getMonths() );
Output: Months left between today and Java 8 release : 2
從上面能夠看到如今是一月,Java 8的發佈日期是3月,中間相隔兩個月。
在Java 8中,ZoneOffset類用來表示時區,舉例來講印度與GMT或UTC標準時區相差+05:30,能夠經過ZoneOffset.of()靜態方法來 獲取對應的時區。一旦獲得了時差就能夠經過傳入LocalDateTime和ZoneOffset來建立一個OffSetDateTime對象。
LocalDateTime datetime = LocalDateTime.of(2014, Month.JANUARY, 14, 19, 30); ZoneOffset offset = ZoneOffset.of("+05:30"); OffsetDateTime date = OffsetDateTime.of(datetime, offset); System.out.println("Date and Time with timezone offset in Java : " + date);
Output : Date and Time with timezone offset in Java : 2014-01-14T19:30+05:30
如今的時間信息裏已經包含了時區信息了。注意:OffSetDateTime是對計算機友好的,ZoneDateTime則對人更友好。
若是你還記得Java 8之前是如何得到當前時間戳,那麼如今你終於解脫了。Instant類有一個靜態工廠方法now()會返回當前的時間戳,以下所示:
Instant timestamp = Instant.now(); System.out.println("What is value of this instant " + timestamp);
Output : What is value of this instant 2014-01-14T08:33:33.379Z
時間戳信息裏同時包含了日期和時間,這和java.util.Date很像。實際上Instant類確實等同於 Java 8以前的Date類,你可使用Date類和Instant類各自的轉換方法互相轉換,例如:Date.from(Instant) 將Instant轉換成java.util.Date,Date.toInstant()則是將Date類轉換成Instant類。
在Java 8之前的世界裏,日期和時間的格式化很是詭異,惟一的幫助類SimpleDateFormat也是非線程安全的,並且用做局部變量解析和格式化日期時顯得 很笨重。幸虧線程局部變量能使它在多線程環境中變得可用,不過這都是過去時了。Java 8引入了全新的日期時間格式工具,線程安全並且使用方便。它自帶了一些經常使用的內置格式化工具。下面這個例子使用了BASIC_ISO_DATE格式化工具 將2014年1月14日格式化成20140114。
String dayAfterTommorrow = "20140116"; LocalDate formatted = LocalDate.parse(dayAfterTommorrow, DateTimeFormatter.BASIC_ISO_DATE); System.out.printf("Date generated from String %s is %s %n", dayAfterTommorrow, formatted);
Output : Date generated from String 20140116 is 2014-01-16
很明顯的看出獲得的日期和給出的日期是同一天,可是格式不一樣。
上 個例子使用了Java內置的格式化工具去解析日期字符串。 儘管內置格式化工具很好用,有時仍是須要定義特定的日期格式,下面這個例子展現瞭如何建立自定義 日期格式化工具。例子中的日期格式是「MMM dd yyyy」。能夠調用DateTimeFormatter的 ofPattern()靜態方法並傳入任意格式返回其實例,格式中的字符和之前表明的同樣,M 表明月,m表明分。若是格式不規範會拋出 DateTimeParseException異常,不過若是隻是把M寫成m這種邏輯錯誤是不會拋異常的。
String goodFriday = "Apr 18 2014"; try { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM dd yyyy"); LocalDate holiday = LocalDate.parse(goodFriday, formatter); System.out.printf("Successfully parsed String %s, date is %s%n", goodFriday, holiday); } catch (DateTimeParseException ex) { System.out.printf("%s is not parsable!%n", goodFriday); ex.printStackTrace(); }
Output : Successfully parsed String Apr 18 2014, date is 2014-04-18
日期值與傳入的字符串是匹配的,只是格式不一樣而已。
上 兩個例子都用到了DateTimeFormatter類,主要是從字符串解析日期。如今咱們反過來,把LocalDateTime日期實例轉換成特定格式的字符串。 這是迄今爲止Java日期轉字符串最爲簡單的方式了。下面的例子將返回一個表明日期的格式化字符串。和前面相似,仍是須要建立 DateTimeFormatter實例並傳入格式,但這回調用的是format()方法,而非parse()方法。這個方法會把傳入的日期轉化成指定格 式的字符串。
LocalDateTime arrivalDate = LocalDateTime.now(); try { DateTimeFormatter format = DateTimeFormatter.ofPattern("MMM dd yyyy hh:mm a"); String landing = arrivalDate.format(format); System.out.printf("Arriving at : %s %n", landing); } catch (DateTimeException ex) { System.out.printf("%s can't be formatted!%n", arrivalDate); ex.printStackTrace(); }
Output : Arriving at : Jan 14 2014 04:33 PM
當前時間被指定的「MMM dd yyyy hh:mm a」格式格式化,格式包含3個表明月的字符串,時間後面帶有AM和PM標記。
經過這些例子,你確定已經掌握了Java 8日期時間API的新知識點。如今咱們來回顧一下這個優雅API的使用要點:
1)提供了javax.time.ZoneId 獲取時區。
2)提供了LocalDate和LocalTime類。
3)Java 8 的全部日期和時間API都是不可變類而且線程安全,而現有的Date和Calendar API中的java.util.Date和SimpleDateFormat是非線程安全的。
4)主包是 java.time,包含了表示日期、時間、時間間隔的一些類。裏面有兩個子包java.time.format用於格式化, java.time.temporal用於更底層的操做。
5)時區表明了地球上某個區域內廣泛使用的標準時間。每一個時區都有一個代號,格式一般由區域/城市構成(Asia/Tokyo),在加上與格林威治或 UTC的時差。例如:東京的時差是+09:00。
6)OffsetDateTime類實際上組合了LocalDateTime類和ZoneOffset類。用來表示包含和格林威治或UTC時差的完整日期(年、月、日)和時間(時、分、秒、納秒)信息。
7)DateTimeFormatter 類用來格式化和解析時間。與SimpleDateFormat不一樣,這個類不可變而且線程安全,須要時能夠給靜態常量賦值。 DateTimeFormatter類提供了大量的內置格式化工具,同時也容許你自定義。在轉換方面也提供了parse()將字符串解析成日期,若是解析 出錯會拋出DateTimeParseException。DateTimeFormatter類同時還有format()用來格式化日期,若是出錯會拋 出DateTimeException異常。
8)再補充一點,日期格式「MMM d yyyy」和「MMM dd yyyy」有一些微妙的不一樣,第一個格式能夠解析「Jan 2 2014」和「Jan 14 2014」,而第二個在解析「Jan 2 2014」就會拋異常,由於第二個格式裏要求日必須是兩位的。若是想修正,你必須在日期只有個位數時在前面補零,就是說「Jan 2 2014」應該寫成 「Jan 02 2014」。
如何使用Java 8的全新日期時間API就介紹到這了。這些簡單的例子對幫助理解新API很是有用。因爲這些 例子都基於真實任務,你在作Java日期編程時不用再東張西望了。咱們學會了如何建立並操做日期實例,學習了純日期、以及包含時間信息和時差信息的日期、 學會了怎樣計算兩個日期的間隔,這些在計算當天與某個特定日期間隔的例子中都有所展現。 咱們還學到了在Java 8中如何線程安全地解析和格式化日期,不用再使用蹩腳的線程局部變量技巧,也不用依賴Joda Time第三方庫。新API能夠做爲處理日期時間操做的標準。