Java 8 日期 – LocalDate、LocalDateTime、Instant

日期時間 API 是 Java 8 發布的最重要功能之一。從一開始,Java 就缺乏一致的日期和時間處理方法,而 Java 8 日期時間 API 是核心 Java API 的一個受歡迎的新增功能。

我們為什麼需要新的 Java 日期時間 API?

在我們開始研究 Java 8 日期時間 API 之前,讓我們看看為什麼我們需要一個新的 API。現有的 java 日期和時間相關類存在幾個問題,其中一些是:

  • Java 日期時間類沒有一致的定義,我們在 java.util 和 java.sql 包中都有 Date 類。同樣,格式化和解析類被定義在 java.text 包中。
  • java.util.Date 同時包含日期和時間值,而 java.sql.Date 只包含日期值。將此放在 java.sql 包中是毫無意義的。而且,這兩個類具有相同的名稱,這本身就是一個非常糟糕的設計。
  • 沒有明確定義的類用於時間、時間戳、格式化和解析。我們有 java.text.DateFormat 抽象類來滿足解析和格式化的需求。通常,會使用 SimpleDateFormat 類來進行解析和格式化。
  • 所有的日期類都是可變的,因此它們不是線程安全的。這是 Java 日期和日曆類的最大問題之一。
  • 日期類別並不提供國際化支援,也沒有時區支援。因此,java.util.Calendar 和 java.util.TimeZone 類別被引入,但它們也有上述列出的所有問題。

日期和日曆類別中定義的方法還有一些其他問題,但上述問題清楚地表明,Java 需要一個堅固的日期時間 API。這就是為什麼 Joda Time 作為 Java 日期時間需求的優質替代品發揮了關鍵作用。

Java 8 日期時間設計原則

Java 8 日期時間 API 是 JSR-310 的實現。它旨在克服遺留日期時間實現中的所有缺陷。新日期時間 API 的一些設計原則包括:

  1. 不可變性:新日期時間 API 中的所有類別都是不可變的,適用於多線程環境。

  2. 關注點分離:新 API 明確地區分了人類可讀的日期時間和機器時間(Unix 時間戳)。它為日期、時間、日期時間、時間戳、時區等定義了獨立的類別。

  3. 清晰度:方法清晰定義,並在所有類中執行相同的操作。例如,要獲取當前實例,我們有now()方法。所有這些類中都定義了format()和parse()方法,而不是為它們單獨創建一個類。

    所有類都使用工廠模式策略模式進行更好的處理。一旦您在其中一個類中使用了方法,與其他類一起工作就不會很困難。

  4. 實用操作: 所有新的日期時間 API 類別都配備了執行常見任務的方法,例如加法、減法、格式化、解析、獲取日期/時間中的各個部分等。

  5. 可擴展: 新的日期時間 API 使用 ISO-8601 日曆系統,但我們也可以與其他非 ISO 日曆一起使用。

日期時間 API 套件

Java 8 日期時間 API 包括以下套件。

  1. java.time: 這是新的 Java 日期時間 API 的基本套件。所有主要的基本類都屬於此套件,如 LocalDate、LocalTime、LocalDateTime、Instant、Period、Duration 等。所有這些類都是不可變的且線程安全的。在大多數情況下,這些類將足以處理常見要求。
  2. java.time.chrono: 此套件定義了非 ISO 日曆系統的通用 API。我們可以擴展 AbstractChronology 類來創建自己的日曆系統。
  3. java.time.format:此套件包含用於格式化和解析日期時間物件的類別。大多數情況下,我們不會直接使用它們,因為java.time套件中的主要類別提供了格式化和解析方法。
  4. java.time.temporal:此套件包含時間物件,我們可以使用它來查找與日期/時間物件相關的特定日期或時間。例如,我們可以使用這些來查找月份的第一天或最後一天。您可以輕鬆識別這些方法,因為它們的格式總是“withXXX”。
  5. java.time.zone Package:此套件包含支援不同時區及其規則的類別。

Java 8 日期時間 API 類別示例

我們已經研究了Java日期時間API的大部分重要部分。現在是時候來看看日期時間API中最重要的類別及其示例了。

1. LocalDate

LocalDate是一個不可變的類別,代表具有默認格式yyyy-MM-dd的日期。我們可以使用now()方法來獲取當前日期。我們還可以為年、月和日提供輸入參數以創建LocalDate實例。

這個類提供了一個對 now() 方法的重載,我們可以傳遞 ZoneId 以獲取特定時區的日期。這個類提供了與 java.sql.Date 相同的功能。

package com.journaldev.java8.time;

import java.time.LocalDate;
import java.time.Month;
import java.time.ZoneId;

/**
 * LocalDate Examples
 * @author pankaj
 *
 */
public class LocalDateExample {

	public static void main(String[] args) {
		
		//當前日期
		LocalDate today = LocalDate.now();
		System.out.println("Current Date="+today);
		
		//通過提供輸入參數創建 LocalDate
		LocalDate firstDay_2014 = LocalDate.of(2014, Month.JANUARY, 1);
		System.out.println("Specific Date="+firstDay_2014);
		
		
		//嘗試通過提供無效輸入創建日期
		//LocalDate feb29_2014 = LocalDate.of(2014, Month.FEBRUARY, 29);
		//執行時發生異常:java.time.DateTimeException:
		//無效日期 'February 29',因為 '2014' 不是閏年
		
		//"Asia/Kolkata" 的當前日期,可以從 ZoneId javadoc 獲取
		LocalDate todayKolkata = LocalDate.now(ZoneId.of("Asia/Kolkata"));
		System.out.println("Current Date in IST="+todayKolkata);

		//java.time.zone.ZoneRulesException: 未知的時區 ID:IST
		//LocalDate todayIST = LocalDate.now(ZoneId.of("IST"));
		
		//從基準日期即 01/01/1970 獲取日期
		LocalDate dateFromBase = LocalDate.ofEpochDay(365);
		System.out.println("365th day from base date= "+dateFromBase);
		
		LocalDate hundredDay2014 = LocalDate.ofYearDay(2014, 100);
		System.out.println("100th day of 2014="+hundredDay2014);
	}

}

在註釋中提供了 LocalDate 方法的解釋。當我們運行此程序時,我們會得到以下輸出。

Current Date=2014-04-28
Specific Date=2014-01-01
Current Date in IST=2014-04-29
365th day from base date= 1971-01-01
100th day of 2014=2014-04-10

2. LocalTime

LocalTime 是一個不可變的類,其實例代表了人類可讀格式的時間。它的默認格式是 hh:mm:ss.zzz。就像 LocalDate 一樣,這個類提供了時區支持,並且通過傳入小時、分鐘和秒作為輸入參數來創建實例。

package com.journaldev.java8.time;

import java.time.LocalTime;
import java.time.ZoneId;

/**
 * LocalTime Examples
 * @author pankaj
 *
 */
public class LocalTimeExample {

	public static void main(String[] args) {
		
		//當前時間
		LocalTime time = LocalTime.now();
		System.out.println("Current Time="+time);
		
		//通過提供輸入參數創建 LocalTime
		LocalTime specificTime = LocalTime.of(12,20,25,40);
		System.out.println("Specific Time of Day="+specificTime);
		
		
		//嘗試通過提供無效輸入來創建時間
		//LocalTime invalidTime = LocalTime.of(25,20);
		//Exception in thread "main" java.time.DateTimeException: 
		//小時數無效(有效值為 0 - 23): 25
		
		//當前日期在 "Asia/Kolkata",你可以在 ZoneId 的 javadoc 中找到它
		LocalTime timeKolkata = LocalTime.now(ZoneId.of("Asia/Kolkata"));
		System.out.println("Current Time in IST="+timeKolkata);

		//java.time.zone.ZoneRulesException: 未知的時區 ID: IST
		//LocalTime todayIST = LocalTime.now(ZoneId.of("IST"));
		
		//從基本日期即 1970/01/01 獲取日期
		LocalTime specificSecondTime = LocalTime.ofSecondOfDay(10000);
		System.out.println("10000th second time= "+specificSecondTime);

	}

}

輸出:

Current Time=15:51:45.240
Specific Time of Day=12:20:25.000000040
Current Time in IST=04:21:45.276
10000th second time= 02:46:40

3. LocalDateTime

LocalDateTime是一個不可變的日期時間物件,表示具有默認格式yyyy-MM-dd-HH-mm-ss.zzz的日期時間。它提供了一個工廠方法,該方法接受LocalDate和LocalTime輸入參數以創建LocalDateTime實例。

package com.journaldev.java8.time;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZoneOffset;

public class LocalDateTimeExample {

	public static void main(String[] args) {
		
		//當前日期
		LocalDateTime today = LocalDateTime.now();
		System.out.println("Current DateTime="+today);
		
		//使用LocalDate和LocalTime獲取當前日期
		today = LocalDateTime.of(LocalDate.now(), LocalTime.now());
		System.out.println("Current DateTime="+today);
		
		//通過提供輸入參數創建LocalDateTime
		LocalDateTime specificDate = LocalDateTime.of(2014, Month.JANUARY, 1, 10, 10, 30);
		System.out.println("Specific Date="+specificDate);
		
		
		//嘗試通過提供無效輸入創建日期
		//LocalDateTime feb29_2014 = LocalDateTime.of(2014, Month.FEBRUARY, 28, 25,1,1);
		//在線程"main"中拋出java.time.DateTimeException: 
		//小時(有效值為0 - 23)的值無效: 25

		
		//"Asia/Kolkata"的當前日期,您可以在ZoneId javadoc中找到它
		LocalDateTime todayKolkata = LocalDateTime.now(ZoneId.of("Asia/Kolkata"));
		System.out.println("Current Date in IST="+todayKolkata);

		//java.time.zone.ZoneRulesException: 未知的時區ID: IST
		//LocalDateTime todayIST = LocalDateTime.now(ZoneId.of("IST"));
		
		//從基本日期即1970年1月1日獲取日期
		LocalDateTime dateFromBase = LocalDateTime.ofEpochSecond(10000, 0, ZoneOffset.UTC);
		System.out.println("10000th second time from 01/01/1970= "+dateFromBase);

	}

}

在這三個示例中,我們看到如果為創建日期/時間提供無效參數,則會拋出java.time.DateTimeException,這是一個RuntimeException,因此我們不需要顯式地捕獲它。

我們也發現通過傳遞 ZoneId,可以獲得日期/時間數據,您可以從其 JavaDoc 獲得支持的 ZoneId 值列表。當我們運行上面的類時,我們獲得以下輸出。

Current DateTime=2014-04-28T16:00:49.455
Current DateTime=2014-04-28T16:00:49.493
Specific Date=2014-01-01T10:10:30
Current Date in IST=2014-04-29T04:30:49.493
10000th second time from 01/01/1970= 1970-01-01T02:46:40

4. Instant

Instant 類用於處理機器可讀的時間格式。Instant 將日期時間存儲在 Unix 時間戳中。

package com.journaldev.java8.time;

import java.time.Duration;
import java.time.Instant;

public class InstantExample {

	public static void main(String[] args) {
		//當前時間戳
		Instant timestamp = Instant.now();
		System.out.println("Current Timestamp = "+timestamp);
		
		//來自時間戳的 Instant
		Instant specificTime = Instant.ofEpochMilli(timestamp.toEpochMilli());
		System.out.println("Specific Time = "+specificTime);
		
		//持續時間示例
		Duration thirtyDay = Duration.ofDays(30);
		System.out.println(thirtyDay);
	}

}

輸出:

Current Timestamp = 2014-04-28T23:20:08.489Z
Specific Time = 2014-04-28T23:20:08.489Z
PT720H

Java 8 日期 API 實用程序

大多數日期時間原則類提供各種實用方法,如增加/減少天數、周數、月份等。還有一些其他實用方法用於使用 TemporalAdjuster 調整日期,以及計算兩個日期之間的時間段。

package com.journaldev.java8.time;

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
import java.time.temporal.TemporalAdjusters;

public class DateAPIUtilities {

	public static void main(String[] args) {
		
		LocalDate today = LocalDate.now();
		
		//取得年份,檢查是否為閏年
		System.out.println("Year "+today.getYear()+" is Leap Year? "+today.isLeapYear());
		
		//比較兩個LocalDate以判斷前後
		System.out.println("Today is before 01/01/2015? "+today.isBefore(LocalDate.of(2015,1,1)));
		
		//從LocalDate創建LocalDateTime
		System.out.println("Current Time="+today.atTime(LocalTime.now()));
		
		//加減操作
		System.out.println("10 days after today will be "+today.plusDays(10));
		System.out.println("3 weeks after today will be "+today.plusWeeks(3));
		System.out.println("20 months after today will be "+today.plusMonths(20));

		System.out.println("10 days before today will be "+today.minusDays(10));
		System.out.println("3 weeks before today will be "+today.minusWeeks(3));
		System.out.println("20 months before today will be "+today.minusMonths(20));
		
		//Temporal adjusters用於調整日期
		System.out.println("First date of this month= "+today.with(TemporalAdjusters.firstDayOfMonth()));
		LocalDate lastDayOfYear = today.with(TemporalAdjusters.lastDayOfYear());
		System.out.println("Last date of this year= "+lastDayOfYear);
		
		Period period = today.until(lastDayOfYear);
		System.out.println("Period Format= "+period);
		System.out.println("Months remaining in the year= "+period.getMonths());		
	}
}

輸出:

Year 2014 is Leap Year? false
Today is before 01/01/2015? true
Current Time=2014-04-28T16:23:53.154
10 days after today will be 2014-05-08
3 weeks after today will be 2014-05-19
20 months after today will be 2015-12-28
10 days before today will be 2014-04-18
3 weeks before today will be 2014-04-07
20 months before today will be 2012-08-28
First date of this month= 2014-04-01
Last date of this year= 2014-12-31
Period Format= P8M3D
Months remaining in the year= 8

Java 8日期解析和格式化

將日期格式化為不同格式,然後解析字符串以獲得日期時間對象是非常常見的。

package com.journaldev.java8.time;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class DateParseFormatExample {

	public static void main(String[] args) {
		
		//格式化示例
		LocalDate date = LocalDate.now();
		//默認格式
		System.out.println("Default format of LocalDate="+date);
		//特定格式
		System.out.println(date.format(DateTimeFormatter.ofPattern("d::MMM::uuuu")));
		System.out.println(date.format(DateTimeFormatter.BASIC_ISO_DATE));
		
		
		LocalDateTime dateTime = LocalDateTime.now();
		//默認格式
		System.out.println("Default format of LocalDateTime="+dateTime);
		//特定格式
		System.out.println(dateTime.format(DateTimeFormatter.ofPattern("d::MMM::uuuu HH::mm::ss")));
		System.out.println(dateTime.format(DateTimeFormatter.BASIC_ISO_DATE));
		
		Instant timestamp = Instant.now();
		//默認格式
		System.out.println("Default format of Instant="+timestamp);
		
		//解析示例
		LocalDateTime dt = LocalDateTime.parse("27::Apr::2014 21::39::48",
				DateTimeFormatter.ofPattern("d::MMM::uuuu HH::mm::ss"));
		System.out.println("Default format after parsing = "+dt);
	}

}

輸出:

Default format of LocalDate=2014-04-28
28::Apr::2014
20140428
Default format of LocalDateTime=2014-04-28T16:25:49.341
28::Apr::2014 16::25::49
20140428
Default format of Instant=2014-04-28T23:25:49.342Z
Default format after parsing = 2014-04-27T21:39:48

Java日期API遺留日期時間支持

遺留日期/時間類別幾乎在所有應用程式中使用,因此具有向後相容性是必不可少的。這就是為什麼有幾個實用工具方法,通過這些方法我們可以將遺留類別轉換為新類別,反之亦然。

package com.journaldev.java8.time;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

public class DateAPILegacySupport {

	public static void main(String[] args) {
		
		//日期轉為Instant
		Instant timestamp = new Date().toInstant();
		//現在我們可以將Instant轉換為LocalDateTime或其他類似的類別
		LocalDateTime date = LocalDateTime.ofInstant(timestamp, 
						ZoneId.of(ZoneId.SHORT_IDS.get("PST")));
		System.out.println("Date = "+date);
		
		//日曆轉為Instant
		Instant time = Calendar.getInstance().toInstant();
		System.out.println(time);
		//時區轉為ZoneId
		ZoneId defaultZone = TimeZone.getDefault().toZoneId();
		System.out.println(defaultZone);
		
		//從特定日曆創建ZonedDateTime
		ZonedDateTime gregorianCalendarDateTime = new GregorianCalendar().toZonedDateTime();
		System.out.println(gregorianCalendarDateTime);
		
		//日期API轉為遺留類別
		Date dt = Date.from(Instant.now());
		System.out.println(dt);
		
		TimeZone tz = TimeZone.getTimeZone(defaultZone);
		System.out.println(tz);
		
		GregorianCalendar gc = GregorianCalendar.from(gregorianCalendarDateTime);
		System.out.println(gc);
		
	}

}

輸出:

Date = 2014-04-28T16:28:54.340
2014-04-28T23:28:54.395Z
America/Los_Angeles
2014-04-28T16:28:54.404-07:00[America/Los_Angeles]
Mon Apr 28 16:28:54 PDT 2014
sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]]
java.util.GregorianCalendar[time=1398727734404,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2014,MONTH=3,WEEK_OF_YEAR=18,WEEK_OF_MONTH=5,DAY_OF_MONTH=28,DAY_OF_YEAR=118,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=4,HOUR_OF_DAY=16,MINUTE=28,SECOND=54,MILLISECOND=404,ZONE_OFFSET=-28800000,DST_OFFSET=3600000]

正如您所見,遺留的TimeZoneGregorianCalendar類別的toString()方法過於冗長,並且不夠用戶友好。

結論

I like this new Date Time API a lot. Some of the most used classes will be LocalDate and LocalDateTime. It’s very easy to work with the new classes. And, having similar methods that does a particular job makes it easy to find. It will take some time from moving legacy classes to new Date Time classes, but I believe it will be worth the time and effort.

Source:
https://www.digitalocean.com/community/tutorials/java-8-date-localdate-localdatetime-instant