# 新的日期 API
## 簡介
Java 8中的時區操作被很大程度上簡化了,新的時區類`java.time.ZoneId`是原有的`java.util.TimeZone`類的替代品。
ZoneId對象可以通過`ZoneId.of()`方法創建,也可以通過`ZoneId.systemDefault()`獲取系統默認時區:
~~~
ZoneId shanghaiZoneId = ZoneId.of("Asia/Shanghai");
ZoneId systemZoneId = ZoneId.systemDefault();
~~~
`of()`方法接收一個“區域/城市”的字符串作為參數,你可以通過`getAvailableZoneIds()`方法獲取所有合法的“區域/城市”字符串:
~~~
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
~~~
對于老的時區類`TimeZone`,Java 8也提供了轉化方法:
~~~
ZoneId oldToNewZoneId = TimeZone.getDefault().toZoneId();
~~~
有了`ZoneId`,我們就可以將一個`LocalDate`、`LocalTime`或`LocalDateTime`對象轉化為`ZonedDateTime`對象:
~~~
LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, shanghaiZoneId);
~~~
`ZonedDateTime`對象由兩部分構成,`LocalDateTime`和`ZoneId`,其中`2018-03-03T15:26:56.147`部分為`LocalDateTime`,`+08:00[Asia/Shanghai]`部分為ZoneId。
另一種表示時區的方式是使用`ZoneOffset`,它是以當前時間和**世界標準時間(UTC)/格林威治時間(GMT)**的偏差來計算,例如:
~~~
ZoneOffset zoneOffset = ZoneOffset.of("+09:00");
LocalDateTime localDateTime = LocalDateTime.now();
OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, zoneOffset);
~~~
## Instant
Instant類在Java日期與時間功能中,表示了時間線上一個確切的點,定義為距離初始時間的時間差(初始時間為GMT 1970年1月1日00:00)經測量一天有86400秒,從初始時間開始不斷向前移動。
**創建一個Instant實例**
你可以通過Instant類的工廠方法創建一個Instant實例,例如你可以調用instant.now()來創建一個確切的表達當前時間的Instant對象:
~~~
Instant now = Instant.now();
~~~
另外也有一些其它方法能創建Instant,具體請查閱Java官方文檔。
**訪問Instant的時間**
一個Instant對象里有兩個域:距離初始時間的秒鐘數、在當前一秒內的第幾納秒,他們的組合表達了當前時間點。你可以通過以下兩個方法得到它們的值:
~~~
long seconds = getEpochSecond()
int nanos = getNano()
~~~
**Instant的計算**
Instant類有一些方法,可以用于獲得另一Instant的值,例如:
* `plusSeconds()`
* `plusMillis()`
* `plusNanos()`
* `minusSeconds()`
* `minusMillis()`
* `minusNanos()`
我下面將向你展示兩個例子,來說明這些方法如何使用:
~~~
Instant now = Instant.now();
Instant later = now.plusSeconds(3);
Instant earlier = now.minusSeconds(3);
~~~
第一行獲得了一個Instant對象,表示當前時間。第二行創建了一個Instant表示三秒后,第三行創建了一個Instant表示三秒前。
> seconds 表示從`1970-01-01 00:00:00`開始到現在的秒數,nanos 表示納秒部分(nanos的值不會超過999,999,999)
## Clock
Clock類提供了訪問當前日期和時間的方法,Clock是時區敏感的,可以用來取代`System.currentTimeMillis()`來獲取當前的微秒數。
某一個特定的時間點也可以使用Instant類來表示,Instant 類也可以用來創建老的`java.util.Date`對象。
~~~
Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant); // legacy java.util.Date
~~~
## LocalDate
LocalDate類是Java 8中日期時間功能里表示一個本地日期的類,它的日期是無時區屬性的。
可以用來表示生日、節假日期等等。這個類用于表示一個確切的日期,而不是這個日期所在的時間(如java.util.Date中的2000.01.01表示的實際是這一天的00:00這個瞬間)。
LocalDate類位于java.time包下,人名叫java.time.LocalDate,創建出來的實例也是不可變對象,所以涉及它的計算方法將返回一個新的LocalDate。
**創建一個LocalDate實例**
我們有多種方式可以創建出`LocalDate`實例。第一種方法是使用`now()`方法獲得值為今天當日的`LocalDate`對象:
~~~
LocalDate localDate = LocalDate.now();
~~~
另一種方法是使用年月日信息構造出LocalDate對象:
~~~
LocalDate localDate2 = LocalDate.of(2018, 3, 3);
~~~
LocalDate 的`of()`方法創建出一個指定年月日的日期,并且沒有時區信息。
**訪問日期信息**
可以用如下方法訪問LocalDate中的日期信息:
~~~
int year = localDate.getYear();
Month month = localDate.getMonth();
int dayOfMonth = localDate.getDayOfMonth();
int dayOfYear = localDate.getDayOfYear();
DayOfWeek dayOfWeek = localDate.getDayOfWeek();
~~~
可以注意到getMonth()與getDayOfWeek()方法返回了一個枚舉類型代替一個int。你可以通過枚舉類型中的getValue()來獲得信息。
**LocalDate計算**
你可以進行一堆簡單的日期計算,只要使用如下的方法:
* `plusDays()`
* `plusWeeks()`
* `plusMonths()`
* `plusYears()`
* `minusDays()`
* `minusWeeks()`
* `minusMonths()`
* `minusYears()`
以下舉幾個使用的例子來幫助理解使用:
~~~
LocalDate d = LocalDate.of(2018, 3, 5);
LocalDate d1 = localDate.plusYears(3);
LocalDate d2 = localDate.minusYears(3);
~~~
1. 第一行創建出一個新的LocalDate對象d,表示2018.3.5。
2. 第二行創建了值等于d日期3年后的LocalDate對象,第三行也是一樣,只是值改為d日期的三年前。
## LocalTime
LocalTime類是Java 8中日期時間功能里表示一整天中某個時間點的類,它的時間是無時區屬性的(早上10點等等)。比如你需要描述學校幾點開學,這個時間不涉及在什么城市,這個描述是對任何國家城市都適用的,此時使用無時區的LocalTime就足夠了。
LocalTime類的對象也是不可變的,所以計算方法會返回一個新的LocalTime實例。
**創建一個LocatTime實例**
有多種方式可以新建LocalTime實例。比如使用當前時間作為值新建對象:
~~~
LocalTime localTime = LocalTime.now();
~~~
另一種方式是使用指定的時分秒和納秒來新建對象:
~~~
LocalTime localTime2 = LocalTime.of(21, 30, 59, 11001);
~~~
也有另一種版本的`of()`方法只需要小時分鐘兩項,或時分秒三項值作為參數。
**訪問LocalTime對象的時間**
你可以通過這些方法訪問其時、分、秒、納秒:
* `getHour()`
* `getMinute()`
* `getSecond()`
* `getNano()`
**LocalTime的計算**
LocalTime類包含一系列方法,能幫你完成時間計算:
* `plusHours()`
* `plusMinutes()`
* `plusSeconds()`
* `plusNanos()`
* `minusHours()`
* `minusMinutes()`
* `minusSeconds()`
* `minusNanos()`
以下舉一個例子:
~~~
LocalTime localTime2 = LocalTime.of(21, 30, 59, 11001);
LocalTime localTimeLater = localTime.plusHours(3);
LocalTime localTimeEarlier = localTime.minusHours(3);
~~~
1. 第一行新建一個LocalTime實例,表示21:30:50的第11001納秒。
2. 第二行新建了一個LocalTime實例表示這個時間的三小時后,第三行表示三小時前。
3. LocalTime類是Java 8中日期時間功能里表示一整天中某個時間點的類,它的時間是無時區屬性的(早上10點等等)。比如你需要描述學校幾點開學,這個時間不涉及在什么城市,這個描述是對任何國家城市都適用的,此時使用無時區的LocalTime就足夠了。
LocalTime類的對象也是不可變的,所以計算方法會返回一個新的LocalTime實例。
## LocalDateTime
LocalDateTime類是Java 8中日期時間功能里,用于表示當地的日期與時間的類,它的值是無時區屬性的。你可以將其視為Java 8中LocalDate與LocalTime兩個類的結合。
LocalDateTime類的值是不可變的,所以其計算方法會返回一個新的LocalDateTime實例。
**創建一個LocatDateTime實例**
可以通過LocalDateTime的靜態工廠方法來創建LocalDateTime實例。以下舉例使用`now()`方法創建:
~~~
LocalDateTime localDateTime = LocalDateTime.now();
~~~
另一種方式是使用指定的年月日、時分秒、納秒來新建對象:
~~~
LocalDateTime localDateTime2 = LocalDateTime.of(2018, 11, 26, 13, 55, 36, 123);
~~~
**訪問LocalDateTime對象的時間**
你可以通過這些方法訪問其日期時間:
* `getYear()`
* `getMonth()`
* `getDayOfMonth()`
* `getDayOfWeek()`
* `getDayOfYear()`
* `getHour()`
* `getMinute()`
* `getSecond()`
* `getNano()`
這些方法中有一些返回int有一些返回枚舉類型,你可以通過枚舉類型中的`getValue()`方法來獲得int值。
**LocalDateTime的計算**
LocalDateTime 類包含一系列方法,能幫你完成時間計算:
* `plusYears()`
* `plusMonths()`
* `plusDays()`
* `plusHours()`
* `plusMinutes()`
* `plusSeconds()`
* `plusNanos()`
* `minusYears()`
* `minusMonths()`
* `minusDays()`
* `minusHours()`
* `minusMinutes()`
* `minusSeconds()`
* `minusNanos()`
以下舉一個例子:
~~~
LocalDateTime localDateTime = LocalDateTime.now();
LocalDateTime localDateTime1 = localDateTime.plusYears(3);
LocalDateTime localDateTime2 = localDateTime.minusYears(3);
~~~
1. 第一行新建一個LocalDateTime實例表示當前這個時間。
2. 第二行新建了一個LocalDateTime實例表示三年后。
3. 第三行也新建了一個LocalDateTime實例表示三小時前。
## ZonedDateTime
ZonedDateTime類是Java 8中日期時間功能里,用于表示帶時區的日期與時間信息的類。可以用于表示一個真實事件的開始時間,如某火箭升空時間等等。
ZonedDateTime 類的值是不可變的,所以其計算方法會返回一個新的ZonedDateTime 實例。
**創建一個ZonedDateTime實例**
有多種方式可以新建ZonedDateTime實例。比如使用當前時間作為值新建對象:
~~~
ZonedDateTime dateTime = ZonedDateTime.now();
~~~
另一種方式是使用指定的年月日、時分秒、納秒以及時區ID來新建對象:
~~~
ZoneId zoneId = ZoneId.of("UTC+1");
ZonedDateTime dateTime2 = ZonedDateTime.of(2015, 11, 30, 23, 45, 59, 1234, zoneId);
~~~
**訪問ZonedDateTime對象的時間**
你可以通過這些方法訪問其日期時間:
* `getYear()`
* `getMonth()`
* `getDayOfMonth()`
* `getDayOfWeek()`
* `getDayOfYear()`
* `getHour()`
* `getMinute()`
* `getSecond()`
* `getNano()`
這些方法中有一些返回int有一些返回枚舉類型,但可以通過枚舉類型中的getValue()方法來獲得int值。
**ZonedDateTime的計算**
ZonedDateTime類包含一系列方法,能幫你完成時間計算:
* `plusYears()`
* `plusMonths()`
* `plusDays()`
* `plusHours()`
* `plusMinutes()`
* `plusSeconds()`
* `plusNanos()`
* `minusYears()`
* `minusMonths()`
* `minusDays()`
* `minusHours()`
* `minusMinutes()`
* `minusSeconds()`
* `minusNanos()`
但注意計算時,若不巧跨越了夏令時(會補一小時或減一小時),可能得不到希望的結果。一個替代的正確做法是使用Period:
~~~
ZonedDateTime zoneDateTime = previousDateTime.plus(Period.ofDays(3));
~~~
**時區**
時區是用ZoneId類表示的,你可以使用ZoneId.now()或ZoneId.of(“xxx”)來實例化:
~~~
ZoneId zoneId = ZoneId.of("UTC+1");
~~~
傳給`of()`方法的參數是時區的ID,如“UTC+1”指距離UTC(格林威治時間)有一小時的時差,你可以使用你想要的時差來表示ZoneId(如+1與-5等等)
你也可以使用另一種方式表示zone id,即使用地區名字,也是可以的:
~~~
ZoneId zoneId2 = ZoneId.of("Europe/Copenhagen");
ZoneId zoneId3 = ZoneId.of("Europe/Paris");
~~~
## DateTimeFormatter
DateTimeFormatter類是Java 8中日期時間功能里,用于解析和格式化日期時間的類,位于`java.time.format`包下。
**預定義的DateTimeFormatter實例**
DateTimeFormatter類包含一系列預定義(常量)的實例,可以解析和格式化一些標準時間格式。這將讓你免除麻煩的時間格式定義,類中包含如下預定義的實例:
~~~
BASIC_ISO_DATE
ISO_LOCAL_DATE
ISO_LOCAL_TIME
ISO_LOCAL_DATE_TIME
ISO_OFFSET_DATE
ISO_OFFSET_TIME
ISO_OFFSET_DATE_TIME
ISO_ZONED_DATE_TIME
ISO_INSTANT
ISO_DATE
ISO_TIME
ISO_DATE_TIME
ISO_ORDINAL_TIME
ISO_WEEK_DATE
RFC_1123_DATE_TIME
~~~
每個預定義的DateTimeFormatter實例都有不同的日期格式,我就不解釋全部的了。具體的可以查閱Java官方文檔,但我在這篇的后續中會解釋其中幾個,以方便理解。
**格式化日期**
當你獲取一個DateTimeFormatter實例后,就可以用format()方便來將一個日期格式化為某種字符串,例如:
~~~
DateTimeFormatter formatter = DateTimeFormatter.BASIC_ISO_DATE;
String formattedDate = formatter.format(LocalDate.now());
System.out.println(formattedDate);
~~~
這個樣例把LocalDate對象格式化了,并輸出20150703,這個輸出表示現在2018年,3月5日。
再舉一個關于ZonedDateTime的例子:
~~~
DateTimeFormatter formatter = DateTimeFormatter.BASIC_ISO_DATE;
String formattedZonedDate = formatter.format(ZonedDateTime.now());
System.out.println("formattedZonedDate = " + formattedZonedDate);
~~~
這個例子會輸出:20180305+0800
表示今年2018年,3月5日,位于UTC+8時區。
## Duration
一個Duration對象表示兩個Instant間的一段時間,是在Java 8中加入的新功能。
一個Duration實例是不可變的,當創建出對象后就不能改變它的值了。你只能通過Duration的計算方法,來創建出一個新的Durtaion對象。你會在之后的教程中見到的。
**創建Duration實例**
使用`Duration`類的工廠方法來創建一個`Duration`對象,以下是一個使用`between()`的例子:
~~~
Instant first = Instant.now();
// wait some time while something happens
Instant second = Instant.now();
Duration duration = Duration.between(first, second);
~~~
**訪問Duration的時間**
一個Duration對象里有兩個域:納秒值(小于一秒的部分),秒鐘值(一共有幾秒),他們的組合表達了時間長度。注意屯使用System.getCurrentTimeMillis()時不同,Duration不包含毫秒這個屬性。
你可以通過以下兩個方法得到它們的值:
~~~
long seconds = getSeconds()
int nanos = getNano()
~~~
你也可以轉換整個時間到其它單位如納秒、分鐘、小時、天:
* `toNanos()`
* `toMillis()`
* `toMinutes()`
* `toHours()`
* `toDays()`
舉例而言:`toNanos()`與`getNano()`不同,`toNanos()`獲得的是`Duration`整個時間共有多少納秒,
而`getNano()`只是獲得這段時間中小于一秒的部分。
你也許會問,為什么沒有`toSeconds()`方法,因為已經有`getSeconds()`這個方法能達到同樣的功能了。
**Duration計算**
Duration類包含一系列的計算方法:
* `plusNanos()`
* `plusMillis()`
* `plusSeconds()`
* `plusMinutes()`
* `plusHours()`
* `plusDays()`
* `minusNanos()`
* `minusMillis()`
* `minusSeconds()`
* `minusMinutes()`
* `minusHours()`
* `minusDays()`
這些方法所做的事都是相似的,我在這兒也不展示內部實現細節了,就展示一個加減的例子吧:
~~~
Duration start = ... //obtain a start duration
Duration added = start.plusDays(3);
Duration subtracted = start.minusDays(3);
~~~
1. 第一行創建了一個Duration對象叫start,具體怎么創建可以參考前面的代碼。
2. 第二三行樣例創建了兩個新的Duration,通過調用start的加減操作,使得added對象表示的時間比start多三天,而substracted則少三天。
所有的計算方法都會返回一個新的Duration,以保證Duration的不可變屬性。
~~~
long days = duration.toDays(); // 這段時間的總天數
long hours = duration.toHours(); // 這段時間的小時數
long minutes = duration.toMinutes(); // 這段時間的分鐘數
long seconds = duration.getSeconds(); // 這段時間的秒數
long milliSeconds = duration.toMillis(); // 這段時間的毫秒數
long nanoSeconds = duration.toNanos(); // 這段時間的納秒數
~~~
## 其他操作
### 增加和減少日期
Java 8中的日期/時間類都是不可變的,這是為了保證線程安全。當然,新的日期/時間類也提供了方法用于創建對象的可變版本,比如增加一天或者減少一天:
~~~
LocalDate date = LocalDate.of(2017, 1, 5); // 2017-01-05
LocalDate date1 = date.withYear(2016); // 修改為 2016-01-05
LocalDate date2 = date.withMonth(2); // 修改為 2017-02-05
LocalDate date3 = date.withDayOfMonth(1); // 修改為 2017-01-01
LocalDate date4 = date.plusYears(1); // 增加一年 2018-01-05
LocalDate date5 = date.minusMonths(2); // 減少兩個月 2016-11-05
LocalDate date6 = date.plus(5, ChronoUnit.DAYS); // 增加5天 2017-01-10
~~~
上面例子中對于日期的操作比較簡單,但是有些時候我們要面臨更復雜的時間操作,比如將時間調到下一個工作日,
或者是下個月的最后一天,這時候我們可以使用`with()`方法的另一個重載方法,它接收一個TemporalAdjuster參數,
可以使我們更加靈活的調整日期:
~~~
LocalDate date7 = date.with(nextOrSame(DayOfWeek.SUNDAY)); // 返回下一個距離當前時間最近的星期日
LocalDate date9 = date.with(lastInMonth(DayOfWeek.SATURDAY)); // 返回本月最后一個星期六
~~~
要使上面的代碼正確編譯,你需要使用靜態導入`TemporalAdjusters`對象:
`import static java.time.temporal.TemporalAdjusters.*;`
`TemporalAdjusters`類中包含了很多靜態方法可以直接使用,下面的表格列出了一些方法:

如果上面表格中列出的方法不能滿足你的需求,你還可以創建自定義的`TemporalAdjuster`接口的實現,
`TemporalAdjuster`也是一個函數式接口,所以我們可以使用Lambda表達式:
~~~
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
~~~
比如給定一個日期,計算該日期的下一個工作日(不包括星期六和星期天):
~~~
LocalDate date = LocalDate.of(2017, 1, 5);
date.with(temporal -> {
// 當前日期
DayOfWeek dayOfWeek = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
// 正常情況下,每次增加一天
int dayToAdd = 1;
// 如果是星期五,增加三天
if (dayOfWeek == DayOfWeek.FRIDAY) {
dayToAdd = 3;
}
// 如果是星期六,增加兩天
if (dayOfWeek == DayOfWeek.SATURDAY) {
dayToAdd = 2;
}
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
});
~~~
### 其他歷法
Java中使用的歷法是ISO 8601日歷系統,它是世界民用歷法,也就是我們所說的公歷。平年有365天,閏年是366天。閏年的定義是:非世紀年,能被4整除;世紀年能被400整除。為了計算的一致性,公元1年的前一年被當做公元0年,以此類推。
此外Java 8還提供了4套其他歷法(很奇怪為什么沒有漢族人使用的農歷),每套歷法都包含一個日期類,分別是:
* `ThaiBuddhistDate`:泰國佛教歷
* `MinguoDate`:中華民國歷
* `JapaneseDate`:日本歷
* `HijrahDate`:伊斯蘭歷
每個日期類都繼承`ChronoLocalDate`類,所以可以在不知道具體歷法的情況下也可以操作。不過這些歷法一般不常用,除非是有某些特殊需求情況下才會使用。
這些不同的歷法也可以用于向公歷轉換:
~~~
LocalDate date = LocalDate.now();
JapaneseDate jpDate = JapaneseDate.from(date);
~~~
由于它們都繼承ChronoLocalDate類,所以在不知道具體歷法情況下,可以通過ChronoLocalDate類操作日期:
~~~
Chronology jpChronology = Chronology.ofLocale(Locale.JAPANESE);
ChronoLocalDate jpChronoLocalDate = jpChronology.dateNow();
~~~
我們在開發過程中應該盡量避免使用`ChronoLocalDate`,盡量用與歷法無關的方式操作時間,因為不同的歷法計算日期的方式不一樣,
比如開發者會在程序中做一些假設,假設一年中有12個月,如果是中國農歷中包含了閏月,一年有可能是13個月,
但開發者認為是12個月,多出來的一個月屬于明年的。
再比如假設年份是累加的,過了一年就在原來的年份上加一,但日本天皇在換代之后需要重新紀年,所以過了一年年份可能會從1開始計算。
在實際開發過程中建議使用`LocalDate`,包括存儲、操作、業務規則的解讀;除非需要將程序的輸入或者輸出本地化,
這時可以使用`ChronoLocalDate`類。
- 序
- 快速開始
- 環境要求
- 環境準備
- 工程導入
- 工程運行
- 技術基礎
- Java8
- Lambda
- Lambda 受檢異常處理
- Stream 簡介
- Stream API 一覽
- Stream API(上)
- Stream API(下)
- Optional 干掉空指針
- 函數式接口
- 新的日期 API
- Lombok
- SpringMVC
- Swagger
- Mybaties
- Mybaties-plus
- 開發初探
- 新建微服務工程
- 第一個API
- API鑒權
- API響應結果
- Redis 緩存
- 第一個CRUD
- 建表
- 建Entity
- 建Service和Mapper
- 新增API
- 修改API
- 刪除API
- 查詢API
- 單條查詢
- 多條查詢
- 分頁
- 微服務遠程調用
- 聲明式服務調用Feign
- 熔斷機制 Hystrix
- 開發進階