Java 日期、時間 API 總結

日期、時間的相關抽象

Java 8 對時間、日期進行了新的抽象,構建了一系列的日期、時間對象用作相關的時間操作。

LocalDate , LocalTime 和 LocalDateTime

LocalDate 是對日期的抽象,即年月日。LocalTime 是對時間的抽象,即時分秒。而 LocalDateTime 即是 LocalDate 以及 LocalTime 的結合,表示日期與時間。
以上三者都是在開發時常用到的三種時間表示方式。三者的實例都是不可變對象,每次時間的改變,都會新創建一個對應的對象,所以它們是線程安全的。
下面將較爲詳細的介紹三者。

LocalDate

LocalDate 表示日期,獲取一個 LocalDate 對象有如下方式:

// 創建表示當前日期的 LocalDate 對象
LocalDate nowDate = LocalDate.now();
// 創建指定日期的 LocalDate 對象
LocalDate specificDate = LocalDate.of(2018, 8, 8);
// 獲取最小日期,通常用來表示很久很久以前
LocalDate minDate = LocalDate.MIN;
// 獲取獲取最大日期,通常用來表示很遠很遠的未來
LocalDate maxDate = LocalDate.MAX;

除此之外,還有一種解析字符串創建 LocalDate 的方式,這也是在實際開發中經常用到的。在默認情況下使用 ISO 8601 標準。有以下用例:

LocalDate specificDate = LocalDate.parse("2018-08-08");

獲取到實例後,有各種實用的、可讀性強的 API 供我們讀取相關的時間信息。有以下用例:

LocalDate date = LocalDate.of(2018, 8, 8);
// 獲取年份
int year = date.getYear();
// 獲取月份對象,可以使用 getMonthValue 方法獲取月份值,範圍爲 1-12
Month month = date.getMonth();
// 獲取日期
int day = date.getDayOfMonth();
// 獲取周對象
DayOfWeek dow = date.getDayOfWeek();
// 獲取本日期的月份長度
int len = date.lengthOfMonth();
// 判斷本日期是否爲閏年
boolean leap = date.isLeapYear();

除此之外,LocalDate 還有一個 get 方法,可以接受一個 TemporalField 參數拿到指定的信息。該接口定義瞭如何獲取各個日期、時間對象某個字段的值,枚舉類 ChronoField 實現了該接口,可以很方便的通過傳遞 ChronoField 枚舉類的實例來獲取指定信息。有以下用例:

int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);

要注意的是,不同的日期、時間對象可以接受的 TemporalField 參數是不同的,如果無法接受傳遞 TemporalField 的參數將會拋出異常 UnsupportedTemporalTypeException 。

除了獲取信息外,修改日期、時間對象也是常見的需求,例如,要獲取當前日期 3 天后的日期。可以利用日期、時間對象的 plus 方法便捷地完成該需求:

LocalDate date = LocalDate.now().plus(3, ChronoUnit.DAYS);

前面提到,LocalDate 是不可變對象,所以 plus 方法返回的是一個新的 LocalDate 對象。如果想獲取的是三天前的日期,傳遞一個負數即可。

LocalTime

LocalTime 表示時間(時分秒),創建一個 LocalTime 實例有如下方式:

// 獲取當前時間
LocalTime nowTime = LocalTime.now();
// 獲取指定時間,12時30分30秒
LocalTime specificTime = LocalTime.of(12, 30, 30);
// 獲取當天最小時間,即 00時00分
LocalTime minTime = LocalTime.MIN;
// 獲取當天最大時間,即 23時59分59.999999999秒
LocalTime maxTime = LocalTime.MAX;
// 半夜十二點,與 LocalTime.MIN 一樣的值
LocalTime midnightTime = LocalTime.MIDNIGHT;
// 中午十二點
LocalTime noonTime = LocalTime.NOON;
// 解析字符串獲取時間對象,以 ISO 8601 標準爲解析格式
LocalTime strSpecificTime = LocalTime.parse("12:30:30");

有以下獲取 LocalTime 對象信息的方式:

int hour = time.getHour();
int minute = time.getMinute(); 
int second = time.getSecond();
int minuteOfDay = time.get(ChronoField.MINUTE_OF_DAY);

同樣也可以使用 plus 方法修改對象,當然返回的將會是一個新的 LocalTime 實例。

// 獲取當前時間 2 分鐘後的時間
LocalTime time = LocalTime.now().plus(2, ChronoUnit.MINUTES);

LocalDateTime

LocalDateTime 表示時間日期,是 LocalDate 與 LocalTime 的結合,LocalDate 或是 LocalTime 有的特性 LocalDateTime 基本上都有。
可以通過以下方式獲取 LocalDateTime 的實例:

// 獲取當前時間和日期
LocalDateTime nowDateTime = LocalDateTime.now();
// 獲取指定的時間日期,如 2018-08-12T12:30:30
LocalDateTime specificDateTime = LocalDateTime.of(2018, Month.AUGUST, 12, 12, 30, 30);
// 解析字符串獲取日期時間
LocalDateTime strSpecificDateTime = LocalDateTime.parse("2018-08-12T12:30:30");
// 最小日期時間,通常用來表示很久很久以前
LocalDateTime minDateTime = LocalDateTime.MIN;
// 最大日期時間,通常用來表示很遠很遠的未來
LocalDateTime maxDateTime = LocalDateTime.MAX;

// 通過 LocalDate 實例以及 LocalTime 實例獲取 LocalDateTime 實例
LocalDate date = LocalDate.of(2018, 8, 8);
LocalTime time = LocalTime.of(12, 30, 30);

LocalDateTime dt = LocalDateTime.of(date, time);
LocalDateTime dt1 = date.atTime(time);
LocalDateTime dt2 = date.atTime(12, 30, 30);
LocalDateTime dt3 = time.atDate(date);

能夠通過 LocalDate 實例以及 LocalTime 實例獲取 LocalDateTime 實例,自然也能通過 LocalDateTime 對象獲取到 LocalDate ,LocalTime 對象。

LocalDate date = dateTime.toLocalDate();
LocalTime time = dateTime.toLocalTime();

通過 LocalDate 和 LocalTime 獲取的信息,基本上也可以通過 LocalDateTime 獲取到。有以下簡單的用例:

int year = dateTime.getYear();
int minute = dateTime.getMinute();
int dayOfHours = dateTime.get(ChronoField.HOUR_OF_DAY);

最後對於 LocalDateTime 對象的修改,同樣,實際上沒有修改 LocalDateTime 對象,而是返回一個新的對象。

LocalDateTime.now().plus(-1, ChronoUnit.DAYS);
LocalDateTime.now().plus(3, ChronoUnit.HOURS);

Instant

前面提到的日期時間類如 LocalDate ,對人類是友好的,因爲它們以我們所屬性的方式表現時間。但是對於機器來說,多少年,幾號,周幾這樣的算法是不友好的,而像 UNIX 時間戳這種用數值來表示當前時間點的方式對機器跟爲友好。
Instant 類是對時間線上的某個時間點的抽象,簡單點說就是對時間戳的抽象。它也是一個不可變對象,當嘗試去修改一個 Instant 實例時,將會得到一個新的 Instant 實例。
可以通過以下方式獲取 Instant 的實例:

// 獲取當前時間
Instant nowInstant = Instant.now();
// 通過時間戳數值獲取時間,單位爲秒
Instant specificInstantBySecond = Instant.ofEpochSecond(1540182630L);
// 在 1540182630 的基礎上加上一百萬納秒,即 1 秒
Instant specificInstantBySecondAndNano = Instant.ofEpochSecond(1540182630L, 1_000_000_000);
// 通過時間戳數值獲取時間,單位爲毫秒
Instant specificInstantByMilli = Instant.ofEpochMilli(1540182630 * 1000L);
// 通過解析字符串獲取時間,默認解析標準爲 ISO 8601 。
Instant specificInstantByStr = Instant.parse("2018-10-22T12:30:00.000Z");

通過 Instant 對象,可以獲取到的是時間戳數值。

long timeStampSecond = Instant.now().getEpochSecond();
long timeStampMill = Instant.now().get(ChronoField.MILLI_OF_SECOND);

對於 Instant 的修改,主要也是用到相關的 plus 方法。

Instant instant1 = instant.plusSeconds(1L);
Instant instant2 = instant.plusMillis(1000L);
Instant instant3 = instant.plus(1L, ChronoUnit.SECONDS);

Duration 和 Period

上面提到的相關時間類表示的是時間點,而 Duration 和 Period 是對時間段的抽象。
Duration 與 Period 的區別在於 Duration 主要是以整數值(秒和納秒)來表示時間段,而 Period 則是以對人類友好的方式來表示時間段,即一週,一個月,一年等。

Duration 和 Period 可以用一系列 of 開頭的靜態方法來創建實例。有以下簡單的例子:

// 十分鐘
Duration tenMin = Duration.ofMinutes(10);
// 兩週
Period twoWeek = Period.ofWeeks(2);

然後可以通過這兩個對象獲取一些簡單的信息:

Duration tenMin = Duration.ofMinutes(10);
long seconds = tenMin.getSeconds();

Period twoWeek = Period.ofWeeks(2);
int days = twoWeek.getDays();

Duration 和 Period 的一個最常用功能就是它們的靜態方法 between ,該方法可以計算兩個時間實例之間的時間差。

LocalDateTime nowDateTime = LocalDateTime.now();
LocalDateTime weekAgoDateTime = nowDateTime.plus(-2, ChronoUnit.WEEKS);
Duration duration = Duration.between(weekAgoDateTime, nowDateTime);
System.out.println(duration.getSeconds()); // 1209600

LocalDate nowDate = LocalDate.now();
LocalDate weekAgoDate = nowDate.plus(-2, ChronoUnit.WEEKS);
Period period = Period.between(weekAgoDate, nowDate);
System.out.println(period.getDays()); // 14

注意的是,Duration 的 between 方法只能用於 Instant ,LocalTime 和 LocalDateTime 等對象的時間差計算。而 Period 的 between 方法只能用於 LocalDate 的時間差計算。而且不同的時間類型對象是不可以混用的,如試圖計算 LocalDateTime 對象和 Instant 對象之間的時間差。

Duration 對象還有一個常見的用處,那就是作爲參數傳給各個日期、時間對象的 plus 方法。
上面提到的各個日期、時間對象的 plus 方法其實在內部都是利用了一個 Duration 對象來計算日期、時間對象修改後的值,當然也可以顯示傳遞一個 Duration 對象。

LocalTime nowTime = LocalTime.now();
LocalTime afterTenMin = nowTime.plus(Duration.ofMinutes(10));

日期、時間對象的修改

首先要明確日期、時間對象都是不可變對象,所以“修改”實際上是創建了一個新的對象。
對於日期、時間對象的修改,最常用的就是它們的 plus 系列方法,在前面關於各個日期、時間對象的介紹中也有提到。除此之外,日期、時間對象還有一系列的 with 方法,可以進行相關的時間修改操作。這裏以 LocalDate 爲例:

// 初始日期爲 2018-08-08
LocalDate date = LocalDate.of(2018, 8, 8);
// 將年份修改爲 2017 年,日期變爲 2017-08-08
LocalDate dateModifyYear = date.withYear(2017);
// 將月份修改爲 9 月,日期變爲 2018-09-08
LocalDate dateModifyMonth = date.withMonth(9);
// 將日期修改爲 15 號,日期變爲 2018-08-15
LocalDate dateModifyDay = date.with(ChronoField.DAY_OF_MONTH, 15);

如果需要更加複雜的修改操作,就需要用到接口 TemporalAdjuster 。

@FunctionalInterface
public interface TemporalAdjuster {
    /**
     * 設置日期變化的邏輯
     * @param temporal 輸入的時間對象,即需要進行修改的時間對象
     * @return 輸出的時間杜希昂,即修改後的時間對象
     */
    Temporal adjustInto(Temporal temporal);
}

該接口聲明爲函數式接口,即可以以函數式編程的方式使用。
各個日期、時間類都有一個 with 方法,該 with 方法接受一個參數,即 TemporalAdjuster 。修改日期或事件的邏輯就在於接口 TemporalAdjuster 方法 adjustInto 的具體實現。

在 Java 8 的時間 API 中,有不少類都實現了該接口,可以利用這些類完成一些時間修改的操作,而且有工廠類 TemporalAdjusters 實現了不少常用到的時間修改操作。

// 初始日期爲 2018-08-08
LocalDate date = LocalDate.of(2018, 8, 8);
// 將日期修改爲本週五的日期
LocalDate dateModifyDayOfWeek = date.with(DayOfWeek.FRIDAY);
// 將日期修改爲月初
LocalDate dateModifyFirstDayOfMonth = date.with(TemporalAdjusters.firstDayOfMonth());

當然除此之外還可以自己實現 TemporalAdjuster 接口,實現自定義的日期、時間修改邏輯。

字符串與時間的轉換

在開發中經常會遇到字符串與日期、時間對象的相互轉換的需求,在 Java 8 中,新添加了一個時間格式化類 DateTimeFormatter 專門用於字符串與日期時間的相互轉換。

DateTimeFormatter 自身定義了許多靜態實例,代表了各種常用的日期時間表示格式,如下:

Formatter Description Example
BASIC_ISO_DATE Basic ISO date '20111203'
ISO_LOCAL_DATE ISO Local Date '2011-12-03'
ISO_OFFSET_DATE ISO Date with offset '2011-12-03+01:00'
ISO_DATE ISO Date with or without offset '2011-12-03+01:00'; '2011-12-03'
ISO_LOCAL_TIME Time without offset '10:15:30'
ISO_OFFSET_TIME Time with offset '10:15:30+01:00'
ISO_TIME Time with or without offset '10:15:30+01:00'; '10:15:30'
ISO_LOCAL_DATE_TIME ISO Local Date and Time '2011-12-03T10:15:30'
ISO_OFFSET_DATE_TIME Date Time with Offset 2011-12-03T10:15:30+01:00'
ISO_ZONED_DATE_TIME Zoned Date Time '2011-12-03T10:15:30+01:00[Europe/Paris]'
ISO_DATE_TIME Date and time with ZoneId '2011-12-03T10:15:30+01:00[Europe/Paris]'
ISO_ORDINAL_DATE Year and day of year '2012-337'
ISO_WEEK_DATE Year and Week 2012-W48-6'
ISO_INSTANT Date and Time of an Instant '2011-12-03T10:15:30Z'
RFC_1123_DATE_TIME RFC 1123 / RFC 822 'Tue, 3 Jun 2008 11:05:30 GMT'

字符串轉換爲日期、時間

在前面可以看到,LocalDate , LocalTime 以及 LocalDate 都可以利用靜態方法 parse 將字符串轉換爲對應的日期、時間對象。而這個 parse 方法就是字符串轉換爲時間的方法。該方法還可以傳遞一個 DateTimeFormatter 對象作爲解析格式,默認情況下使用的就是 DateTimeFormatter.ISO_LOCAL_DATE 、DateTimeFormatter.ISO_LOCAL_TIME , DateTimeFormatter.ISO_LOCAL_DATE_TIME 這三個表示 ISO 8601 標準的對象。

除了可以傳遞上述提到的靜態對象,還可以自定義格式,然後進行字符串的解析,如下:

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
LocalDate date = LocalDate.parse("2018/08/08", dateTimeFormatter);

日期、時間轉換爲字符串

日期、時間轉換爲字符串主要是利用 DateTimeFormatter 的 format 方法。有如下示例:

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
String dateTimeStr = dateTimeFormatter.format(LocalDateTime.now());

除了常規的解析外,還可以進行國際化。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM uuuu", Locale.SIMPLIFIED_CHINESE);
String dateStr = formatter.format(LocalDate.now());
System.out.println(dateStR); // 十月 2018

更多預定義的格式

除了上面提到定義好的靜態實例外,DateTimeFormatter 有三個靜態方法 ofLocalizedDate ,ofLocalizedTime 和 ofLocalizedDateTime ,接受參數 FormatStyle 對象。FormatStyle 是一個枚舉類,它有以下定義:

public enum FormatStyle {
    /**
     * Full text style, with the most detail.
     * For example, the format might be 'Tuesday, April 12, 1952 AD' or '3:30:42pm PST'.
     */
    FULL,
    /**
     * Long text style, with lots of detail.
     * For example, the format might be 'January 12, 1952'.
     */
    LONG,
    /**
     * Medium text style, with some detail.
     * For example, the format might be 'Jan 12, 1952'.
     */
    MEDIUM,
    /**
     * Short text style, typically numeric.
     * For example, the format might be '12.13.52' or '3:30pm'.
     */
    SHORT;
}

根據傳遞的 FormatStyle 不同,獲取到的 DateTimeFormatter 也不同,即解析的格式也不同,而且得到的 DateTimeFormatter 是進行了本地化的,有如下例子:

String dateStr = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).format(LocalDate.now());
System.out.println(dateStr); // 2018年10月22日 星期一

三個靜態方法中,ofLocalizedDateTime 稍微特別一點,因爲它可以接受兩個 FormatStyle 對象作爲參數,分別對應於解析日期和時間。

String dateStr = DateTimeFormatter
        .ofLocalizedDate(FormatStyle.FULL)
        .format(LocalDate.now());
System.out.println(dateStr); // 2018年10月22日 星期一

如果不想本地化,可以利用方法 withLocal 方法改變語言。

String dateStr = DateTimeFormatter
        .ofLocalizedDate(FormatStyle.FULL)
        .withLocale(Locale.US)
        .format(LocalDate.now());
System.out.println(dateStr); // Monday, October 22, 2018

時區

一個完整的時間表達是包含時區的,同一個時間點,對於不同時區的人來說時間的表達是不一樣的,不同的時區,在日期和時間的計算上有不同的標準。
LocalDate 、LocalTime 和 LocalDateTime 是一種主觀上的時間表示方式,對於人來說,同一個 LocalDate 實例所表達的意義是相同的,但是在時間線上,不同的時區在不同的時間點上。

對於時區信息的問題,可以使用類 ZoneId ,它由一個地區 ID 來進行標識,利用地區 ID 來獲取 ZoneId 的實例,而且是不可變的,地區 ID 爲“{區域}/{城市}”的格式。獲取一個 ZoneId 實例:

ZoneId eastEightArea = ZoneId.of("Asia/Shanghai");

還有一種常用的方式是利用它的靜態方法 systemDefault 來獲取本地機器的時區信息:

ZoneId systemZoneId = ZoneId.systemDefault();

對於時區信息的獲取,還可以利用UTC/格林尼治時間的固定偏差計算時區。

// 獲取東八區時區的兩種方式
ZoneOffset eastNightAreaZoneOffset = ZoneOffset.of("+8");
ZoneId eastNightAreaZoneId = ZoneId.of("UTC+8");

ZoneOffset 是對時區偏差的抽象,是 ZoneId 的一個子類。如果 ZoneId 不使用“{區域}/{城市}”的地區 ID 格式,內部將會使用偏差來計算時區。

獲取到時區後,可以在相關的日期、時間對象上添加時區信息,構造成一個具有時區信息的日期、時間對象 ZonedDateTime。ZonedDateTime 也可以轉換爲其他日期、時間對象。

ZoneId eastEightArea = ZoneId.of("Asia/Shanghai");

// LocalDate 與 ZonedDateTime 的相互轉換
LocalDate date = LocalDate.now();
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
date = zdt1.toLocalDate();

// LocalDateTime 與 ZonedDateTime 的相互轉換
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
dateTime = zdt2.toLocalDateTime();

// Instant 與 ZonedDateTime 的相互轉換
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
instant = zdt3.toInstant();

日曆系統

ISO-8601 日曆系統是世界文明日曆系統的事實標準,也是 Java 8 時間 API 體系中默認使用的日曆系統。除此之外,還有其他的日曆系統,如我們常用的農曆(可惜的是 Java 8 沒提供),不同的日曆系統,對時間的表示有所不同。
接口 Chronology 抽象了日曆,相關的日曆系統都實現了該接口。對此感興趣的可以參考其 API 文檔。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章