java8 新的時間api
本篇文章分為三個部分:
- 基礎的日期時間對象的使用
- 操作和解析日期時間對象
- 基于時區的調整,使用不同的歷法
由于眾所周知的原因,java中的java.util.Date
和java.util.Calendar
無論從設計上還是使用上都存在問題,同時也不適應新的函數式編程的新浪潮。出于多方面原因的考慮,最后在java8中新增了java.time
,這個專門處理時間相關問題的包。
1. LocalDate, LocalTime, Instant, Duration, Period
想了解time包中的時間api, LocalDate, LocalTime, Instant, Duration, Period
這幾個類庫應該是最基礎的內容。
1.1 使用LocalDate, LocalTime
使用新的日期和時間api,LocalDate
和LocalTime
應該是基礎中的基礎,我們來一個一個了解。
LocalDate
LocalDate
第一次使用肯定會與Date
產生聯想,其實作為time
包中基礎的類,LocalDate
和原Date
對象有很大的不同。
首先,LocalDate
對象是不可變對象(類似于String
,對象的屬性和值不可改變);其次,只提供簡單的日期信息,并不包含日期當天的時分秒等時間信息;當然也不包含任何時區相關的信息。所以簡單來說,LocalDate
對象是只記錄了簡單日期信息的不可變對象。
可以通過靜態方法of()
獲取指定的日期或者使用工廠now()
方法獲取當前日期
/**
* 創建LocalDate對象的幾種方式
*
* 1. of 方式
* 2. now 方式
*
* 輸出:
* 2018-04-20
* 2018-05-09
*/
public static void createLocalDateDemo() {
// 通過of方法創建LocalDate對象
int year = 2018;
Month month = Month.of(4);
// month是內置的枚舉,直接通過具體的值指定月份也可以
Month month2 = Month.APRIL;
int day = 20;
LocalDate ofDate = LocalDate.of(year, month, day);
System.out.println(ofDate.toString());
// 通過靜態方法now 創建,當前日期
LocalDate nowDate = LocalDate.now();
System.out.println(nowDate);
}
讀取LocalDate
的屬性也很簡單:
/**
* 讀取localDate的屬性
*
* 輸出:
* year:2018 month:5 day-of-month:9
*/
public static void getLocalDateField() {
LocalDate now = LocalDate.now();
int year = now.getYear();
Month month = now.getMonth();
int day = now.getDayOfMonth();
System.out.print("year:" + year + " ");
System.out.print("month:" + month.getValue() + " ");
System.out.println("day-of-month:" + day);
}
除了簡單的年月日之外,LocalDate
還記錄了一些額外日期的信息
// 除了簡單的年月日之外,LocalDate還記錄了一些十分有用的和日期相關的信息
boolean isLeapYear = now.isLeapYear(); // 是否是閏年
int lengthOfMonth = now.lengthOfMonth(); // 當前月份有多少天
DayOfWeek dow = now.getDayOfWeek(); // 當前是周幾
如果你有看到LocalDate
的源碼,你會發現LocalDate
實現了Temporal
接口。所以,還可以通過傳遞TemporalField
參數給get
方法來獲取指定的信息。TemporalField
是一個接口,定義了如何訪問temporal
對象的某個字段。而ChronoField
枚舉則實現了這一接口,所以可以很方便的使用get
方法獲取到枚舉元素的值。
/**
* 通過Temporal接口獲取LocalDate的屬性
*
* 輸出:
* 通過Temporal訪問LocalDate的屬性:2018-5-9
*/
public static void getFieldByTemporal() {
LocalDate now = LocalDate.now();
int year = now.get(ChronoField.YEAR);
int month = now.get(ChronoField.MONTH_OF_YEAR);
// 如果想把int類型的month轉為枚舉,可以使用of 方法
Month month2 = Month.of(month);
int day = now.get(ChronoField.DAY_OF_MONTH);
System.out.println("通過Temporal訪問LocalDate的屬性:" + year + "-" + month + "-" + day);
}
LocalTime
LocalTime
存儲的是單純的時間信息,不包含日期。除此之外基本和LocalDate
的屬性相似,都是不可變對象,實現了Temporal
接口等等。
首先來看LocalTime
的創建
/**
* 創建LocalTime
*
* 輸出:
* 10:39:44.951
* 12:12:12.000100
*/
public static void createLocalTime() {
// 通過工廠方法now創建
LocalTime now = LocalTime.now();
System.out.println(now);
// 通過制定參數of 方法創建
LocalTime ofTime = LocalTime.of(12, 12, 12,100000);
System.out.println(ofTime);
}
然后是對應的屬性的讀取,同樣也是和LocalDate
類似
/**
* 讀取LocalTime的值
*
* 輸出:
* 10:50:54
* 10:50:54
*/
public static void getLocalTimeField() {
LocalTime nowTime = LocalTime.now();
int hour = nowTime.getHour();
int minute = nowTime.getMinute();
int second = nowTime.getSecond();
System.out.println(hour + ":" + minute + ":" + second);
// 同樣,LocalTime 也可以通過Temporal來獲取指定屬性的值
int tempOfHour = nowTime.get(ChronoField.HOUR_OF_DAY);
int tempOfMinute = nowTime.get(ChronoField.MINUTE_OF_HOUR);
int tempOfSecond = nowTime.get(ChronoField.SECOND_OF_MINUTE);
System.out.println(tempOfHour + ":" + tempOfMinute + ":" + tempOfSecond);
}
以上是LocalDate
和LocalTime
的基本使用,但是實際開發中其實我們用的最多的是格式化的String轉為日期和時間對象。當然,新的時間api在這方面的支持也是相當完善的,而且比以前的效果更好更簡潔:
LocalDate date = LocalDate.parse("2018-12-12");
LocalTime time = LocalTime.parse("12:12:12");
System.out.println(date + " " + time); //輸出:2018-12-12 12:12:12
查看源碼可以看出,這里其實是使用默認的標準的ISO formatter,DateTimeFormatter
是新版的時間格式化類,規定的如何將String
和Local
系列的日期時間對象對應起來,實際使用中可以使用該對象來完成字符串和日期時間對象之間的互轉。
1.2 使用LocalDateTime
實際開發中,我們很少會將日期和時間拆開使用,大多數情況下兩者都是存在的。新的time包中有LocalDateTime
這一組合對象,此時的LocalDateTime
有一點點類似于Date
了,同時存有日期和時間。不同之處在于LocalDateTime
仍然是不可變對象,且不包含任何時區信息。
創建LocalDateTime
的方式和LocalDate
與LocalTime
類似
/**
* 創建LocalDateTime 對象
*
* 指定時間為:2018-5-11T12:12:12
*/
public static void createLocalDateTime() {
// 通過of 方法直接創建
LocalDateTime dateTime = LocalDateTime.of(2018, 5, 11, 12, 12, 12);
// 通過LocalDate和Time 合并實現
LocalDate date = LocalDate.of(2018, 5, 11);
LocalTime time = LocalTime.of(12, 12, 12);
LocalDateTime dateTime2 = LocalDateTime.of(date, time);
// 通過LocalDate的 atTime 創建
LocalDateTime dateTime3 = date.atTime(time);
LocalDateTime dateTime4 = date.atTime(12, 12, 12);
// 通過LocalTime的 atDate 創建
LocalDateTime dateTime5 = time.atDate(date);
}
因為是組合對象,所以可讀取一部分來獲取LocalDate
或者LocalTime
LocalDateTime now = LocalDateTime.now();
LocalDate date = now.toLocalDate();
LocalTime time = now.toLocalTime();
到目前為止我們了解了LocalDate
,LocalTime
以及 LocalDateTime
,它們的關系如下:
1.3 Instant,關于機器的時間
作為人類,我們理解時間的概念都是幾年幾月幾天幾分幾秒等等,毫無疑問機器肯定已經不會以這種方式處理時間,這一點從老的Date
和Calendar
就可以看出來。所以在time
包中,類似于時間戳的這種底層的處理時間的類為Instant
。
當然,我們最好的理解應該是:Instant
是與機器交互的時間處理類。因此Instant
不需要記錄年,月,日等等,類似于時間戳,Instant
記錄的是從Unix元年(UTC時區1970年1月1日午夜零分)到現在的秒數,可以通過ofEpochSecond
工廠方法創建,當然還存在一個增強版本,可以額外的接口一個以納秒為單位的數值,來精確的記錄時間。
/**
* 創建Instant
*/
public static void createInstant() {
// 通過工廠方法now創建
Instant instant = Instant.now();
// 通過工廠方法ofEpochSecond創建
Long timestamp = instant.getEpochSecond();
Instant instant1 = Instant.ofEpochSecond(timestamp);
// 增強版本,可以傳遞一個納秒,
Instant instant2 = Instant.ofEpochSecond(1000);
Instant instant3 = Instant.ofEpochSecond(1000, 0L);
Instant instant4 = Instant.ofEpochSecond(999, 1_000_000_000L);
Instant instant5 = Instant.ofEpochSecond(998, 2_000_000_000L);
Instant instant6 = Instant.ofEpochSecond(1001, -1_000_000_000L);
}
關于
ofEpochSecond(int second, long nanoSecond)
的增強版本,會將納秒調整在0~999,999,999 之間。所以當納秒數超過這個范圍的時候,程序會根據具體的值進行調整。所以,demo代碼中的這幾種方式創建的Instant都是相等的。
增強重載版本的源碼
Instant
是設計用于和機器交互的時間類,雖然實現了Temporal
接口,但是內部是沒有年月日,時分秒等屬性的,因此一下代碼的調用會扔出Runtime異常
// 會扔出運行時異常
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);
1.4 Duration和Period
處理常規的表達時間點的概念之外,time新增了表示時間段的類Duration
和Period
。在java8之前,我們只能通過數字加人為規定的單位來表達時間段這一概念。但是在實際開發中,老的時間api計算時間段是真的不方便,而且效率低下。使用的新表達時間段的類就可以很方便的解決這個問題
創建Duration
的方式很簡單,使用between
方法即可,可以傳入兩個Instant
,兩個LocalDateTime
或者兩個Localtime
對象來進行創建
LocalDateTime from = LocalDateTime.of(2018, 4, 1, 0, 0, 0);
LocalDateTime to = LocalDateTime.now();
Duration duration = Duration.between(from, to);
System.out.println(duration); // PT953H28M16.279S 代表:953小時28分鐘16.279秒
為什么不能將
Instant
和LocalDateTime
混用呢,因為Instant
是給機器設計的,LocalDateTime
是給人設計的,兩個目的不一樣,因此不能混用。除此之外,Duration
類是主要以秒和納秒來表達時間段的,從單位上來說比較精確,因此也不能使用LocalDate
來計算兩個日期之間的時間段。
當然,如果要表達最小以天為單位的時間段,就可以使用Period
類
LocalDate from = LocalDate.of(2018, 4, 1);
LocalDate to = LocalDate.of(2018, 5, 2);
Period period = Period.between(from, to);
System.out.println(period); // P1M1D 表示:1個月零2天
到這里,我們就很明白了。Duration
和Period
都可以表示一段時間。兩者最主要的卻別在于度量的單位不同,Duration
主要是以時分秒甚至于毫秒來較為精確的度量一段時間,而Period
則是從年月日的角度來表示一段時間。實際開發中,可以視不同的業務需求來使用。
除了between
之外,Duration
和Period
還有很多工廠方法來獲取實例化的時間對象
Duration threeMinutes = Duration.ofMinutes(3); // 三分鐘
Duration fiveMinutes = Duration.of(5, ChronoUnit.MINUTES); //五分鐘
Period threeDays = Period.ofDays(3); //三天
Period twoWeeks = Period.ofWeeks(2); // 兩周
Period oneYear = Period.ofYears(1); // 一年
Period fiveMonth = Period.ofMonths(5); //五個月
Period towYearsOneMonthTenDays = Period.of(2, 1, 10); // 兩年一個月零十天
上述代碼中只是簡單地舉了一個例子,其實Duration
和Period
中有很多相似的工廠方法來創建實例化的時間段。
方法名 | 是否是靜態方法 | 方法描述 |
---|---|---|
between | 是 | 創建兩個時間點之間的interval |
from | 是 | 由一個臨時節點創建interval |
of | 是 | 由它的組成部分創建interval的實例 |
parse | 是 | 由字符串創建nterval |
addTo | 否 | 創建該interval的副本,并將其疊加到某個指定的Temporal對象 |
get | 否 | 讀取該interval的狀態 |
isNegative | 否 | 檢查該interval是否為負值,不包含0 |
isZero | 否 | 檢查該interval是否為0 |
minus | 否 | 減去一定的時間創建interval的副本 |
multipliedBy | 否 | 將interval乘以某個標量來創建其副本 |
negated | 否 | 以忽略某個時長的方式創建interval的副本 |
plus | 否 | 以增加某個時長的方式創建interval的副本 |
subtractFrom | 否 | 從指定的temporal對象中減去該interval來創建其副本 |
2. 操作和解析日期與時間
除了創建和讀取日期時間對象,實際開發中不可避免的存在修改,解析日期時間對象的需求,下面對這方面的內容進行講解。
2.1 操作日期和時間對象
with操作
首先是修改日期時間對象。第一部分反復強調,以上我們提到的所有的日期時間對象都是固定的不可更改的對象。所以,下文除非特殊說明的情況下都是基于原對象修改后返回的新日期時間對象,而原對象的屬性值都不變。
最常用的基本的修改日期和時間對象屬性的方法是withAttribute
類型的方法。
// 2018-04-01
LocalDate date = LocalDate.of(2018, 4 , 1);
// 使用 withAttribute 類型的方法可基于已有對象的屬性修改創建得到新的日期對象,原對象不變
LocalDate date2 = date.withDayOfMonth(12); // 2018-04-12
System.out.println(date + " => " + date2);
LocalDate date3 = date2.withYear(2019); // 2019-04-12
System.out.println(date2 + " => " + date3);
當然,除了固定的修改某個字段的with
方法之外還有通用的with
方法,因為我們上面提到的所有的日期時間對象都實現了Temporal
接口,這個就不在贅述,舉例如下:
// 也可以使用通用的with方法來對指定的屬性進行修改, 比如之類指定修改月份這一屬性
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 10); // 2019-10-12
System.out.println(date3 + " => " + date4);
加減操作
with
類型的方法是直接基于原有屬性修改為指定的屬性,除此之外開發中也會存在基于已有時間的加減操作。比如兩周之后,五個月之前等等。
// 2018-04-01
LocalDate date = LocalDate.of(2018, 4, 1);
LocalDate date2 = date.plusDays(10); // 2018-04-11
System.out.println(date + " => " + date2);
LocalDate date3 = date2.minusMonths(2).plusYears(1); // 2018-02-11 => 2019-02-11
System.out.println(date2 + " => " + date3);
// 19年2月為28天,所以四周后為 2019-03-11
LocalDate date4 = date3.plus(4, ChronoUnit.WEEKS);
System.out.println(date3 + " => " + date4);
總結一下:到這里我們講了兩種操作方法with類型的方法和加減類型的方法。需要說明的是LocalDate
, LocalTime
, LocalDateTime
都是支持上述方法的。且with和加減方法都支持指定單位修改和傳入指定單位兩種修改模式。前者簡單直接調用,后者則更為通用,實際開發中可視具體情況調用。
此外,提到了兩個Chrono開頭的枚舉,一個是
ChronoField
,這個指定的日期時間對象的具體屬性(比如:時間對象中的一小時的秒數,一秒鐘的納秒數等等,with方法修改的就是直接日期和時間對象的屬性)。另一個ChronoUnit
,這個指的是日期的長度單位(比如:年,月,周等等,加減類型的方法則是基于時間單位進行運算,從而修改日期時間對象的屬性)。
LocalDate
, LocalTime
, LocalDateTime
, Instant
這幾個類中還存在著大量通用型的方法,實際開發中可以針對具體的需求來查看和使用,這里不再一一贅述。
TemporalAdjuster
本來講到這里,關于日期和時間對象的修改已經滿足了大部分的需求了。但是,實際開發中我們遇到的變態需求往往才是我們關注的重點,如何滿足這一部分的需求才是重點需要描述的內容。
舉例一下情況:
- 當前日期后的下一個周日
- 五月的第二個周四
- 當前月的最后一天
- 明年的第一天是周幾
以上四個類似基于目前我們了解到的內容處理起來還是比較棘手的,因為這些邏輯都相對來說比較復雜,不是很直接。這個時候就需要TemporalAdjuster
類來幫助我們更加靈活的處理和計算日期。
首先,TemporalAdjuster
中預置了很多日常開發中比較常見的調整模式,我們可以借助通用的with方法,來對已有日期進行計算。下面我們對上面的四個例子來進行實現和說明。
// 假設當前是:2018-04-02
LocalDate date = LocalDate.of(2018, 4, 2);
// 當前日期的下一個周日
LocalDate date2 = date.with(nextOrSame(DayOfWeek.SUNDAY));
System.out.println(date2); // 2018-04-08
// 五月的第二個周四
LocalDate date3 = date.plusMonths(1).withDayOfMonth(1) // 先修改日期至5月1日
.with(nextOrSame(DayOfWeek.THURSDAY)) //如果5月1日為周四,則不往后,所以這里用nextOrSame
.with(next(DayOfWeek.THURSDAY)); // 第二次,這個日期肯定是周四,所以強制往后,使用next
System.out.println(date3); // 2018-05-10
// 當月的最后一天的日期
LocalDate date4 = date.with(lastDayOfMonth());
System.out.println(date4); // 2018-04-30
// 明年的第一天是周幾
DayOfWeek date5 = date.with(firstDayOfNextYear()).getDayOfWeek();
System.out.println(date5.getValue()); // 2 周二
定制TemporalAdjuster
當然了,這種復雜的日期調整規則除了常見的之外,還有很多奇奇怪怪的需求,這些需求都是預置的規則滿足不了的。這個時候我們就需要根據自己的需求來實現對應的邏輯。
要實現自己的TemporalAdjuster
也十分容易,首先來看一下其源碼:
很明顯,這是一個函數式申明的接口,對應的輸入輸出都是Temporal
對象。所以,我們只需要針對這個接口實現對應的邏輯即可,如果項目中實現的邏輯較為復雜且多處調用,就可以抽象為靜態的工具方法;否則直接使用lambda表達式即可。
這里我們舉個例子,實現一個TemporalAdjuster
,返回當前日期往后的第一個工作日。這里不考慮法定節假日(當然,如果實際項目中有這樣的需求,則必須有法定節假日相關的接口或者配置數據,否則沒有辦法動態實現,因為目前來說國內的節假日都是國家根據當前的情況調整的)
規則抽象:
如果當前是周一到周四,則返回當前日期的下一天,否則返回下一個周一
實現:
// 下一個工作日的實現
TemporalAdjuster nextWorkingDay = (Temporal temporal) -> {
Objects.requireNonNull(temporal);
int dayOfWeek = temporal.get(ChronoField.DAY_OF_WEEK);
if (dayOfWeek >= 1 && dayOfWeek <= 4) {
return temporal.plus(1L, ChronoUnit.DAYS);
}
return temporal.with(next(DayOfWeek.MONDAY));
};
// 測試
LocalDate date = LocalDate.of(2018, 4, 1);
System.out.println(date.with(nextWorkingDay)); // 2018-04-02
LocalDate date2 = date.plusMonths(3).plusDays(2);
System.out.println(date2.with(nextWorkingDay)); // 2018-07-04
2.2 解析和格式化日期和時間對象
處理日期和時間相關方面的業務,還有一個很重要的方面就是格式化輸出日期和解析日期相關的字符串。在java8中,java.time.format
包就是用來格式化和解析日期相關的內容。
上文我們提到過格式化輸出日期的的類DateTimeFormatter
就是java.tiem.format
包下最常用的格式化日期時間的類。接下來的內容就圍繞DateTimeFormatter
來進行講解。
DateTimeFormatter基本使用
DateTimeFormatter
和原來的java.util.DateFormat
最大的不同就是其是線程安全的。這是一個十分重要的點,線程安全意味著能夠以單例的模式創建格式化的容器,并在多個線程之間共享。除此之外,其實新的time
包中幾乎所有的設計都在強調不可變性,這就意味著在多線程的情況下,新的time
包中的內容我們可以大膽放心的使用,這在多線程流的配合下,處理大量的日期時間類數據時十分有效的。
關鍵字: 線程安全
因為是線程安全的,所以DateTimeFormatter
內置了很多常用的實例,如下:
// 2018-04-01
LocalDate date = LocalDate.of(2018, 4, 1);
// 格式化輸出
String basicIsoDateStr = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20180401
String isoLocalDate = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2018-04-01
System.out.println("格式化輸出:\n" + basicIsoDateStr + "\n" + isoLocalDate);
// 解析
LocalDate date2 = LocalDate.parse(basicIsoDateStr, DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date3 = LocalDate.parse(isoLocalDate, DateTimeFormatter.ISO_LOCAL_DATE);
System.out.println("解析輸出:\n" + date2 + "\n" + date3);
這里需要說明的是,將日期時間格式化輸出為字符串和將字符串解析為對應的日期時間對象往往同時出現的。換個角度理解,DateTimeFormatter
存在的意義就是將日期時間對象和特定格式的日期時間字符串聯系起來,成為兩者互轉的一個紐帶。
當然,實際開發中自定義格式化的格式也是不可避免的,如下:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
LocalDate date = LocalDate.of(2018, 4, 1);
String dateStr = date.format(formatter);
System.out.println(dateStr);
LocalDate date2 = LocalDate.parse(dateStr, formatter);
System.out.println(date2);
除了自定格式之外,本地化也是一個十分重要的點,如下 :
LocalDate date = LocalDate.of(2018, 4, 1);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yy年 MMMM d", Locale.CHINA);
String dateStr = date.format(formatter);
System.out.println(dateStr); // 18年 四月 1
LocalDate date2 = LocalDate.parse(dateStr, formatter);
System.out.println(date2); // 2018-04-01
最后,需要說明的是formatter
還支持builder模式,這樣創建自定的格式時將會非常的高效和使用,如下:
LocalDate date = LocalDate.of(2018, 04, 01);
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.YEAR)
.appendLiteral("年")
.appendText(ChronoField.MONTH_OF_YEAR, TextStyle.FULL)
.appendText(ChronoField.DAY_OF_MONTH, TextStyle.FULL_STANDALONE)
.appendLiteral("日 ")
.appendText(ChronoField.DAY_OF_WEEK, TextStyle.FULL)
.parseCaseInsensitive()
.toFormatter(Locale.CHINESE);
String dateStr = date.format(formatter);
System.out.println(dateStr); // 2018年四月1日 星期日
LocalDate date2 = LocalDate.parse(dateStr, formatter);
System.out.println(date2); // 2018-04-01
3. 處理不同的時區和歷法
未完待續。。。