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

詳解Java8的日期和時間API

詳解Java8的日期和時間API

JDK1.0的時候,Java引入了java.util.Date來處理日期和時間;在JDK1.1的時候又引入了功能更強大的java.util.Calendar,但是Calendar的API還是不盡如人意,,存在例項易變、沒有處理閏秒等等的問題。所以在JDK1.8的時候,Java引入了java.timeAPI,這才真正修改了過去的缺陷,且更為好用。本篇就詳細介紹一下JDK1.8的日期和時間API。本篇主要包括以下內容:

  • 詳解Java8的日期和時間API
    • Java8之前的日期和時間API的缺陷
    • java.time類圖介紹
      • 概況
      • chrono
      • format
      • temporal
      • zone
    • Java 8日期/時間類
      • Instant
      • Duration
      • Period
      • LocalDate和LocalTime
      • LocalDateTime
    • 日期操作和格式化
    • 時區

Java8之前的日期和時間API的缺陷

在Java 8之前,所有關於時間和日期的API都存在各種使用方面的缺陷,主要有:

  1. Java的java.util.Date和java.util.Calendar類易用性差,不支援時區,而且他們都不是執行緒安全的;
  2. 用於格式化日期的類DateFormat被放在java.text包中,它是一個抽象類,所以我們需要例項化一個SimpleDateFormat物件來處理日期格式化,並且DateFormat也是非執行緒安全,這意味著如果你在多執行緒程式中呼叫同一個DateFormat物件,會得到意想不到的結果。
  3. 對日期的計算方式繁瑣,而且容易出錯,因為月份是從0開始的,從Calendar中獲取的月份需要加一才能表示當前月份。

由於以上這些問題,出現了一些第三方的日期處理框架,例如Joda-Time,date4j等開源專案。但是,Java需要一套標準的用於處理時間和日期的框架,於是Java 8中引入了新的日期API。新的日期API是JSR-310規範的實現,Joda-Time框架的作者正是JSR-310的規範的倡導者,所以能從Java 8的日期API中看到很多Joda-Time的特性。

java.time類圖介紹

概況

首先來看一下java.time這個包下的類結構圖:

可以看到,除了一些日期、時間類之外,還有四個包:chrono、format、temporal、zone。先簡略介紹下這四個包的用途。

chrono

chrono包提供曆法相關的介面與實現。Java中預設使用的歷法是ISO 8601日曆系統,它是世界民用曆法,也就是我們所說的公曆。平年有365天,閏年是366天。閏年的定義是:非世紀年,能被4整除;世紀年能被400整除。為了計算的一致性,公元1年的前一年被當做公元0年,以此類推。此外chrono包提供了四種其他曆法,每種曆法有自己的紀元(Era)類、日曆類和日期類,分別是:

  • 泰國佛教歷:ThaiBuddhistEra、ThaiBuddhistChronology和ThaiBuddhistDate;
  • 民國曆:MinguoEra、MinguoChronology和MinguoDate;
  • 日本歷:JapaneseEra、JapaneseChronology和JapaneseDate
  • 伊斯蘭曆:HijrahEra、HijrahChronology和HijrahDate:

每個紀元類都是一個列舉類,實現Era介面。Era表示的是一個時間線的分割,比如Java預設的ISO曆法中的IsoEra,就包含兩個列舉量:BCE和CE,前者表示“公元前”,後者表示“公元”;再比如MinguoEra,包含了兩個列舉量:BEFORE_ROC和ROC,ROC的意思是Republic of China,也即新中國,前者表示的就是新中國之前,也即民國,後者表示新中國;所以中國的歷法用了“Minguo”這個名字。每種曆法的日曆系統的實現都是依賴於其紀元的。每個日曆類都實現了抽象類AbstractChronology,其中定義了從時間、id、地域設定獲取具體日曆系統的介面和實現,以及獲取特定日曆系統下的時間的方法。定義了紀元和日曆系統之後,日期類自然就確定好了,每種曆法的日期類提供的介面並無大的不同,在實際開發中應用的比較少,也不是本篇的重點,暫且略過。

format

format包提供了日期格式化的方法。format包中定義了時區名稱、日期解析和格式化的各種列舉,以及最為重要的格式化類DateTimeFormatter。需要注意的是,format包類中的類都是final的,都提供了執行緒安全的訪問。在DateTimeFormatter類中提供了ofPattern的靜態方法來獲得一個DateTimeFormatter,但細看其實現,其實還是呼叫的DateTimeFormatterBuilder的靜態方法:DateTimeFormatterBuilder.appendPattern(pattern).toFormatter();所以我們在實際格式化日期和時間的時候,是兩種方式都可以使用的。

temporal

temporal包中定義了整個日期時間框架的基礎:各種時間單位、時間調節器,以及在年月日時分秒中用到的各種屬性。Java8中的日期時間類都是實現了temporal包中的時間單位(Temporal)、時間調節器(TemporalAdjuster)和各種屬性的介面,所以在後面的日期的操作方法中都是以最基本的時間單位和各種屬性為引數的。

zone

這個包沒啥多說的,就是定義了時區轉換的各種方法。

Java 8日期/時間類

Java 8的日期和時間類包括Instant、Duration、Period、LocalDate、LocalTime,這些類都包含在java.time包中。下面逐一來看看這些類的用法。

Instant

Instant是時間線上的一個點,表示一個時間戳。Instant可以精確到納秒,這超過了long的最大表示範圍,所以在Instant的實現中是分成了兩部分來表示,一部分是seconds,表示從1970-01-01 00:00:00開始到現在的秒數,另一個部分是nanos,表示納秒部分。以下是建立Instant的兩種方法:

1
2
Instant now = Instant.now(); 
Instant instant = Instant.ofEpochSecond(60, 100000);
  1. 獲取當前時刻的時間戳,結果為:2020-02-20T14:14:15.913Z

  2. ofEpochSecond()方法的第一個引數為秒,第二個引數為納秒,上面的程式碼表示從1970-01-01 00:00:00開始後一分鐘的10萬納秒的時刻,其結果為:1970-01-01T00:01:00.000100Z

Duration

有了時間點,自然就衍生出時間段了,那就是Duration。Duration的內部實現與Instant類似,也是包含兩部分:seconds表示秒,nanos表示納秒。Duration是兩個時間戳的差值,所以使用java.time中的時間戳類,例如Instant、LocalDateTime等實現了Temporal類的日期時間類為引數,通過Duration.between()方法建立Duration物件:

1
2
3
LocalDateTime from = LocalDateTime.of(2020, Month.JANUARY, 22, 16, 6, 0);    // 2020-01-22 16:06:00
LocalDateTime to = LocalDateTime.of(2020, Month.FEBRUARY, 22, 16, 6, 0);     // 2020-02-22 16:06:00
Duration duration = Duration.between(from, to);     // 表示從 2020-01-22 16:06:00到 2020-02-22 16:06:00 這段時間

Duration物件還可以通過of()方法建立,該方法接受一個時間段長度,和一個時間單位作為引數:

1
2
Duration duration1 = Duration.of(5, ChronoUnit.DAYS);       // 5天
Duration duration2 = Duration.of(1000, ChronoUnit.MILLIS);  // 1000毫秒

Period

Period在概念上和Duration類似,區別在於Period是以年月日來衡量一個時間段,比如1年2個月3天:Period period = Period.of(1, 2, 3);Period物件也可以通過between()方法建立,值得注意的是,由於Period是以年月日衡量時間段,所以between()方法只能接收LocalDate型別的引數:

1
2
3
4
// 2020-01-22 到 2020-02-22 這段時間
Period period = Period.between(
                LocalDate.of(2020, 1, 22),
                LocalDate.of(2020, 2, 22));

LocalDate和LocalTime

LocalDate類表示一個具體的日期,但不包含具體時間,也不包含時區資訊。可以通過LocalDate的靜態方法of()建立一個例項,LocalDate也包含一些方法用來獲取年份,月份,天,星期幾等:

1
2
3
4
5
6
7
LocalDate localDate = LocalDate.of(2020, 2, 22);     // 初始化一個日期:2022-02-22
int year = localDate.getYear();                     // 年份:2020
Month month = localDate.getMonth();                 // 月份:February
int dayOfMonth = localDate.getDayOfMonth();         // 月份中的第幾天:22
DayOfWeek dayOfWeek = localDate.getDayOfWeek();     // 一週的第幾天:Saturday
int length = localDate.lengthOfMonth();             // 月份的天數:29
boolean leapYear = localDate.isLeapYear();          // 是否為閏年:true

也可以呼叫靜態方法now()來獲取當前日期:LocalDate now = LocalDate.now();LocalTime和LocalDate類似,他們之間的區別在於LocalDate不包含具體時間,而LocalTime包含具體時間,例如:

1
2
3
4
LocalTime localTime = LocalTime.of(16, 14, 52);     // 初始化一個時間:16:14:52
int hour = localTime.getHour();                     // 時:16
int minute = localTime.getMinute();                 // 分:14
int second = localTime.getSecond();                 // 秒:52

LocalDateTime

LocalDateTime類是LocalDate和LocalTime的結合體,可以通過of()方法直接建立,也可以呼叫LocalDate的atTime()方法或LocalTime的atDate()方法將LocalDate或LocalTime合併成一個LocalDateTime:

1
2
3
4
5
LocalDateTime ldt1 = LocalDateTime.of(2020, Month.FEBRUARY, 22, 16, 23, 12);

LocalDate localDate = LocalDate.of(2020, Month.FEBRUARY, 22);
LocalTime localTime = LocalTime.of(16, 23, 12);
LocalDateTime ldt2 = localDate.atTime(localTime);

LocalDateTime也提供用於向LocalDate和LocalTime的轉化:

1
2
LocalDate date = ldt1.toLocalDate();
LocalTime time = ldt1.toLocalTime();

日期操作和格式化

在上面對java.time包中的類的介紹中已經提到,Java8的的日期和時間類都實現了Temporal、TemporalAdjuster,然後在temporal包中定義了日期操作的方法,在format中定義了日期格式化的方法,由此實現了比較通用的日期操作和格式化的方式。首先需要再次明確的一點是,Java8中提供的日期時間物件都是不可變的,因而也是執行緒安全的。所以每次對日期時間物件進行操作的時候都是返回新的日期時間物件。比較簡單的日期操作,比如增加、減少一天、修改年月日等,程式碼如下:

1
2
3
4
5
6
7
LocalDate date = LocalDate.of(2020, 2, 22);          // 2020-02-22
LocalDate date1 = date.withYear(2021);              // 修改為 2021-02-22
LocalDate date2 = date.withMonth(3);                // 修改為 2020-03-22
LocalDate date3 = date.withDayOfMonth(1);           // 修改為 2020-02-01
LocalDate date4 = date.plusYears(1);                // 增加一年 2021-02-22
LocalDate date5 = date.minusMonths(2);              // 減少兩個月,到2019年的12月  2019-12-22
LocalDate date6 = date.plus(5, ChronoUnit.DAYS);    // 增加5天 2020-02-27

比較複雜的日期操作,比如將時間調到下一個工作日,或者是下個月的最後一天,這時候我們可以使用with()方法的另一個過載方法,它接收一個TemporalAdjuster引數,可以使我們更加靈活的調整日期:

1
2
LocalDate date7 = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));      // 返回下一個距離當前時間最近的星期日 2020-02-23
LocalDate date9 = date.with(TemporalAdjusters.lastInMonth(DayOfWeek.SATURDAY));  // 返回本月最後衣蛾週六 2020-02-29

下面列出時間調節器類TemporalAdjuster提供的一些方法,可供選用:

方法名描述
dayOfWeekInMonth  返回同一個月中每週的第幾天
firstDayOfMonth  返回當月的第一天
firstDayOfNextMonth  返回下月的第一天
firstDayOfNextYear  返回下一年的第一天
firstDayOfYear  返回本年的第一天
firstInMonth  返回同一個月中第一個星期幾
lastDayOfMonth  返回當月的最後一天
lastDayOfNextMonth  返回下月的最後一天
lastDayOfNextYear  返回下一年的最後一天
lastDayOfYear  返回本年的最後一天
lastInMonth  返回同一個月中最後一個星期幾
next / previous  返回後一個/前一個給定的星期幾
nextOrSame / previousOrSame  返回後一個/前一個給定的星期幾,如果這個值滿足條件,直接返回

日期格式化的用法請看上面對format包的介紹。

時區

對時區處理的優化也是Java8中日期時間API的一大亮點。之前在業務中是真的遇到過一些奇葩的時區問題,在舊的java.util.TimeZone提供的時區不全不說,操作還非常繁瑣。新的時區類java.time.ZoneId是原有的java.util.TimeZone類的替代品。ZoneId物件可以通過ZoneId.of()方法建立,也可以通過ZoneId.systemDefault()獲取系統預設時區:

1
2
ZoneId shanghaiZoneId = ZoneId.of("Asia/Shanghai");
ZoneId systemZoneId = ZoneId.systemDefault();

of()方法接收一個“區域/城市”的字串作為引數,你可以通過getAvailableZoneIds()方法獲取所有合法的“區域/城市”字串:

1
Set<String> zoneIds = ZoneId.getAvailableZoneIds();

對於老的時區類TimeZone,Java 8也提供了轉化方法:

1
ZoneId oldToNewZoneId = TimeZone.getDefault().toZoneId();

有了ZoneId,我們就可以將一個LocalDate、LocalTime或LocalDateTime物件轉化為ZonedDateTime物件:

1
2
LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, shanghaiZoneId);

將zonedDateTime列印到控制檯為:

1
2020-02-22T16:50:54.658+08:00[Asia/Shanghai]

ZonedDateTime物件由兩部分構成,LocalDateTime和ZoneId,其中2020-02-22T16:50:54.658部分為LocalDateTime,+08:00[Asia/Shanghai]部分為ZoneId。另一種表示時區的方式是使用ZoneOffset,它是以當前時間和世界標準時間(UTC)/格林威治時間(GMT)的偏差來計算,例如:

1
2
3
ZoneOffset zoneOffset = ZoneOffset.of("+09:00");
LocalDateTime localDateTime = LocalDateTime.now();
OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, zoneOffset);

以上就是Java8中關於日期和時間API的內容了。

關注我的公眾號,獲取更多關於面試、技術的文章及福利資源。

&n