標籤:
伴隨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包裡的一些關鍵類:
- Instant:瞬時執行個體。
- LocalDate:本地日期,不包含具體時間 例如:2014-01-14 可以用來記錄生日、紀念日、加盟日等。
- LocalTime:本地時間,不包含日期。
- LocalDateTime:組合了日期和時間,但不包含時差和時區資訊。
- ZonedDateTime:最完整的日期時間,包含時區和相對UTC或格林威治的時差。
新API還引入了ZoneOffSet和ZoneId類,使得解決時區問題更為簡便。解析、格式化時間的DateTimeFormatter類也全部重新設 計。注意,這篇文章是我在一年前Java 8即將發布時寫的,以下範例程式碼中的時間都是那一年的,當運行這些例子時會返回你當前的時間。
在Java 8中如何處理日期和時間
常有人問我學習一個新庫的最好方式是什嗎?我的答案是在實際項目中使用它。項目中有很多真正的需求驅使開發人員去發掘並學習新庫。簡單得說就是任務驅 動學習探 索。這對Java 8新日期時間API也不例外。我建立了20個基於任務的執行個體來學習Java 8的新特性。從最簡單建立當天的日期開始,然後建立時間及時區,接著類比一個日期提醒應用中的任務——計算重要日期的到期天數,例如生日、紀念日、賬單 日、保費到期日、信用卡到期日等。
樣本 1、在Java 8中擷取今天的日期
Java 8 中的 LocalDate 用於表示當天日期。和java.util.Date不同,它只有日期,不包含時間。當你僅需要表示日期時就用這個類。
LocalDate today = LocalDate.now();System.out.println("Today‘s Local date : " + today);
OutputToday‘s Local date : 2014-01-14
上面的代碼建立了當天的日期,不含時間資訊。列印出的日期格式非常友好,不像老的Date類列印出一堆沒有格式化的資訊。
樣本 2、在Java 8中擷取年、月、日資訊
LocalDate類提供了擷取年、月、日的快捷方法,其執行個體還包含很多其它的日期屬性。通過調用這些方法就可以很方便的得到需要的日期資訊,不用像以前一樣需要依賴java.util.Calendar類了。
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);
OutputToday‘s Local date : 2014-01-14Year : 2014 Month : 1 day : 14
看到了吧,在Java 8 中得到年、月、日資訊是這麼簡單直觀,想用就用,沒什麼需要記的。對比看看以前Java是怎麼處理年月日資訊的吧。
樣本 3、在Java 8中處理特定日期
在 第一個例子裡,我們通過靜態Factory 方法now()非常容易地建立了當天日期,你還可以調用另一個有用的Factory 方法LocalDate.of()建立任意日期, 該方法需要傳入年、月、日做參數,返回對應的LocalDate執行個體。這個方法的好處是沒再犯老API的設計錯誤,比如年度起始於1900,月份是從0開 始等等。日期所見即所得 (WYSIWYG),就像下面這個例子表示了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日完全一致。
樣本 4、在Java 8中判斷兩個日期是否相等
現 實生活中有一類時間處理就是判斷兩個日期是否相等。你常常會檢查今天是不是個特殊的日子,比如生日、紀念日或非交易日。這時就需要把指定的日期與某個特定 日期做比較,例如判斷這一天是否是假期。下面這個例子會協助你用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);}
Outputtoday 2014-01-14 and date1 2014-01-14 are same date
這個例子中我們比較的兩個日期相同。注意,如果比較的日期是字元型的,需要先解析成日期對象再作判斷。對比Java老的日期比較方式,你會感到清風拂面。
樣本 5、在Java 8中檢查像生日這種周期性事件
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 !!
只要當天的日期和生日匹配,無論是哪一年都會列印出祝賀資訊。你可以把程式整合進系統時鐘,看看生日時是否會受到提醒,或者寫一個單元測試來檢測代碼是否運行正確。
樣本 6、在Java 8中擷取目前時間
與Java 8擷取日期的例子很像,擷取時間使用的是LocalTime類,一個只有時間沒有日期的LocalDate近親。可以調用靜態Factory 方法now()來擷取目前時間。預設的格式是hh:mm:ss:nnn。對比一下Java 8之前擷取目前時間的方式。
LocalTime time = LocalTime.now();System.out.println("local time now : " + time);
Outputlocal time now : 16:33:33.369 // in hour, minutes, seconds, nano seconds
可以看到目前時間就只包含時間資訊,沒有日期。
樣本 7、如何在現有的時間上增加小時
通過增加小時、分、秒來計算將來的時間很常見。Java 8除了不變類型和安全執行緒的好處之外,還提供了更好的plusHours()方法替換add(),並且是相容的。注意,這些方法返回一個全新的LocalTime執行個體,由於其不可變性,返回後一定要用變數賦值。
LocalTime time = LocalTime.now();LocalTime newTime = time.plusHours(2); // adding two hoursSystem.out.println("Time after 2 hours : " + newTime);
Output :Time after 2 hours : 18:33:33.369
可以看到,新的時間在目前時間16:33:33.369的基礎上增加了2個小時。和舊版Java的增減時間的處理方式對比一下,看看哪種更好。
樣本 8、如何計算一周后的日期
和上個例子計算兩小時以後的時間類似,這個例子會計算一周后的日期。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-14Date after 1 week : 2014-01-21
可以看到新日期離當天日期是7天,也就是一周。你可以用同樣的方法增加1個月、1年、1小時、1分鐘甚至一個世紀,更多選項可以查看Java 8 API中的ChronoUnit類。
樣本 9、計算一年前或一年後的日期
繼續上面的例子,上個例子中我們通過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-14Date after 1 year : 2015-01-14
例子結果得到了兩個日期,一個2013年、一個2015年、分別是2014年的前一年和後一年。
樣本 10、使用Java 8的Clock時鐘類
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 zoneClock 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)) { ... } }}
這種方式在不同時區下處理日期時會非常管用。
樣本 11、如何用Java判斷日期是早於還是晚於另一個日期
另一個工作中常見的操作就是如何判斷給定的一個日期是大於某天還是小於某天?在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 todayYesterday is day before today
在Java 8中比較日期非常方便,不需要使用額外的Calendar類來做這些基礎工作了。
樣本 12、在Java 8中處理時區
Java 8不僅分離了日期和時間,也把時區分離出來了。現在有一系列單獨的類如ZoneId來處理特定時區,ZoneDateTime類來表示某時區下的時間。這在Java 8以前都是 GregorianCalendar類來做的。下面這個例子展示了如何把本時區的時間轉換成另一個時區的時間。
// Date and time with timezone in Java 8ZoneId 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)
樣本 13、如何表示信用卡到期這類固定日期,答案就在YearMonth
與 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: 31Your credit card expires on 2018-02
根據上述資料,你可以提醒客戶信用卡快要到期了,個人認為這個類非常有用。
樣本 14、如何在Java 8中檢查閏年
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單元測試做判斷。
樣本 15、計算兩個日期之間的天數和月數
有一個常見日期操作是計算兩個日期之間的天數、周數或月數。在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月,中間相隔兩個月。
樣本 16、包含時差資訊的日期和時間
在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則對人更友好。
樣本 17、在Java 8中擷取當前的時間戳記
如果你還記得Java 8以前是如何獲得目前時間戳,那麼現在你終於解脫了。Instant類有一個靜態Factory 方法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類。
樣本 18、在Java 8中如何使用預定義的格式化工具去解析或格式化日期
在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
很明顯的看出得到的日期和給出的日期是同一天,但是格式不同。
樣本 19、如何在Java中使用自訂格式化工具解析日期
上 個例子使用了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
日期值與傳入的字串是匹配的,只是格式不同而已。
樣本 20、在Java 8中如何把日期轉換成字串
上 兩個例子都用到了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的重點
通過這些例子,你肯定已經掌握了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可以作為處理日期時間操作的標準。
Java 8時間和日期API 20例