Joda-Time
上節介紹了JDK API中的日期和時間類,我們提到了JDK API的一些不足,並提到,實踐中有一個廣泛使用的日期和時間類庫,Joda-Time,本節我們就來介紹Joda-Time。俗話說,工欲善其事,必先利其器,Joda-Time就是操作日期和時間的一把利器。
Joda-Time的官網是http://www.joda.org/joda-time/。它的基本概念和工作原理與上節介紹的是類似的,比如說,都有時刻和年歷的概念,都有時區和Locale的概念,主要工作,都是在毫秒和年月日等年歷信息之間進行相互轉換。
Joda-Time的主要類和Java API的類也有一個粗略的對應關系:
Joda-Time需要說明的是,這只是一個非常粗略的對應,並不嚴謹,Joda-Time也還有非常多的其他類。
雖然基本概念是類似的,但API的設計卻有很大不同,Joda-Time的API更容易理解和使用,代碼也更為簡潔,下面我們會通過例子來說明。
另外,與Date/Calendar的設計有一個很大的不同,Joda-Time中的主要類都被設計為了不可變類,我們之前介紹過不可變類,包裝類/String都是不可變類,不可變類有一個很大的優點,那就是簡單、線程安全,所有看似的修改操作都是通過創建新對象來實現的。
本文並不打算全面介紹Joda-Time的每個類,相反,我們主要通過一些例子來說明其基本用法,體會其方便和強大,同時,學習其API的設計理念。
創建對象
新建一個DateTime對象,表示當前日期和時間:
DateTime dt = new DateTime();
新建一個DateTime對象,給定年月日時分秒等信息:
//2016-08-18 15:20 DateTime dt = new DateTime(2016,8,18,15,20); //2016-08-18 15:20:47 DateTime dt2 = new DateTime(2016,8,18,15,20,47); //2016-08-18 15:20:47.345 DateTime dt3 = new DateTime(2016,8,18,15,20,47,345);
獲取日歷信息
與Calendar不同,DateTime為每個日歷字段都提供了單獨的方法,取值的范圍也都是符合常識的,易於理解和使用,來看代碼:
//2016-08-18 15:20:47.345 DateTime dt = new DateTime(2016,8,18,15,20,47,345); System.out.println("year: "+dt.getYear()); System.out.println("month: "+dt.getMonthOfYear()); System.out.println("day: "+dt.getDayOfMonth()); System.out.println("hour: "+dt.getHourOfDay()); System.out.println("minute: "+dt.getMinuteOfHour()); System.out.println("second: "+dt.getSecondOfMinute()); System.out.println("millisecond: " +dt.getMillisOfSecond()); System.out.println("day_of_week: " +dt.getDayOfWeek());
輸出為:
year: 2016 month: 8 day: 18 hour: 15 minute: 20 second: 47 millisecond: 345 day_of_week: 4
每個字段的輸出都符合常識,且保持一致,都是從1開始,比如dayOfWeek,周四就是4, 易於理解。
格式化
Java API中,格式化必須使用一個DateFormat對象,而Joda-Time中,DateTime自己就有一個toString方法,可以接受一個pattern參數,看例子:
//2016-08-18 14:20:45.345 DateTime dt = new DateTime(2016,8,18,14,20,45,345); System.out.println(dt.toString("yyyy-MM-dd HH:mm:ss"));
輸出為:
2016-08-18 14:20:45
Joda-Time也有與DateFormat類似的類,看代碼:
DateTime dt = new DateTime(2016,8,18,14,20); DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm"); System.out.println(formatter.print(dt));
輸出為:
2016-08-18 14:20
這裡有兩個類,一個是DateTimeFormat,另一個是DateTimeFormatter。DateTimeFormatter是具體的格式化類,提供了print方法將DateTime轉換為字符串。DateTimeFormat是一個工廠類,專門生成具體的格式化類,除了forPattern方法,它還有一些別的工廠方法,本文就不介紹了。
程序設計的一個基本思維是關注點分離,程序一般總是比較復雜的,涉及方方面面,解決的思路就是分解,將復雜的事情盡量分解為不同的方面,或者說關注點,各個關注點之間耦合度要盡量低。
具體來說,對應到Java,每個類應該只關注一點。上面的例子中,因為生成DateTimeFormatter的方式比較多,就將生成DateTimeFormatter這個事單獨拿了出來,就有了工廠類DateTimeFormat,只關注生產DateTimeFormatter,Joda-Time中還有別的工廠類,比如ISODateTimeFormat,工廠類是一種常見的設計模式。
除了將DateTime轉換為字符串,DateTimeFormatter還可以將字符串轉化為DateTime,代碼如下:
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm"); DateTime dt = formatter.parseDateTime("2016-08-18 14:20");
與上節介紹的格式化類不同,Joda-Time的DateTimeFormatter是線程安全的,可以安全的被多個線程共享。
設置和修改時間
上節介紹Calendar時提到,修改時期和時間有兩種方式,一種是直接設置絕對值,另一種是在現有值的基礎上進行相對增減操作,DateTime也支持這兩種方式。
不過,需要注意的是,DateTime是不可變類,修改操作是通過創建並返回新對象來實現的,原對象本身不會變。
我們來看一些例子。
調整時間為下午3點20
DateTime dt = new DateTime(); dt = dt.withHourOfDay(15).withMinuteOfHour(20);
DateTime有很多withXXX方法來設置絕對時間。DateTime中非常方便的一點是,方法的返回值是修改後的DateTime對象,可以接著進行下一個方法調用,這樣,代碼就非常簡潔,也非常容易閱讀,這種一種流行的設計風格,稱為流暢接口 (Fluent Interface),相比之下,使用Calendar,就必須要寫多行代碼,比較臃腫,下面我們會看到更多例子。
另外,注意需要將最後的返回值賦值給dt,否則dt的值不會變。
三小時五分鐘後
DateTime dt = new DateTime().plusHours(3).plusMinutes(5);
DateTime有很多plusXXX和minusXXX方法,用於相對增加和減少時間。
今天0點
DateTime dt = new DateTime().withMillisOfDay(0); System.out.println(dt.toString("yyyy-MM-dd HH:mm:ss.SSS"));
當前時間為2016-08-18,所以輸出為
2016-08-18 00:00:00.000
withMillisOfDay直接設置當天毫秒信息,會同時將時分秒等信息進行修改。
下周二上午10點整
DateTime dt = new DateTime().plusWeeks(1).withDayOfWeek(2) .withMillisOfDay(0).withHourOfDay(10);
明天最後一刻
DateTime dt = new DateTime().plusDays(1).millisOfDay().withMaximumValue(); System.out.println(dt.toString("yyyy-MM-dd HH:mm:ss.SSS"));
當前時間為2016-08-18,所以輸出為
2016-08-19 23:59:59.999
這裡說明一下,plusDays(1)容易理解,設為第二天。millisOfDay()的返回值比較特別,它是一個屬性,具體類為DateTime的一個內部類Property,這個屬性代表當天毫秒信息,這個屬性有一些方法,可以接著對日期進行修改,withMaximumValue就是將該屬性的值設為最大值。
這樣,代碼是不是非常簡潔?除了millisOfDay,DateTime還有很多類似屬性。我們來看更多的例子。
本月最後一天最後一刻
DateTime dt = new DateTime().dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue();
下個月第一個周一的下午5點整
DateTime dt = new DateTime().plusMonths(1).dayOfMonth().withMinimumValue() .plusDays(6).withDayOfWeek(1).withMillisOfDay(0).withHourOfDay(17);
我們稍微解釋下:
new DateTime().plusMonths(1).dayOfMonth().withMinimumValue()
將時間設為了下個月的第一天。.plusDays(6).withDayOfWeek(1)將時間設為第一個周一。
時間段的計算
JDK API中沒有關於時間段計算的類,而Joda-Time包含豐富的表示時間段和用於時間段計算的方法,我們來看一些例子。
計算兩個時間之間的差
Joda-Time有一個類,Period,表示按日歷信息的時間段,看代碼:
DateTime start = new DateTime(2016,8,18,10,58); DateTime end = new DateTime(2016,9,19,12,3); Period period = new Period(start,end); System.out.println(period.getMonths()+"月"+period.getDays()+"天" +period.getHours()+"小時"+period.getMinutes()+"分");
輸出為:
1月1天1小時5分
只要給定起止時間,Period就可以自動計算出來,兩個時間之間有多少月、多少天、多少小時等。
如果只關心一共有多少天,或者一共有多少周呢?Joda-Time有專門的類,比如Years用於年,Days用於日,Minutes用於分鐘,來看一些例子。
根據生日計算年齡
年齡只關心年,可以使用Years,看代碼:
DateTime born = new DateTime(1990,11,20,12,30); int age = Years.yearsBetween(born, DateTime.now()).getYears();
計算遲到分鐘數
假定早上9點是上班時間,過了9點算遲到,遲到要統計遲到的分鐘數,怎麼計算呢?看代碼:
int lateMinutes = Minutes.minutesBetween( DateTime.now().withMillisOfDay(0).withHourOfDay(9), DateTime.now()).getMinutes();
單獨的日期和時間類
我們一直在用DateTime表示完整的日期和時間,但在年齡的例子中,只需要關心日期,在遲到的例子中,只需要關心時間,Joda-Time分別有單獨的日期類LocalDate和時間類LocalTime。
使用LocalDate計算年齡
LocalDate born = new LocalDate(1990,11,20); int age = Years.yearsBetween(born, LocalDate.now()).getYears();
使用LocalTime計算遲到時間
int lateMinutes = Minutes.minutesBetween( new LocalTime(9,0), LocalTime.now()).getMinutes();
LocalDate和LocalTime可以與DateTime進行相互轉換,比如:
DateTime dt = new DateTime(1990,11,20,12,30); LocalDate date = dt.toLocalDate(); LocalTime time = dt.toLocalTime(); DateTime newDt = DateTime.now().withDate(date).withTime(time);
與JDK API的互操作
Joda-Time中的類可以方便的與JDK中的類進行相互轉換。
JDK -> Joda
Date、Calendar可以方便的轉換為DateTime對象:
DateTime dt = new DateTime(new Date()); DateTime dt2 = new DateTime(Calendar.getInstance());
也可以方便的轉換為LocalDate和LocalTime對象:
LocalDate.fromDateFields(new Date()); LocalDate.fromCalendarFields(Calendar.getInstance()); LocalTime.fromDateFields(new Date()); LocalTime.fromCalendarFields(Calendar.getInstance());
Joda -> JDK
DateTime對象也可以方便的轉換為JDK對象:
DateTime dt = new DateTime(); Date date = dt.toDate(); Calendar calendar = dt.toCalendar(Locale.CHINA);
LocalDate也可以轉換為Date對象:
LocalDate localDate = new LocalDate(2016,8,18); Date date = localDate.toDate();
小結
本節介紹了Joda-Time,一個方便和強大的日期和時間類庫,本文並未全面介紹,主要是通過一些例子展示了其基本用法。
我們也介紹了Joda-Time之所以易用的一些設計思維,比如,關注點分離,為方便操作,提供單獨的功能明確的類和方法,設計API為流暢接口,設計為不可變類,使用工廠類等。
下一節,我們來討論一個有趣的話題,那就是隨機。
----------------
未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。