Java 8新特性之新的日期和時間API
在Java 1.0中,對日期和時間的支援只能依賴java.util.Date類。這個類只能以毫秒的精度表示時間。這個類還有很多糟糕的問題,比如年份的起始選擇是1900年,月份的起始從0開始。這意味著你要想表示2018年8月22日,就必須建立下面這樣的Date例項:
Date date = new Date (118,7,22);
Wed Aug 22 00:00:00 CST 2018
甚至Date類的toString方法返回的字串也容易誤人。現在這個返回值甚至還包含了JVM的預設時區CST,但這不表示Date類在任何方面支援時區。
Java 1.1中,Date類中的很多方法被廢棄了,取而代之的是java.util.Calendar類。但是Calendar也有類似的問題和設計缺陷,導致使用這些方法寫出的程式碼非常容易出錯。比如月份依舊是從0開始計算,不過去掉了1900年開始計算年份這一設計。更糟的是同時存在Date和Calendar這兩個類,也增加了程式設計師的困惑。到底該使用哪一個類呢?此外,有的特性只有在某一個類有提供,比如用於以語言無關方式格式化和解析日期或時間的DateFormat方法就只有在Date類裡有。
DateFormat也有它自己的問題,首先他不是縣城安全的。這意味著兩個縣城如果嘗試使用同一個formatter解析日期,你可能無法得到預期的結果。
最後,Date和Calendar類都是可變的。
Java 8 在java.time包中整合了很多Joda-Time的特性,java.time包中提供了很多新的類可以幫助你解決問題:LocalDate、LocalTime、Instant、Duration和Period。
LocalDate
首先該類的例項是一個不可變物件,它只提供了簡單的日期,並不含當天的時間。另外,它也不附帶任何時區相關的資訊。 你可以通過靜態工廠of建立一個LocalDate例項,LocalDate提供了很多方法來讀取常用的值,比如年月日星期幾等:
LocalDate date = LocalDate.of(2018,8,22);
int year = date.getYear(); //2018
Month month = date.getMonth(); //AUGUST(7)
int day = date.getDayOfMonth(); //22
DayOfWeek dow = date.getDayOfWeek(); //WEDNESDAY
int len = date.lengthOfMonth(); //31
boolean leap = date.isLeapYear(); //false
LocalDate today = LocalDate.now(); //2018-08-22
傳遞TemporalField引數給 date的get方法也可以拿到同樣的資訊,TemporalField是一個藉口,它定義瞭如何訪問temporal物件某個欄位的值。ChronoField列舉實現了這一藉口,所以你可以很方便的使用get方法得到列舉元素的值:
int yearofGet = date.get(ChronoField.YEAR); //2018
int monthofGet = date.get(ChronoField.MONTH_OF_YEAR); //8
int dayofGet = date.get(ChronoField.DAY_OF_MONTH); //22
LocalTime
與LocalDate類似,LocalTime表示時間,你可以使用of過載的兩個工廠方法建立LocalTime例項,第一個過載是小時和分鐘,第二個過載還接受秒。也提供了getter方法訪問這些變數的值:
LocalTime time = LocalTime.of(17,55,55);
int hour = time.getHour();//17
int minute = time.getMinute();//55
int second = time.getSecond();//55
LocalDate和LocalTime都支援從字串建立,使用靜態方法parse:
LocalDate dateofString = LocalDate.parse("2018-08-22");
LocalTime timeofString = LocalTime.parse("15:53:52");
如果格式不正確,無法被解析成合法的LocalDate或LocalTime物件。parse方法會丟擲一個繼承自RuntimeException的DateTimeParseException異常。
LocalDateTime
這個複合類是LocalDate何LocalTime的合體,同時表示了日期和時間。但不帶有時區資訊,你可以直接建立,也可以通過合併日期和時間物件構造。
LocalDateTime dt1 = dateofString.atTime(timeofString);
LocalDateTime dt2 = time.atDate(date);
LocalDateTime dt3 = LocalDateTime.of(date,time);
LocalDateTime dt4 = LocalDateTime.of(2018,8,22,17,55,55);
//2018-08-22T17:55:55
也可以將LocalDateTime拆分為獨立的LocalDate 和LocalTime:
LocalDate localDate = dt1.toLocalDate();
LocalTime localTime = dt1.toLocalTime();
Instant
作為人,我們習慣於星期幾、幾號、幾點、幾分這樣的方式理解日期和時間,但是對於計算機而言並不容易理解。java.time.Instant類對時間的建模方式是 以Unix元年時間(UTC時區1970年1月1日午夜時分)開始所經歷的描述進行計算。
可以使用靜態工廠方法ofEpochSecond傳遞一個代筆哦啊秒數的值建立一個該類的例項。這個方法還有一個過載版本,它接受第二個以納秒為單位的引數值,對傳入作為秒數的引數進行調整。過載的版本會調整納秒引數,確保儲存的納秒分片在0到999999999之間,這意味著下面這些對ofEpochSecond工廠方法的呼叫返回幾乎同樣的Instant物件:
Instant instant = Instant.ofEpochSecond(3);
Instant instant2 = Instant.ofEpochSecond(3, 0);
Instant instant3 = Instant.ofEpochSecond(2, 1_000_000_000);
Instant instant4 = Instant.ofEpochSecond(4, -1_000_000_000);
1970-01-01T00:00:03Z
Instant類也支援靜態工廠方法now,它能夠幫你獲取當前時刻的時間戳。Instant的設計初衷是為了便於機器使用,它所包含的是由秒及納秒所構成的數字。所以,它無法處理那些我們非常容易理解的時間單位。
定義Duration或Period
計算日期時間差使用這兩個類
Duration d1 = Duration.between(time1, time2);
Duration d2 = Duration.between(datetime1, datetime2);
Duration d3 = Duration.between(instant,instant2);
由於LocalDateTime和Instant是為不同的目的而設計的,一個是為了人閱讀的,一個是為了機器處理的,所以不能將二者混用。此外,由於Duration類主要用於以秒和納秒衡量時間的長短,所以不能向between方法傳遞LocalDate。
Duration和Period類都提供了很多非常方便的工廠類,直接建立對應的例項。
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);
Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMothsOneDay = Period.of(2,6,1);
Period計算時間差
LocalDate today = LocalDate.now();
System.out.println("Today : " + today);
LocalDate birthDate = LocalDate.of(1991, 1, 11);
System.out.println("BirthDate : " + birthDate);
Period p = Period.between(birthDate, today);
System.out.printf("年齡 : %d 年 %d 月 %d 日", p.getYears(), p.getMonths(), p.getDays());
Today : 2018-08-22
BirthDate : 1991-01-11
年齡 : 27 年 7 月 11 日
Duration計算相差秒數
Instant inst1 = Instant.now();
System.out.println("Inst1 : " + inst1);
Instant inst2 = inst1.plus(Duration.ofSeconds(10));
System.out.println("Inst2 : " + inst2);
System.out.println("Difference in milliseconds : " + Duration.between(inst1, inst2).toMillis());
System.out.println("Difference in seconds : " + Duration.between(inst1, inst2).getSeconds());
Difference in milliseconds : 10000
Difference in seconds : 10
到目前為止,這些日期和時間都是不可修改的,這是為了更好的支援函數語言程式設計,確保執行緒安全。如果你想在LocalDate例項上增加3天,或者將日期解析和輸入 dd/MM/yyyy這種格式。 將在下一節講解。
操作、解析和格式化日期
對已存在的LocalDate物件,建立它的修改版,最簡單的方式是使用withAttribute方法。withAttribute方法會建立物件的一個副本,並按照需要修改它的屬性。以下所有的方法都返回了一個修改屬性的物件,他們不會影響原來的物件。
LocalDate dd = LocalDate.of(2018,8,23); //2018-08-23
LocalDate dd1 = dd.withYear(2017); //2017-08-23
LocalDate dd2 = dd.withDayOfMonth(22); //2018-08-22
LocalDate dd4 = dd.withMonth(10); //2018-10-23
LocalDate dd3 = dd.with(ChronoField.MONTH_OF_YEAR,9); //2018-09-23
除了withAttribute詳細的年月日,也可以採用通用的with方法,第一個引數是TemporalField物件,第二個引數是修改的值。它也可以操縱LocalDate物件:
LocalDate dd5 = dd.plusWeeks(1); //加一週
LocalDate dd6 = dd.minusYears(3); //減去三年
LocalDate dd7 = dd.plus(6,ChronoUnit.MONTHS); //加6月
plus和minus方法和上面的with類似,他們都聲明於Temporal介面中。像LocalDate、LocalTime、LocalDateTime以及Instant這些表示日期和時間的類提供了大量的通用方法:
1、from 靜態方法 依據傳入的Temporal物件建立是例項
2、now 靜態方法 依據系統時鐘建立Temporal物件
3、of 靜態方法 由Temporal物件的某個部分建立物件的例項
4、 parse 靜態方法 由字串建立Temporal物件的例項
5、atOffset 將Temporal物件和某個時區偏移相結合
6、atZone 將Temporal 物件和某個時區相結合
7、format 使用某個指定的格式將Temporal物件轉換為字串(Instant類不提供此方法)
8、get 讀取Temporal物件的某一部分的值
9、minus 建立物件的一個副本,然後將當前的Temporal物件的值減去一定的時長建立該副本
10、plus 建立物件的一個副本,然後將當前Temporal物件的值加上一定的時長建立該副本
11、with 以該物件為模板,對某些狀態進行修改建立該物件的副本。
TemporalAdjuster
操縱更復雜的日期,比如將日期調整到下個週日、下個工作日,或者是本月的最後一天。這時可以使用with的過載版本,向其傳遞一個提供了更多定製化選擇的TemporalAdjuster物件,更加靈活的處理日期。
import static java.time.temporal.TemporalAdjusters.*;
LocalDate dd = LocalDate.of(2018,8,23);
LocalDate dd1 = dd.with(dayOfWeekInMonth(2,DayOfWeek.FRIDAY)); //同一個月中,第二個星期五 2018-08-10
LocalDate dd2 = dd.with(firstDayOfMonth()); //當月的第一天 2018-08-01
LocalDate dd3 = dd.with(firstDayOfNextMonth()); //下月的第一天 2018-09-01
LocalDate dd4 = dd.with(firstDayOfNextYear()); //明年的第一天 2019-01-01
LocalDate dd5 = dd.with(firstDayOfYear()); //當年的第一天 2018-01-01
LocalDate dd6 = dd.with(firstInMonth(DayOfWeek.MONDAY)); //當月第一個星期一 2018-08-06
LocalDate dd7 = dd.with(lastDayOfMonth()); //當月的最後一天 2018-08-31
LocalDate dd8 = dd.with(lastDayOfYear()); //當年的最後一天 2018-12-31
LocalDate dd9 = dd.with(lastInMonth(DayOfWeek.SUNDAY)); //當月最後一個星期日 2018-08-26
LocalDate dd10 = dd.with(previous(DayOfWeek.MONDAY)); //將日期向前調整到第一個符合星期一 2018-08-20
LocalDate dd11 = dd.with(next(DayOfWeek.MONDAY)); //將日期向後調整到第一個符合星期一 2018-08-27
LocalDate dd12 = dd.with(previousOrSame(DayOfWeek.FRIDAY)); //將日期向前調整第一個符合星期五,如果該日期已經符合,直接返回該物件 2018-08-17
LocalDate dd13 = dd.with(nextOrSame(DayOfWeek.FRIDAY)); //將日期向後調整第一個符合星期五,如果該日期已經符合,直接返回該物件 2018-08-24
TemporalAdjuster可以進行復雜的日期操作,如果沒有找到符合的預定義方法,可以自己建立一個,TemporalAdjuster介面只聲明瞭一個方法所以他說一個函式式介面:
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
這意味著TemporalAdjuster介面的實現需要定義如何將一個Temporal物件轉換為另一個Temporal物件。比如設計一個NextWorkingDay類,實現計算下一個工作日,過濾掉週六和週日節假日。
public class NextWorkingDay implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
//如果當前日期 星期一到星期五之間返回下一天
if(dow == DayOfWeek.FRIDAY) dayToAdd = 3;
//如果當前日期 週六或週日 返回下週一
else if(dow == DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
}
}
由於TemporalAdjuster是函式式介面,所以你只能以Lambda表示式的方式向Adjuster介面傳遞行為:
date.with(t->{
//上面一坨
})
為了可以重用,TemporalAdjuster物件推薦使用TemporalAdjusters類的靜態工廠方法ofDateAdjuster:
LocalDate dd = LocalDate.of(2018, 8, 23);
TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(
temporal -> {
DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
//如果當前日期 星期一到星期五之間返回下一天
if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
//如果當前日期 週六或週日 返回下週一
else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
}
);
LocalDate next = dd.with(nextWorkingDay);
列印輸出及解析日期
處理日期和時間物件時,格式化以及解析日期-時間物件是另一個非常重要的功能。新的java.time.format包就是為了這個目的而設計的。這個包中最重要的類是DateTimeFormatter,建立格式器最簡單的方式是通過他的靜態工廠方法及常量。
String s1 = next.format(DateTimeFormatter.BASIC_ISO_DATE); //20180824
String s2 = next.format(DateTimeFormatter.ISO_LOCAL_DATE); //2018-08-24
除了解析為字串外,還可以通過解析代表日期或時間的字串重新建立該日期物件。
LocalDate date1 = LocalDate.parse("20180901",DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2018-09-02",DateTimeFormatter.ISO_LOCAL_DATE);
與老的java.util.DateFormat想比較,所有的DateTimeFormatter例項都是執行緒安全的。DateTimeFormatter類還支援一個靜態工廠方法,它按照某個特定的模式建立格式器。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate now = LocalDate.now();
String formatterDate = now.format(formatter);
LocalDate nowparse = LocalDate.parse(formatterDate,formatter);
ofPattern可以按照指定的格式進行解析成字串,然後又呼叫了parse方法的過載 將該格式的字串轉換成了 LocalDate物件。
ofPattern也提供了過載版本,使用它可以建立某個Locale的格式器:
DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MMMMd號", Locale.CHINA);
LocalDate chinaDate = LocalDate.parse("2018-08-21");
String formatterDate2 = chinaDate.format(formatter2); //2018年八月21號
LocalDate chinaDate2 = LocalDate.parse(formatterDate2,formatter2);
DateFormatterBuilder類還提供了更復雜的格式器和更強大的解析功能:
DateTimeFormatter chinaFormatter = new DateTimeFormatterBuilder().appendText(ChronoField.YEAR)
.appendLiteral("年")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral("號")
.parseCaseInsensitive().toFormatter(Locale.CHINA);
處理不同的時區和曆法
之前所看到的日期和時間種類都不包含時區資訊。時區的處理是新版日期和時間API新增加的重要功能,新的 java.time.ZoneId 類是老版 java.util.TimeZone 的替代品。
時區是按照一定的規則將區域劃分成的標準時間相同的區間。在ZoneRules這個類中包含了40個這樣的例項。你可以使用ZoneId的getRules()得到指定時區的規則。每個特定的ZoneId物件都由一個地區標識:
ZoneId romeZone = ZoneId.of("Europe/Rome"); //格式 歐洲/羅馬
地區ID都為 “{區域}/{城市}”的格式,這些地區集合的設定都由英特網編號分配機構(IANA)的時區資料庫提供。你可以通過java 8的新方法toZoneId將一個老的時區物件轉換為ZoneId
ZoneId zoneId = TimeZone.getDefault().toZoneId();
一旦得到一個ZoneId物件,就可以將它與LocalDate、LocalDateTIme或者是Instant物件整合起來,構造為一個ZonedDateTime例項,它代表了相對於指定時區的時間點,
LocalDate date = LocalDate.of(2018,8,22);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2018,8,23,13,48,00);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
ZonedDateTime = LocalDateTime(LocalDate + LocalTime) + ZoneId
通過ZoneId,你還可以將LocalDateTime轉換為Instant
LocalDateTime dateTime = LocalDateTime.of(2018,8,23,13,48,00);
Instant instantFromDateTime = dateTime.toInstant(romeZone);
Instant instant1 = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(romeZone);
利用和 UTC/格林尼治時間的固定偏差計算時區
另一種比較常用的表達時區的方式就是利用當前時區和 UTC/格林尼治 的固定偏差,比如,紐約落後倫敦5小時。這種情況下,你可以使用ZoneOffset類,它是ZoneId的一個子類,表示的是當前時間和倫敦格林尼治子午時間的差異:
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
這種方式不推薦使用,因為 -05:00 的偏差實際上是對應的美國東部標準時間,並未考慮任何日光時的影響。
LocalDateTime dateTime1 = LocalDateTime.now();
OffsetDateTime dateTimeInNewYork1 = OffsetDateTime.of(dateTime1,newYorkOffset);
它使用ISO-8601的歷法系統,以相對於UTC時間的偏差方式表示日期時間。
使用別的日曆系統
ISO-8601日曆系統是世界文明日曆系統的事實標準。但是,java 8 中另外提供了4種其他的日曆系統。這些日曆系統中的每一個都有一個對應的日誌類,分別是ThaiBuddhistDate、MinguoDate、JapaneseDate以及HijrahDate。所有這些類以及LocalDate都實現了ChronoLocalDate介面,能夠對公曆的日期進行建模。利用LocalDate物件,可以建立這些類的例項。
小結:
1、Java 8之前的java.util.Date類以及其他用於建模日期時間的雷有很多不一致及設計上的缺陷,包括易變性以及糟糕的偏移值、預設值和命名。
2、新版的日期和時間API中,日期-時間物件是不可變的。
3、新的API提供了兩種不同的時間表示方式,有效地區分了執行時人喝機器的不同需求。
4、操縱的日期不會影響老值,而是新生成一個例項。
5、TemporalAdjuster可以更精確的操縱日期,還可以自定義日期轉換器。
6、他們都是執行緒安全的