除了lambda表達式,stream以及幾個小的改進以外,Java 8還引入了一套全新的時間日期API,在本篇教程中咱們將經過幾個簡單的任務示例來學習如何使用Java 8的這套API。Java對日期,日曆及時間的處理一直以來都飽受詬病,尤爲是它決定將 java.util.Date 定義爲可修改的以及將SimpleDateFormat實現成非線程安全的。html
看來Java已經意識到須要爲時間及日期功能提供更好的支持了,這對已經習慣使用Joda時間日期庫的社區而言也是件好事。關於這個新的時間日期庫的最大的優勢就在於它定義清楚了時間日期相關的一些概念,比方說,瞬時時間(Instant),持續時間(duration),日期(date),時間(time),時區(time-zone)以及時間段(Period)。同時它也借鑑了Joda庫的一些優勢,好比將人和機器對時間日期的理解區分開的。Java 8仍然延用了ISO的日曆體系,而且與它的前輩們不一樣,java.time包中的類是不可變且線程安全的。新的時間及日期API位於java.time包中,下面是裏面的一些關鍵的類:java
新的庫還增長了ZoneOffset及Zoned,能夠爲時區提供更好的支持。有了新的DateTimeFormatter以後日期的格式化也變得面目一新了。隨便提一句,我是在去年這個時候Java正要推出這個新功能時寫的這篇文章,因此你會發現示例中的時間都仍是去年的。你運行下這些例子,它們返回的值確定都是正確的。api
有人問我學習一個新庫的最佳途徑是什麼?個人回答是,就是在實際項目中那樣去使用它。在一個真實的項目中會有各類各樣的需求,這會促使開發人員去探索和研究這個新庫。簡言之,只有任務自己纔會真正促使你去探索及學習。java 8的新的日期及時間API也是同樣。爲了學習Java 8的這個新庫,這裏我建立了20個以任務爲導向的例子。咱們先從一個簡單的任務開始,好比說如何用Java 8的時間日期庫來表示今天,接着再進一步生成一個帶時間及時區的完整日期,而後再研究下如何完成一些更實際的任務,好比說開發一個提醒類的應用,來找出距離一些特定日期好比生日,週日記念日,下一個賬單日,下一個溢價日或者信用卡過時時間還有多少天。安全
Java 8中有一個叫LocalDate的類,它能用來表示今天的日期。這個類與java.util.Date略有不一樣,由於它只包含日期,沒有時間。所以,若是你只須要表示日期而不包含時間,就可使用它。bash
LocalDate today = LocalDate.now();
System.out.println("Today's Local date : " + today);
輸出
Today's Local date : 2018-02-11 複製代碼
你能夠看到它建立了今天的日期卻不包含時間信息。它還將日期格式化完了再輸出出來,不像以前的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);
輸出
Today's Local date : 2018-02-11 Year : 2018 Month : 2 day : 11 複製代碼
能夠看到,在Java 8中獲取年月信息很是簡單,只需使用對應的getter方法就行了,無需記憶,很是直觀。你能夠拿它和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);
輸出 : Your Date of birth is : 2010-01-14
複製代碼
能夠看出,建立出來的日期就是咱們所寫的那樣,2014年1月14日。學習
若是提及現實中實際的處理時間及日期的任務,有一個常見的就是要檢查兩個日期是否相等。你可能常常會碰到要判斷今天是否是某個特殊的日子,好比生日啊,週年記念日啊,或者假期之類。有的時候,會給你一個日期,讓你檢查它是否是某個日子比方說假日。下面這個例子將會幫助你在Java 8中完成這類任務。正如你所想的那樣,LocalDate重寫了equals方法來進行日期的比較,以下所示:
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);
}
輸出
today 2014-01-14 and date1 2014-01-14 are same date
複製代碼
在本例中咱們比較的兩個日期是相等的。同時,若是在代碼中你拿到了一個格式化好的日期串,你得先將它解析成日期而後才能比較。你能夠將這個例子與Java以前比較日期的方式進行下比較,你會發現它真是爽多了。
在Java中還有一個與時間日期相關的實際任務就是檢查重複事件,好比說每個月的賬單日,結婚記念日,每個月還款日或者是每一年交保險費的日子。若是你在一家電商公司工做的話,那麼確定會有這麼一個模塊,會去給用戶發送生日祝福而且在每個重要的假日給他們捎去問候,好比說聖誕節,感恩節,在印度則多是萬燈節(Deepawali)。如何在Java中判斷是不是某個節日或者重複事件?使用MonthDay類。這個類由月日組合,不包含年信息,也就是說你能夠用它來表明每一年重複出現的一些日子。固然也有一些別的組合,好比說YearMonth類。它和新的時間日期庫中的其它類同樣也都是不可變且線程安全的,而且它仍是一個值類(value class)。咱們經過一個例子來看下如何使用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");
}
輸出: Many Many happy returns of the day !!
複製代碼
雖然年不一樣,但今天就是生日的那天,因此在輸出那裏你會看到一條生日祝福。你能夠調整下系統的時間再運行下這個程序看看它是否能提醒你下一個生日是何時,你還能夠試着用你的下一個生日來編寫一個JUnit單元測試看看代碼可否正確運行。
這與第一個例子中獲取當前日期很是類似。此次咱們用的是一個叫LocalTime的類,它是沒有日期的時間,與LocalDate是近親。這裏你也能夠用靜態工廠方法now()來獲取當前時間。默認的格式是hh:mm:ss:nnn,這裏的nnn是納秒。能夠和Java 8之前如何獲取當前時間作一下比較。
LocalTime time = LocalTime.now();
System.out.println("local time now : " + time);
輸出
local time now : 16:33:33.369 // in hour, minutes, seconds, nano seconds
複製代碼
能夠看到,當前時間是不包含日期的,由於LocalTime只有時間,沒有日期。
不少時候咱們須要增長小時,分或者秒來計算出未來的時間。Java 8不只提供了不可變且線程安全的類,它還提供了一些更方便的方法譬如plusHours()來替換原來的add()方法。順便說一下,這些方法返回的是一個新的LocalTime實例的引用,由於LocalTime是不可變的,可別忘了存儲好這個新的引用。
LocalTime time = LocalTime.now();
LocalTime newTime = time.plusHours(2); // adding two hours
System.out.println("Time after 2 hours : " + newTime);
輸出 :
Time after 2 hours : 18:33:33.369
複製代碼
能夠看到當前時間2小時後是16:33:33.369。如今你能夠將它和Java中增長或者減小小時的老的方式進行下比較。一看便知哪一種方式更好。
這與前一個獲取2小時後的時間的例子相似,這裏咱們將學會如何獲取到1周後的日期。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);
輸出:
Today is : 2018-01-14
Date after 1 week : 2018-01-21
複製代碼
能夠看到7天也就是一週後的日期是什麼。你能夠用這個方法來增長一個月,一年,一小時,一分鐘,甚至是十年,查看下Java 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);
輸出:
Date before 1 year : 2013-01-14
Date after 1 year : 2015-01-14
複製代碼
能夠看到如今一共有兩年,一個是2013年,一個是2015年,分別是2014的先後那年。
Java 8中自帶了一個Clock類,你能夠用它來獲取某個時區下當前的瞬時時間,日期或者時間。能夠用Clock來替代System.currentTimeInMillis()與 TimeZone.getDefault() 方法。
// 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);
輸出:
Clock : SystemClock[Z]
Clock : SystemClock[Z]
複製代碼
你能夠用指定的日期來和這個時鐘進行比較,好比下面這樣:
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");
}
輸出:
Tomorrow comes after today
Yesterday is day before today
複製代碼
能夠看到在Java 8中進行日期比較很是簡單。不須要再用像Calendar這樣的另外一個類來完成相似的任務了。
Java 8不只將日期和時間進行了分離,同時還有時區。如今已經有好幾組與時區相關的類了,好比ZonId表明的是某個特定的時區,而ZonedDateTime表明的是帶時區的時間。它等同於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);
輸出 :
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又是另外一個組合,它表明的是像信用卡還款日,按期存款到期日,options到期日這類的日期。你能夠用這個類來找出那個月有多少天,lengthOfMonth()這個方法返回的是這個YearMonth實例有多少天,這對於檢查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);
輸出:
Days in month year 2014-01: 31
Your credit card expires on 2018-02
複製代碼
這並沒什麼複雜的,LocalDate類有一個isLeapYear()的方法可以返回當前LocalDate對應的那年是不是閏年。若是你還想重複造輪子的話,能夠看下這段代碼,這是純用Java編寫的判斷某年是不是閏年的邏輯。
if(today.isLeapYear()){
System.out.println("This year is Leap year");
}else {
System.out.println("2018 is not a Leap year");
}
輸出: 2018 is not a Leap year
複製代碼
你能夠多檢查幾年看看結果是否正確,最好寫一個單元測試來對正常年份和閏年進行下測試。
還有一個常見的任務就是計算兩個給定的日期之間包含多少天,多少周或者多少年。你能夠用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() );
輸出:
Months left between today and Java 8 release : 2
複製代碼
能夠看到,本月是1月,而Java 8的發佈日期是3月,所以中間隔着2個月。
在Java 8裏面,你能夠用ZoneOffset類來表明某個時區,好比印度是GMT或者UTC5:30,你可使用它的靜態方法ZoneOffset.of()方法來獲取對應的時區。只要獲取到了這個偏移量,你就能夠拿LocalDateTime和這個偏移量建立出一個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);
輸出 :
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);
輸出 :
What is value of this instant 2014-01-14T08:33:33.379Z
複製代碼
能夠看出,當前時間戳是包含日期與時間的,與java.util.Date很相似,事實上Instant就是Java 8前的Date,你可使用這兩個類中的方法來在這兩個類型之間進行轉換,好比Date.from(Instant)是用來將Instant轉換成java.util.Date的,而Date.toInstant()是將Date轉換成Instant的。
在Java 8以前,時間日期的格式化但是個技術活,咱們的好夥伴SimpleDateFormat並非線程安全的,而若是用做本地變量來格式化的話又顯得有些笨重。多虧了線程本地變量,這使得它在多線程環境下也算有了用武之地,但Java維持這一狀態也有很長一段時間了。此次它引入了一個全新的線程安全的日期與時間格式器。它還自帶了一些預約義好的格式器,包含了經常使用的日期格式。好比說,本例 中咱們就用了預約義的BASIC_ISO_DATE格式,它會將2014年2月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);
輸出 :
Date generated from String 20140116 is 2014-01-16
複製代碼
你能夠看到生成的日期與指定字符串的值是匹配的,就是日期格式上略有不一樣。
在上例中,咱們使用了內建的時間日期格式器來解析日期字符串。固然了,預約義的格式器的確不錯但有時候你可能仍是須要使用自定義的日期格式,這個時候你就得本身去建立一個自定義的日期格式器實例了。下面這個例子中的日期格式是」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();
}
輸出 :
Successfully parsed String Apr 18 2014, date is 2014-04-18
複製代碼
能夠看到日期的值與傳入的字符串的確是相符的,只是格式不一樣。
在上兩個例子中,儘管咱們用到了DateTimeFormatter類但咱們主要是進行日期字符串的解析。在這個例子中咱們要作的事情正好相反。這裏咱們有一個LocalDateTime類的實例,咱們要將它轉換成一個格式化好的日期串。這是目前爲止Java中將日期轉換成字符串最簡單便捷的方式了。下面這個例子將會返回一個格式化好的字符串。與前例相同的是,咱們仍需使用指定的模式串去建立一個DateTimeFormatter類的實例,但調用的並非LocalDate類的parse方法,而是它的format()方法。這個方法會返回一個表明當前日期的字符串,對應的模式就是傳入的DateTimeFormatter實例中所定義好的。
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」模式來表示的,它包含了三個字母表示的月份以及用AM及PM來表示的時間。
看完了這些例子後,我相信你已經對Java 8這套新的時間日期API有了必定的瞭解了。如今咱們來回顧下關於這個新的API的一些關鍵的要素。
關於Java 8這個新的時間日期API就講到這了。這幾個簡短的示例 對於理解這套新的API中的一些新增類已經足夠了。因爲它是基於實際任務來說解的,所以後面再遇到Java中要對時間與日期進行處理的工做時,就不用再四處尋找了。咱們學習瞭如何建立與修改日期實例。咱們還了解了純日期,日期加時間,日期加時區的區別,知道如何比較兩個日期,如何找到某天到指定日期好比說下一個生日,週年記念日或者保險日還有多少天。咱們還學習瞭如何在Java 8中用線程安全的方式對日期進行解析及格式化,而無需再使用線程本地變量或者第三方庫這種取巧的方式。新的API能勝任任何與時間日期相關的任務。
譯文出處: 花名有孚
原文出處: javarevisited