計算機程式的思維邏輯 (33) - Joda-Time

swiftma發表於2016-10-24

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (33) - Joda-Time

Joda-Time

上節介紹了JDK API中的日期和時間類,我們提到了JDK API的一些不足,並提到,實踐中有一個廣泛使用的日期和時間類庫,Joda-Time,本節我們就來介紹Joda-Time。俗話說,工欲善其事,必先利其器,Joda-Time就是操作日期和時間的一把利器。

Joda-Time的官網是www.joda.org/joda-time/。它的基本概念和工作原理與上節介紹的是類似的,比如說,都有時刻和年曆的概念,都有時區和Locale的概念,主要工作,都是在毫秒和年月日等年曆資訊之間進行相互轉換。

Joda-Time的主要類和Java API的類也有一個粗略的對應關係:

|Joda-Time |Java API |說明 | | ------------- |-------------| |Instant |Date| 時刻| |DateTime |Calendar| 年曆| |DateTimeZone |TimeZone| 時區| |DateTimeFormatter| DateFormat| 格式化|

需要說明的是,這只是一個非常粗略的對應,並不嚴謹,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程式設計及計算機技術的本質。用心原創,保留所有版權。

計算機程式的思維邏輯 (33) - Joda-Time

相關文章