1. 程式人生 > >Java8日期和時間API

Java8日期和時間API

如何正確處理時間

現實生活的世界裡,時間是不斷向前的,如果向前追溯時間的起點,可能是宇宙出生時,又或是是宇宙出現之前,
但肯定是我們目前無法找到的,我們不知道現在距離時間原點的精確距離。所以我們要表示時間,
就需要人為定義一個原點。

原點被規定為,格林威治時間(GMT)1970年1月1日的午夜 為起點,之於為啥是GMT時間,大概是因為本初子午線在那的原因吧。

Java中的時間

如果你跟你朋友說:“我們 1484301456 一起去吃飯,別遲到!”,而你朋友能馬上理解你說的時間,表示時間就會很簡單,
只需要一個long值來表示原點的偏移量,這是個絕對時間,在世界範圍內都適用。但實際上我們不能馬上理解這串數字,
而且我們需要不同的時間單位來表示時間的跨度,比如一個季度是3個月,一個月有30天等。
你可以跟朋友約好“明天這個時候再見面”,你朋友很容易理解明天的意思,但要是沒有’天’這個單位,
他就需要在那串數字上加上86400(一天是86400秒)。

Java三次引入處理時間的API,JDK1.0中包含了一個Date類,但大多數方法在java1.1引入Calendear類之後被棄用了。
它的例項都是可變的,而且它的API很難使用,比如月份是從0開始這種反人類的設定。

java8引入的java.time API 已經糾正了之前的問題。它已經完全實現了JSR310規範。

java8時間API介紹及使用

在新的時間API中,Instant表示一個精確的時間點,DurationPeriod表示兩個時間點之間的時間量。
LocalDate表示日期,即xx年xx月xx日,即不包括時間也不帶時區。LocalTimeLocalDate類似,
但只包含時間。LocalDateTime

則包含日期和時間。ZoneDateTime表示一個帶時區的時間。
DateTimeFormatter提供格式化和解析功能。下面詳細的介紹使用方法。

Instant

Instant表示一個精確的時間,時間數軸就是由無數個時間點組成,數軸的原點就是上面提
到的1970-1-1 00:00:00Instant由兩部分組成,一是從原點開始到指定時間點的秒數s,
二是距離該秒數s的納秒數。

使用靜態方法Instant.now()可以獲取當前的時間點,該方法預設使用的是UTC(協調世界時——由原子鐘提供)時間,可以使用equealcompareTo來比較兩個時間點的值。

計算某段程式碼執行時間可以使用下面的方式:


Instant start = Instant.now();

doSomething();

Instant end = Instant.now();

Duration timeElapsed = Duration.between(start, end);
long millis = timeElapsed.toMillis();
System.out.println("millis = " + millis);

Duration物件表示兩個時間點之間的距離,通過類似toMillis() toDays() getSeconds()等方法,
得到各種時間單位表示的Duration物件。如果確實需要使用納秒來做一些計算,可以呼叫toNanos()
獲得一個long型別的值,該值表示距離原點的納秒值。大概300年的納秒值會導致long值溢位

Duration內部使用一個long型別來儲存秒鐘的值,使用一個int來儲存納秒的值,與Instant類似,
這個納秒儲存的是距離該秒鐘的納秒值.

Instant與Duration都可以進行一些運算,來調整表示的時間,比如:plus() minus 方法,
表示增加或減少一段時間,plusSeconds() minusSeconds() plusXxx()等表示增加或減少相應時間單位的一段時間。

Duration可以進行multipliedBy()乘法和dividedBy()除法運算。negated()做取反運算,即1.2秒取反後為-1.2秒。

非常重要的是,Instant 和 Duration類都是不可變的,他們的所有方法都返回一個新的例項。不可變類有很多優點:
不可變類使用起來不容易出錯,其本質上是執行緒安全的,物件可以被自由的共享,而不用擔心被某個方法修改。

LocalDate(本地日期)

上面介紹的Instant是一個絕對的準確時間點,是人類不容易理解的時間,現在介紹人類使用的時間。

LocalDate 表示像 2017-01-01這樣的日期。它包含有年份、月份、當月天數,它不不包含一天中的時間,
以及時區資訊。由於上面的這些特點,所以LocalDate不能表示一個準確的時間點,即Instant。

有很多時間的計算是不需要時區的,而且有一些情況下使用時區會導致一些問題,例如你在中國設定了一個
2017-01-01 UT+8:00 的放假提醒,但之後你去了美國,到了2017-01-01 UT+8:00時間時你收到了提醒,
但是此時美國還沒到放假的時間。

API的設計者推薦使用不帶時區的時間,除非真的希望表示絕對的時間點。

可以使用靜態方法now()of()建立LocalDate。java.util.Date使用0作為月份的開始,年份從1990年開始算起,
而新的API中完全是用生活中一樣的方式來表示年和月份。

//獲取當前日期
LocalDate now = LocalDate.now();
//2017-01-01
LocalDate newYear = LocalDate.of(2017, 1, 1);

可以通過一些方法對日期做一些運算。

//三天後
now.plusDays(3);
//一週後
now.plusWeeks(1)
//兩天前 
now.minusDays(2)
//增加一個月不會出現2017-02-31 而是會返回該月的最後一個有效日期,即2017-02-28
LocalDate.of(2017, 1, 31).plusMonths(1)

LocalDate feb = LocalDate.of(2017, 2, 1);
//withXxx()表示以該日期為基礎,修改年、月、日欄位,並返回一個新的日期
//2019-2-1
feb.withYear(2019);
//2017-1-10
feb.withDayOfYear(10);
//2017-2-10
feb.withDayOfMonth(10);

上面講過Duration表示的是Instant對應的時間段,LocalDate對應的表示時間段的是Period,
Period內部使用三個int值分表表示年、月、日。
Duration和Period都是TemporalAmount介面的實現,該介面表示時間量。

LocalDate 也可以增加或減少一段時間:

//2019-02-01
feb.plus(Period.ofYears(2));
//2015-02-01
feb.minus(Period.ofYears(2);

使用until獲得兩個日期之間的Period物件

//輸出P9D,表示相差9天
feb.until(LocalDate.of(2017, 2, 10));//輸出---> P9D

LocalDate提供了一些測試方法:
isBefore isAfter比較兩個LocalDate,isLeapYear判斷是否是閏年。

LocalDate還提供了各種getXxx方法來返回所需要的資料,其中getDayOfWeek()返回DayOfWeek列舉。
DayOfWeek提供了plus minus來方便計算星期。

//SUNDAY
LocalDate.of(2017, 1, 1).getDayOfWeek();
//TUESDAY
DayOfWeek.SUNDAY.plus(2);

除了LocalDate,Java8還提供了Year MonthDay YearMonth來表示部分日期,例如MonthDay可以表示1月1日。

日期校正器TemporalAdjuster

如果想找到某個月的第一個週五,或是某個月的最後一天,像這樣的日期就可以使用TemporalAdjuster來進行日期調整。
TemporalAdjusters提供一些靜態方法,返回常用的TemporalAdjuster

//2017-02-03的下一個星期五(包含當天)  2017-03-03
LocalDate.of(2017, 2, 3).with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY));
//2017-02-03的下一個星期五(不包含當天)  2017-02-10
LocalDate.of(2017, 2, 3).with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
//2月中的第3個星期五  2017-02-17
LocalDate.of(2017, 2, 3).with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.FRIDAY));
//2月中的最後一個星期五  2017-02-24
LocalDate.of(2017, 2, 3).with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY));
//下個月的第一天
LocalDate.of(2017, 2, 3).with(TemporalAdjusters.firstDayOfNextMonth());

這是上面例子對應的當月日曆

LocalTime(本地時間)

LocalTime表示一天中的某個時間,例如18:00:00。LocaTime與LocalDate類似,他們也有相似的API。

需要注意的是:LocalTime本身不關心是AM還是PM,而是格式化程式來負責這個事情。

LocalDateTime(本地日期時間)

LocalDateTime表示一個日期和時間,它適合用來儲存確定時區的某個時間點。不適合跨時區的問題。

若需要處理跨時區的時間,需要使用ZonedDateTime.

ZonedDateTime(帶時區的時間)

時區(Time Zone)是地球上的區域使用同一個時間定義。1884年在華盛頓召開國際經度會議時,
為了克服時間上的混亂,規定將全球劃分為24個時區。
由於實用上常常1個國家,或1個省份同時跨著2個或更多時區,為了照顧到行政上的方便,
常將1個國家或1個省份劃在一起。所以時區並不嚴格按南北直線來劃分,而是按自然條件來劃分。

Java使用ZoneId來標識不同的時區.

//獲得所有可用的時區  size=590
ZoneId.getAvailableZoneIds();
//獲取預設ZoneId物件
ZoneId defZoneId = ZoneId.systemDefault();
//獲取指定時區的ZoneId物件
ZoneId shanghaiZoneId = ZoneId.of("Asia/Shanghai");
//ZoneId.SHORT_IDS返回一個Map<String, String> 是時區的簡稱與全稱的對映。下面可以得到字串 Asia/Shanghai
String shanghai = ZoneId.SHORT_IDS.get("CTT");

我在測試的時候一共有590個時區可用,但要知道,這個時區的個數不是固定的。

IANA(Internet Assigned Numbers Authority,因特網撥號管理局)維護著一份全球所有已知的時區資料庫,
每年會更新幾次,主要處理夏令時規則的改變。Java使用了IANA的資料庫。

建立ZonedDateTime

//2017-01-20T17:35:20.885+08:00[Asia/Shanghai]
ZonedDateTime.now();
//2017-01-01T12:00+08:00[Asia/Shanghai]
ZonedDateTime.of(2017, 1, 1, 12, 0, 0, 0, ZoneId.of("Asia/Shanghai"));
//使用一個準確的時間點來建立ZonedDateTime,下面這個程式碼會得到當前的UTC時間,會比北京時間早8個小時
ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC"));

LocalDateTime轉換為ZonedDateTime

//atZone方法可以將LocalDateTime轉換為ZonedDateTime,下面的方法將時區設定為UTC。
//假設現在的LocalDateTime是2017-01-20 17:55:00 轉換後的時間為2017-01-20 17:55:00[UTC]
LocalDateTime.now().atZone(ZoneId.of("UTC"));
//使用靜態of方法建立zonedDateTime
ZonedDateTime.of(LocalDateTime.now(), ZoneId.of("UTC"));

ZonedDateTime的一些方法

ZonedDateTime的許多方法與LocalDateTime、LocalDate、LocalTime類似,下面簡單介紹幾個方法的使用。


ZonedDateTime utcDateTime = ZonedDateTime.of(2017, 1, 1, 12, 0, 0, 0, ZoneId.of("UTC"));//2017-01-01T12:00Z[UTC]
//withZoneSameLocal返回指定時區中的一個新ZonedDateTime,替換時區為指定時區,表示相同的本地時間的該時區時間。
utcDateTime.withZoneSameLocal(ZoneId.of("Asia/Shanghai"));//2017-01-01T12:00+08:00[Asia/Shanghai]
//withZoneSameInstant返回指定時區中的一個新ZonedDateTime,替換為指定時區,表示相同時間點的該時區時間。
utcDateTime.withZoneSameInstant(ZoneId.of("Asia/Shanghai"));//2017-01-01T20:00+08:00[Asia/Shanghai]

有一些國家和地區使用夏令時,處理起來需要注意,但在中國沒有該問題,
需要注意的是使用plus()時要用Period物件表示的時間量,而不應該用Duration表示的時間量,
Duration不能處理夏令時。

utcDateTime.plus(Duration.ofDays(7));//不能處理夏令時
utcDateTime.plus(Period.ofDays(7));//正確方式

格式化和解析 DateTimeFormatter

DateTimeFormatter是不可變類,而SimpleDateFormat是非執行緒安全的,是一個常見的坑。

格式化

DateTimeFormatter使用了三種格式化方法來列印日期和時間

  • 預定義的標準格式

DateTimeFormatter預定義了一些格式,可以直接呼叫format方法

//2017-01-01
DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.of(2017, 1, 1))
//20170101
DateTimeFormatter.BASIC_ISO_DATE.format(LocalDate.of(2017, 1, 1));
//2017-01-01T09:10:00
DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.of(2017, 1, 1, 9, 10, 0));
  • 語言環境相關的格式化風格

根據當前作業系統語言環境,有SHORET MEDIUM LONG FULL 四種不同的風格來格式化。
可以通過DateTimeFormatter的靜態方法ofLocalizedDate ofLocalizedTime ofLocalizedDateTime

//201711日 星期日
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).format(LocalDate.of(2017, 1, 1));
//上午091000秒
DateTimeFormatter.ofLocalizedTime(FormatStyle.LONG).format(LocalTime.of(9, 10, 0));
//2017-2-27 22:32:03
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).format(LocalDateTime.now());

上面的方法都使用的是預設的語言環境,如果想改語言環境,需要使用withLocale方法來改變。

//Feb 27, 2017 10:34:36 PM
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.US).format(LocalDateTime.now());
  • 使用自定義模式格式化
//2017-02-27 22:48:52
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now())

解析

//使用的ISO_LOCAL_DATE格式解析  2017-01-01
LocalDate.parse("2017-01-01");
//使用自定義格式解析  2017-01-01T08:08:08
LocalDateTime.parse("2017-01-01 08:08:08", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

遺留程式碼相互操作

Instant 類似於java.util.Date

ZonedDateTime類似於java.util.GregorianCalendar

//Date --> Instant
Instant timestamp = new Date().toInstant();
//Instant --> Date
Date.from(Instant.now());

//GregorianCalendar --> ZonedDateTime
new GregorianCalendar().toZonedDateTime();
//ZonedDateTime --> GregorianCalendar
GregorianCalendar.from(zonedDateTime);

//2017-02-27T21:16:13.647
LocalDateTime.ofInstant(timestamp, ZoneId.of(ZoneId.SHORT_IDS.get("PST")));


//Calendar --> Instant
//2017-02-28T05:16:13.656Z
Calendar.getInstance().toInstant();