詳解 Java 日期與時間

日期和時間是計算機處理的重要數據,在絕大多數軟件程序中,我們都要和日期和時間打交道。本篇文章我們將系統地學習 Java 對日期和時間的處理。(在這裏特別感謝廖雪峯大佬的文章,傳送門:廖雪峯 Java 教程-日期和時間,本篇文章參考了其文章中的資料,事實上,筆者並不認爲本文比廖大佬的文章更好,有時間的讀者可以直接閱讀原教程。)

一、時區

地球人都知道,我們地球是自西向東自轉的,所以東邊會比西邊早看到太陽,東邊的時間也總比西邊的快。如果全球採用統一的時間,比如都用北京時間,會產生什麼問題呢?

當正午十二點的太陽照射到北京時,身處地球另一面的紐約還是漆黑一片。對於紐約來說,日常作息時間就成了晚上九點開始上班,因爲那時太陽剛剛升起;所有紐約人都上班到第二天早上六點下班,因爲那時太陽剛剛落下。

雖然對於長期居住在一個地方的人來說,他可以適應自己本地的作息時間,但當他去其他地方旅遊或是與其他地方的人交流時,就必須查詢當地的作息時間,這會帶來很大的不便。

image.png

於是,在 1879 年,加拿大鐵路工程師弗萊明首次提出全世界按統一標準劃分“時區”。1884 年華盛頓子午線國際會議正式通過採納這種時區劃分,稱爲世界標準時制度。

時區劃分的初衷是儘量使中午貼近太陽上中天的時間,從此以後,各地的時間經過換算,都能統一地早上六點起牀,中午十二點午餐,晚上六點下班。

image.png

全球共分爲 24 個時區,所以每個時區佔 15˚ 經度。理論時區以能被 15 整除的經線爲中心,向東西兩側延伸 7.5˚。國際規定經過英國格林威治天文臺的那一條經線爲 0˚ 經線,這條經線也被稱作本初子午線。選擇格林威治既是因爲當初“日不落帝國”的強大,也是由於格林威治常年提供準確的航海觀測數據,19 世紀晚期,72% 的世界貿易都依靠以格林威治作爲本初子午線的航海圖表。

爲了避開國界線,有的時區的形狀並不規則,而是比較大的國家以國家內部行政分界線爲時區界線,這是實際時區,也稱爲法定時區

1600px-World_Time_Zones_Map.png

身處地球的不同地區,時間可能是不同的,所以光靠時間我們無法確定一個時刻,要確定一個時刻必須要帶上時區。

image.png

表示時區有兩種常見的寫法,最常見的是 GMT,它的全稱是 Greenwich Mean Time,意思是格林威治標準時間,世界各地根據東西偏移量計算時區。比如,北京位於東八區,記做 GMT+8,紐約位於西五區,記做 GMT-5。

還有一種寫法是 UTC,它的全稱是 Coordinated Universal Time,意思是協調世界時,如果時間以 UTC 表示,則在時間後面直接加上一個“Z”(不加空格),“Z”是協調世界時中 0 時區的標誌。比如,“09:30 UTC” 寫作 “09:30Z” 或是 “0930Z”。“14:45:15 UTC” 寫作 “14:45:15Z” 或 “144515Z”。因爲在北約音標字母中用 “Zulu” 表示 “Z”,所以 UTC 時間也被稱做祖魯時間。

GMT 和 UTC 基本一樣,只不過 UTC 使用更加精確的原子鐘計時,每隔幾年會有一個閏秒。但我們無需關注兩者的差別,計算機在聯網時會自動與時間服務器同步時間。

計算不同時區的時間差很簡單,我們平時常用的北京時間位於東八區,即:GMT+8,它的值是在 GMT 的基礎上增加了 8 小時,紐約位於西五區,即:GMT-5,它的值是在 GMT 的基礎上減少了 5 小時。所以北京時間通常比紐約時間快 13 個小時。

我們現在知道,每往西越過一個時區,時間便提前一小時。據此我們來思考一個有趣的問題:如果我們一直往西,以每小時一個時區的速度前進,時間是否會靜止呢?

  • 1.比如我們從北京出發,此時時間是 2020-2-11 8:00 GMT+8
  • 2.當我們花費一個小時,走到東七區時,時間是 2020-2-11 8:00 GMT+7
  • 3.當我們走到本初子午線時,時間是 2020-2-11 8:00 GMT
  • 4.當我們走到西五區時,時間是 2020-2-11 8:00 GMT-5

我們都知道地球是個球體,當我們繞地球一圈回到北京時,如果時間還是 2020-2-11 8:00 GMT+8,豈不是時間真的靜止了?進一步思考,如果我們以半小時一個時區的速度向西前進,豈不是時間還會倒流?

常識告訴我們,時間是不可能靜止也不可能倒流的。那麼這裏的問題出在哪裏呢?問題就出在東西時區的交界處。上文說到,地球分爲 24 個時區,包括標準時區、東一區~東十二區、西一區~西十二區。實際上,東十二區和西十二區是同一時區。

從 0˚ 經線開始,每往西跨一個時區時間便減少 1 小時,每往東跨一個時區便增加 1 小時。如此一來,到了另一端 180˚ 經線時,就會有 24 小時的落差,爲了平衡這一落差,人們規定由西向東越過此線日期需減少一天,由東向西越過此線時日期需增加一天。故而這一條線被稱之爲國際日期變更線,也叫換日線,它位於本初子午線的另一面。和時區界限類似,爲了避開國界線,換日線並不與 180˚ 經線重合,換日線實際上也是不規則的。

時區-min.png

如果我們接着走下去:

  • 5.當我們走到東 / 西十二區時,時間是 2020-2-11 8:00 GMT±12
  • 6.我們越過國際換日線,日期增加一天,時間是 2020-2-12 8:00 GMT±12
  • 7.當我們走到東十一區時,時間是 2020-2-12 8:00 GMT+11
  • 8.當我們回到北京時,時間是 2020-2-12 8:00 GMT+8

此時,我們的環球之旅剛好用了 24 小時。

再來看一下如果我們以每半小時一個時區的速度向西行走,時間爲什麼不會逆流:

  • 1.我們還是從北京出發,此時時間是 2020-2-11 8:00 GMT+8
  • 2.當我們花費半小時,走到東七區時,時間是 2020-2-11 7:30 GMT+7
  • 3.當我們走到本初子午線時,時間是 2020-2-11 4:00 GMT
  • 4.當我們走到西五區時,時間是 2020-2-11 1:30 GMT-5
  • 5.當我們走到東 / 西十二區時,時間是 2020-2-10 22:00 GMT±12
  • 6.我們越過國際換日線,日期增加一天,時間是 2020-2-11 22:00 GMT±12
  • 7.當我們走到東十一區時,時間是 2020-2-11 21:30 GMT+11
  • 8.當我們回到北京時,時間是 2020-2-11 20:00 GMT+8

此時,我們的環球之旅剛好用了 12 小時。

二、夏令時

由於夏季和冬季白晝時間不一致,部分國家施行了夏令時制度,目的是讓人們根據白晝時間來調整作息。

夏令時:在夏天開始的時候,把時間往後撥 1 小時,夏天結束的時候,再把時間往前撥 1 小時。

施行夏令時使得人們可以儘量在白天工作,從而減少照明,節省電能。但夏令時也帶來了很多的不便,如夏令時開始和結束時,人們不得不調整睡眠時間;夏令時也使得時間計算變得複雜,在夏令時結束的當天,某些時間會出現兩次,容易造成交通、生產、會議安排等時間的混亂。中國曾經施行過一段時間夏令時,在 1992 年就被廢除了,而美國大部分地區現在還在使用夏令時。

美國使用夏令時時,紐約時間按照西四區計算,即:GMT-4。這段時間北京時間比紐約時間快 12 個小時,夏令時結束後,紐約時間又恢復到西五區 GMT-5。

由於各國規定有所差異,所以夏令時計算非常複雜。當我們需要計算夏令時時,應儘量使用 Java 庫提供的類,避免自己計算夏令時。

三、舊 API

Java 標準庫提供了兩套關於時間和日期的 API:

  • 舊 API:位於 java.util 包中,裏面主要有 Date、Calendar、TimeZone 類
  • 新 API:位於 java.time 包中,裏面主要有 LocalDateTime、ZonedDateTime、ZoneId 類

有兩套 API 的原因是舊 API 在設計時沒有考慮好時區問題,常量設計也有些不合理,導致使用起來不夠方便。新 API 很好的解決了這些問題。我們在開發時,除非維護老代碼,其他時候都應該儘量使用新 API。

3.1. Date

Date 類用於存儲日期和時間,查看其源碼可以發現,它保存了一個 long 類型的時間戳。時間戳是指格林威治時間從 1970 年 1 月 1 日零點到此刻經歷的秒數或毫秒數。

public class Date implements Serializable, Cloneable, Comparable<Date> {

    private transient long fastTime;

    ...
}

Date 的基本用法如下:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        // 獲取當前時間
        Date date = new Date();
        // 年份,需要加上 1900
        System.out.println(date.getYear() + 1900);
        // 月份,取值範圍是 0~11,代表 1~12 月,所以需要加上 1
        System.out.println(date.getMonth() + 1);
        // 日期,取值範圍是 1~31
        System.out.println(date.getDate());
        // 轉換爲 String,如:Tue Feb 11 17:24:10 CST 2020
        System.out.println(date.toString());
        // 轉換爲 GMT 時間,如:11 Feb 2020 09:24:10 GMT
        System.out.println(date.toGMTString());
        // 轉換爲本地化字符串,如:2020年2月11日 下午5:24:10
        System.out.println(date.toLocaleString());
    }
}

Date 在使用時有幾個缺點:

  • 每次獲取年份、月份都需要轉換
  • 只能獲取當前時區的時間,無法設置時區
  • 無法加減日期和時間
  • 無法計算某個月的第幾個星期幾

3.2. SimpleDateFormat

默認輸出的時間字符串的格式通常不能滿足我們的要求,所以我們需要用 SimpleDateFormat 來格式化輸出,它使用一些預定義的字符串表示格式化,較常用的字符串有:

  • y:年
  • M:月
  • d:日
  • H:小時
  • m:分鐘
  • s:秒
  • S:毫秒
  • a:上午 / 下午
  • E:星期
  • z:時區

附:Java 官網文檔中給出的預定義字符串表格

image.png

SimpleDateFormat 的使用:

import java.text.*;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Date date = new Date();
        var formatter = new SimpleDateFormat("y-M-d a H:m:s.S E z");
        // 輸出:2020-2-11 下午 17:26:13.776 週二 CST
        System.out.println(formatter.format(date));
    }
}

這裏的時區信息輸出爲 CST,表示 China Standard Time,也就是中國標準時間。

SimpleDateFormat 會根據預定義字符的長度打印不同長度的信息。以 M 爲例:

  • M:輸出 2
  • MM:輸出 02
  • MMM:輸出 2月
  • MMMM:輸出 二月

如果預定義字符串的長度短於需要輸出的信息,這時 Java 會輸出能包含全部信息的最短字符串,也就是說 Java 不會丟棄任何信息,如上例中只用了一個 y,仍然輸出了 2020,並不會只輸出一個 2。

我們來發揮一下極客精神,探索一下預定義字符串過長 Java 會怎麼處理:

import java.text.*;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Date date = new Date();
        var formatter = new SimpleDateFormat("yyyyyyyyyy-MMMMMMMMMM-dddddddddd aaaaaaaaaa HHHHHHHHHH:mmmmmmmmmm:ssssssssss.SSSSSSSSSS EEEEEEEEEE zzzzzzzzzz");
        // 輸出:0000002020-二月-0000000011 下午 0000000017:0000000027:0000000008.0000000504 星期二 中國標準時間
        System.out.println(formatter.format(date));
    }
}

本例中,每個預定義字符的長度都爲 10,可以看到,系統對年、日、時、分、秒、毫秒的處理是用前置 0 補足位數,對月份、上午 / 下午、星期、時區的處理是輸出全中文。

SimpleDateFormat 可以設置時區,我們可以用 SimpleDateFormat 把 Date 獲取的時間轉換爲其他時區顯示出來:

import java.text.*;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Date date = new Date();
        var formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzzzzz");
        // 輸出:2020-02-11 17:27:33 中國標準時間
        System.out.println(formatter.format(date));
        formatter.setTimeZone(TimeZone.getTimeZone("America/New_York"));
        // 輸出:2020-02-11 04:27:33 北美東部標準時間
        System.out.println(formatter.format(date));
    }
}

3.3. Calendar

舊 API 中,爲了加減日期和時間,Java 提供了 Calendar 類。

Calendar 的基本使用:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        // 獲取當前時間
        Calendar c = Calendar.getInstance();
        // 年份,不必加 1900
        int y = c.get(Calendar.YEAR);
        // 月份,取值範圍是 0~11,代表 1~12 月,所以需要加上 1
        int M = c.get(Calendar.MONTH) + 1;
        // 日期,取值範圍是 1~31
        int d = c.get(Calendar.DAY_OF_MONTH);
        int H = c.get(Calendar.HOUR_OF_DAY);
        int m = c.get(Calendar.MINUTE);
        int s = c.get(Calendar.SECOND);
        int S = c.get(Calendar.MILLISECOND);
        // 星期,取值範圍 1~7,代表週日~週六
        int E = c.get(Calendar.DAY_OF_WEEK);
        // 輸出:2020-2-11 17:28:19.364 3
        System.out.println(y + "-" + M + "-" + d + " " + H + ":" + m + ":" + s + "." + S + " " + E);
    }
}

Calendar 修復了 Date 獲取年份時必須 + 1900 的問題,但月份仍然使用 0~11 表示 1~12 月,星期採用 1~7 表示週日~週六。雖然咱們程序員都從 0 開始計數,但日期和時間一般都是要展示給用戶看的,每次顯示時都要轉換實在是太不方便了,這也是需要新 API 的原因之一。

Calendar 提供的日期和時間的加減功能使用如下:

import java.text.*;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Calendar c = Calendar.getInstance();
        var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 當前時間:2020-02-11 17:28:56
        System.out.println(sdf.format(c.getTime()));
        // 加 2 天
        c.add(Calendar.DAY_OF_MONTH, 2);
        // 減 5 小時
        c.add(Calendar.HOUR_OF_DAY, -5);
        // 計算後的時間:2020-02-13 12:28:56
        System.out.println(sdf.format(c.getTime()));
    }
}

使用日期加減時有一點需要特別注意,我們來看一個例子:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Calendar c = Calendar.getInstance();
        c.set(2020, 11, 31);
        // 輸出:Thu Dec 31 17:09:11 CST 2020
        System.out.println(c.getTime());
        c.add(Calendar.MONTH, -1);
        c.add(Calendar.MONTH, 1);
        // 輸出:Wed Dec 30 17:09:11 CST 2020
        System.out.println(c.getTime());
    }
}

我們將 12 月 31 日減去 1 個月,再加上 1 個月,日期變成了 12 月 30 日!這是因爲 11 月 沒有 31 日,所以 12 月 31 日減去 1 個月時, Calendar 會自動將日期調整到 11 月 30 日,再加 1 個月,就變成了 12 月 30 日。也就是說Calendar 加減時,會根據月份自動調整日期

上文介紹 Date 時我們說到,單靠 Date 和 SimpleDateFormat 只能把本地時區的時間用其他時區顯示出來,無法自由的實現時區的轉換,比如我們身在中國,無法把紐約時間 GMT-5 轉換爲東京時間 GMT+9。但 Calendar 是可以設置時區的,所以我們現在有了一種間接轉換任意時區的方法:

import java.text.*;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Calendar c = Calendar.getInstance();
        // 清空 Calendar 獲取到的本地時間
        c.clear();
        // 將時間重設爲:2020-2-11 13:00:00
        c.set(2020, 1, 11, 13, 0, 0);
        // 設定爲紐約時區
        c.setTimeZone(TimeZone.getTimeZone("America/New_York"));
        var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzzzzz");
        // c.getTime() 轉換成 Date 時,時間會轉換成本地時區
        Date d = c.getTime();
        // 輸出:2020-02-12 02:00:00 中國標準時間
        System.out.println(sdf.format(d));
        // 設置 SimpleDateFormat 的時區爲東京時區
        sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
        // 輸出紐約時間轉換成的東京時間
        // 輸出:2020-02-12 03:00:00 日本標準時間
        System.out.println(sdf.format(d));
    }
}

實際轉換過程爲:Calendar 保存的紐約時間先轉換成 Date 保存的北京時間,再用 SimpleDateFormat 將 Date 轉換成東京時間展示出來。

上例中還可以看到,Calendar 使用 set 方法設置指定時間,除了此例中的一次性全部指定的方式外,也可以單個指定:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Calendar c = Calendar.getInstance();
        // 輸出:Tue Feb 11 17:30:20 CST 2020
        System.out.println(c.getTime());
        c.set(Calendar.DAY_OF_MONTH, 9);
        // 輸出:Sun Feb 09 17:30:20 CST 2020
        System.out.println(c.getTime());
    }
}

四、新 API

由於舊 API 存在的諸多不便,從 Java 8 開始,java.time 包提供了一套新的日期和時間的 API。主要有 LocalDateTime、ZonedDateTime、Instant、ZoneId、Duration、DateTimeFormatter。

新 API 不僅使用更方便,而且修正了 舊 API 中不合理的常量設計:

  • 新 API 中,Month 取值範圍變成:1~12,表示 1~12月
  • 新 API 中,Week 取值範圍變成:1~7,表示週一~週日

4.1. LocalDateTime

LocalDateTime 用來代替 Date 和 Calendar,LocalDateTime 的基本用法如下:

import java.time.*;

public class Main {
    public static void main(String[] args) {
        // 當前日期和時間
        LocalDateTime dt = LocalDateTime.now();
        // 嚴格按照 ISO 8601 格式打印,輸出:2020-02-11T17:31:27.027983
        System.out.println(dt);
    }
}

LocalDateTime 使用 now() 函數獲取當前日期和時間,輸出時嚴格按照 ISO 8601 格式打印。ISO 8601 是國際標準化組織的日期和時間的表示方法,全稱爲《數據存儲和交換形式·信息交換·日期和時間的表示方法》,ISO 8601 規定使用 T 分隔日期和時間。標準格式如下:

  • 日期:yyyy-MM-dd
  • 時間:HH:mm:ss
  • 帶毫秒的時間:HH:mm:ss.SSS
  • 日期和時間:yyyy-MM-dd’T’HH:mm:ss
  • 帶毫秒的日期和時間:yyyy-MM-dd’T’HH:mm:ss.SSS

我們可以通過 parse() 函數解析一個符合 ISO 8601 格式的字符串,創建出 LocalDateTime:

LocalDateTime dt = LocalDateTime.parse("2020-02-11T13:00:00");

除此之外,我們還可以通過 of() 函數指定日期和時間創建 LocalDateTime:

import java.time.*;

public class Main {
    public static void main(String[] args) {
        // 當前日期和時間,最後的 1 表示納秒,可不傳
        LocalDateTime dt = LocalDateTime.of(2020, 2, 11, 13, 20, 30, 1);
        // 輸出:2020-02-11T13:20:30.000000001
        System.out.println(dt);
    }
}

LocalDateTime 存儲了當前的日期信息和時間信息,如果我們只需要當前日期或當前時間,可以使用 LocalDate 和 LocalTime:

import java.time.*;

public class Main {
    public static void main(String[] args) {
        // 當前日期和時間
        LocalDateTime dt = LocalDateTime.now();
        LocalDate d = dt.toLocalDate();
        LocalTime t = dt.toLocalTime();
        // 輸出:2020-02-11
        System.out.println(d);
        // 輸出:17:32:09.742114
        System.out.println(t);
    }
}

同 LocalDateTime 類一樣,LocalDate 和 LocalTime 類也可以通過 now()、parse()、of() 方法創建。

LocalDateTime 在加減日期時,可以採用簡潔的鏈式調用:

import java.time.*;

public class Main {
    public static void main(String[] args) {
        LocalDateTime dt = LocalDateTime.of(2020, 2, 11, 13, 0, 0);
        // 加 2 天,減 5 小時
        LocalDateTime dt2 = dt.plusDays(2).minusHours(5);
        // 輸出: 2020-02-13T08:00
        System.out.println(dt2);
    }
}

和 Calendar 一樣,LocalDateTime 在加減時,仍然會自動調整日期:

import java.time.*;

public class Main {
    public static void main(String[] args) {
        LocalDateTime dt = LocalDateTime.of(2020, 12, 31, 1, 0, 0);
        // 輸出:2020-12-31T01:00
        System.out.println(dt);
        LocalDateTime dt2 = dt.minusMonths(1).plusMonths(1);
        // 輸出:2020-12-30T01:00
        System.out.println(dt2);
    }
}

與 Calendar 不同的是,LocalDateTime 是不可變類,如此例中調用 minusMonths(1) 和 plusMonths(1) 方法後,dt 的值並沒有改變,這個函數返回的是一個調整後的新值,我們將這個新值賦值給了 dt2。

對應 Calendar 的 set() 方法,LocalDateTime 調整時間使用 withXxx() 方法:

  • 調整年:withYear()
  • 調整月:withMonth()
  • 調整日:withDayOfMonth()
  • 調整時:withHour()
  • 調整分:withMinute()
  • 調整秒:withSecond()
import java.time.LocalDateTime;

public class Main {
    public static void main(String[] args) {
        LocalDateTime dt = LocalDateTime.now();
        // 輸出:2020-02-11T17:34:17.570764
        System.out.println(dt);
        LocalDateTime dt2 = dt.withDayOfMonth(9);
        // 輸出:2020-02-09T17:34:17.570764
        System.out.println(dt2);
    }
}

LocalDateTime 還有一個 with() 方法允許我們做更復雜的運算:

import java.time.*;
import java.time.temporal.*;

public class Main {
    public static void main(String[] args) {
        // 今天 0:00
        LocalDateTime startOfToday = LocalDate.now().atStartOfDay();
        // 輸出:2020-02-11T00:00
        System.out.println(startOfToday);

        // 本月最後一天
        LocalDate lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
        // 輸出:2020-02-29
        System.out.println(lastDay);

        // 下個月第一天
        LocalDate firstDayOfNextMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth());
        // 輸出:2020-03-01
        System.out.println(firstDayOfNextMonth);

        // 本月第1個週一
        LocalDate firstWeekday = LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
        // 輸出:2020-02-03
        System.out.println(firstWeekday);
    }
}

要比較兩個日期的先後,可以使用 LocalDateTime 的 isBefore()、isAfter() 方法:

import java.time.*;

public class Main {
    public static void main(String[] args) {
        LocalDateTime dt = LocalDateTime.now();
        LocalDateTime dt2 = dt.plusDays(2);
        // 輸出: true, true
        System.out.println(dt.isBefore(dt2) + ", " + dt2.isAfter(dt));
    }
}

4.2. ZonedDateTime

LocalDateTime 和 Date 類一樣,總是表示本地時區的時間,如果要表示帶時區的時間,需要使用 ZonedDateTime,它相當於 LocalDateTime + ZoneId。LocalDateTime 提供的方法,如 now()、of()、plusDays() 等,ZonedDateTime 也都提供。

ZonedDateTime 的使用:

import java.time.*;

public class Main {
    public static void main(String[] args) {
        ZonedDateTime date = ZonedDateTime.now();
        ZonedDateTime dateEST = ZonedDateTime.now(ZoneId.of("America/New_York"));
        // 輸出: 2020-02-11T17:35:27.191452+08:00[Asia/Shanghai]
        System.out.println(date);
        // 輸出: 2020-02-11T04:35:27.193329-05:00[America/New_York]
        System.out.println(dateEST);
    }
}

ZonedDateTime 通過 now() 函數獲取當前時區的時間,通過 now(ZoneId zone) 函數獲取指定時區的時間。這樣獲取到的兩個時間雖然時區不同,但表示的都是同一時刻(毫秒數不同是由於執行代碼會花費一點時間)。

通過給 LocalDateTime 設置 ZoneId,也可以創建出 ZonedDateTime:

import java.time.*;

public class Main {
    public static void main(String[] args) {
        LocalDateTime dt = LocalDateTime.now();
        ZonedDateTime zdt = dt.atZone(ZoneId.systemDefault());
        ZonedDateTime zdt2 = dt.atZone(ZoneId.of("America/New_York"));
        // 輸出: 2020-02-11T17:35:50.246180+08:00[Asia/Shanghai]
        System.out.println(zdt);
        // 輸出: 2020-02-11T17:35:50.246180-05:00[America/New_York]
        System.out.println(zdt2);
    }
}

通過這種方式創建的 ZonedDateTime 日期和時間一樣,但時區不同,所以表示的是兩個不同時刻。

ZonedDateTime 可以通過 toLocalDateTime() 函數轉換成 LocalDateTime:

import java.time.*;

public class Main {
    public static void main(String[] args) {
        ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("America/New_York"));
        LocalDateTime ldt = zdt.toLocalDateTime();
        // 輸出: 2020-02-11T04:36:16.839591-05:00[America/New_York]
        System.out.println(zdt);
        // 輸出: 2020-02-11T04:36:16.839591
        System.out.println(ldt);
    }
}

我們看到,ZonedDateTime 轉換成 LocalDateTime 時,不會自動切換成本地時區的時間,而是直接丟棄時區信息。

由於 ZonedDateTime 自帶時區信息,所以在涉及時區轉換時使用 ZonedDateTime 非常方便。如上文中提到的將紐約時間轉換成的東京時間,使用 ZonedDateTime 實現如下:

import java.time.*;

public class Main {
    public static void main(String[] args) {
        ZonedDateTime zdt = ZonedDateTime.of(2020, 2, 11, 13, 0, 0, 0, ZoneId.of("America/New_York"));
        // 使用 withZoneSameInstant() 方法切換時區
        ZonedDateTime zdt2 = zdt.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
        // 輸出: 2020-02-11T13:00-05:00[America/New_York]
        System.out.println(zdt);
        // 輸出: 2020-02-12T03:00+09:00[Asia/Tokyo]
        System.out.println(zdt2);
    }
}

4.3. DateTimeFormatter

上文已經說到,DateTimeFormatter 是用來代替 SimpleDateFormat 的。與 SimpleDateFormat 相比,DateTimeFormatter 的一個明顯優勢在於它是線程安全的。SimpleDateFormat 由於不是線程安全的,使用時只能在方法內部創建新的局部變量,而 DateTimeFormatter 可以只創建一個實例。

DataTimeFormat 預定義的字符串和 SimpleDateFormat 一模一樣,來看下 DateTimeFormatter 的基本使用:

import java.time.*;
import java.time.format.*;

public class Main {
    public static void main(String[] args) {
        ZonedDateTime date = ZonedDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
        // 輸出: 2020-02-11 17:37
        System.out.println(formatter.format(date));
    }
}

還記得 LocalDateTime 的 parse() 方法嗎?我們查看一下它的源碼:

public static LocalDateTime parse(CharSequence text) {
    return parse(text, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}

public static LocalDateTime parse(CharSequence text, DateTimeFormatter 
formatter) {
    Objects.requireNonNull(formatter, "formatter");
    return formatter.parse(text, LocalDateTime::from);
}

從源碼中我們看到,parse() 方法可以傳入兩個參數,第二個參數就是一個 DateTimeFormatter,也就是說不僅 ISO 8601 標準格式的字符串可以被解析,我們完全可以自定義被解析的字符串格式。

import java.time.*;
import java.time.format.*;

public class Main {
    public static void main(String[] args) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
        // 自定義格式化:2020/02/11 17:37:23
        System.out.println(dtf.format(LocalDateTime.now()));

        LocalDateTime dt2 = LocalDateTime.parse("2020/02/11 13:00:00", dtf);
        // 用自定義格式解析:2020-02-11T13:00
        System.out.println(dt2);
    }
}

DataTimeFormatter 的 ofPattern() 方法還可以傳入一個 Locale 參數,這個參數的作用是使用當地的習慣來格式化時間:

import java.time.*;
import java.time.format.*;
import java.util.Locale;

public class Main {
    public static void main(String[] args) {
        ZonedDateTime zdt = ZonedDateTime.now();
        var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
        // 輸出:2020-02-11T17:37 GMT+08:00
        System.out.println(formatter.format(zdt));

        var zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
        // 輸出:2020 2月 11 週二 17:37
        System.out.println(zhFormatter.format(zdt));

        var usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
        // 輸出:Tue, February/11/2020 17:37
        System.out.println(usFormatter.format(zdt));
    }
}

4.4. Instant

在新 API 中,使用 Instant 表示時間戳,它類似於 System.currentTimeMillis()。Instant 使用如下:

import java.time.*;

public class Main {
    public static void main(String[] args) {
        Instant now = Instant.now();
        // UTC 標準時間,輸出:2020-02-11T09:38:13.891708Z
        System.out.println(now);
        // 以秒爲單位的時間戳, 輸出:1581413893
        System.out.println(now.getEpochSecond());
        // 以毫秒爲單位的時間戳,輸出:1581413893891
        System.out.println(now.toEpochMilli());
    }
}

給 Instant 加上一個時區,就可以創建出 ZonedDateTime:

import java.time.*;

public class Main {
    public static void main(String[] args) {
        Instant now = Instant.ofEpochSecond(1581413893);
        ZonedDateTime zdt = now.atZone(ZoneId.systemDefault());
        // GMT 標準時間,輸出:2020-02-11T17:38:13+08:00[Asia/Shanghai]
        System.out.println(zdt);
    }
}

五、新舊 API 的轉換

舊 API 轉新 API 可以通過 toInstant() 方法轉換爲 Instant,再由 Instant 轉換成 ZonedDateTime:

// Date -> Instant:
Instant ins1 = new Date().toInstant();

// Calendar -> Instant -> ZonedDateTime:
Calendar calendar = Calendar.getInstance();
Instant ins2 = Calendar.getInstance().toInstant();
ZonedDateTime zdt = ins2.atZone(calendar.getTimeZone().toZoneId());

新 API 轉舊 API 時,需要藉助 long 類型時間戳實現:

// ZonedDateTime -> long:
ZonedDateTime zdt = ZonedDateTime.now();
long ts = zdt.toEpochSecond() * 1000;

// long -> Date:
Date date = new Date(ts);

// long -> Calendar:
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.setTimeZone(TimeZone.getTimeZone(zdt.getZone().getId()));
calendar.setTimeInMillis(zdt.toEpochSecond() * 1000);

以上,就是 Java 日期和時間的全部內容了,有什麼收穫或疑問歡迎在留言區一起交流。

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