1. 程式人生 > 其它 >Java 日期類

Java 日期類

技術標籤:Javajava

Java 的 API 提供了很多有用的元件,能幫我們構建複雜的應用。比如日期處理,Java 從 1.0,就提供了 java.util.Date 類用於支援日期和時間的處理,不過由於該 API 設計的缺陷,產生了糟糕的易用性。隨著 1.0 退出舞臺,Date 類中的很多方法都被廢棄了,Java 1.1 使用 java.util.Calendar 類取而代之,很不幸,Calendar 類也有類似的問題和設計缺陷,導致使用這些方法寫出的程式碼非常容易出錯。

所有這些缺陷和不一致導致使用者們轉投第三方的日期和時間庫,比如 Joda-Time。為了解決這些問題,Oracle 決定在原生的 Java API 中提供高質量的日期和時間支援。你會看到 Java 8 在 java.time

包中整合了很多 Joda-Time 的特性。

在開始介紹日期類之前,我們先陳述幾個經常出現的名次:

UTC

協調世界時,是最主要的世界時間標準,基於原子鐘。

在 UTC 中,大約每一年或兩年會多出一秒,稱為“閏秒”,新增到一天的最後一秒,並且總是在 12 月 31 日或 6 月 30 日。

GMT

格林尼治標準時間,代表時區,標準時間為 UTC+0,是之前的民間標準,根據地球的自轉和公轉來計算時間,在歐洲國家和非洲國家用做當地標準時間。

GMT 因為是根據地球的轉動來計算時間的,而地球的自轉目前正在緩速變慢,所以 UTC 比 GMT 更加精準。但是一些計算機的標準是根據格林尼治標準時間(GMT)定義的,所以大多數計算機時鐘不夠精確。

在民用領域可以認為兩者相同。

CST

北美中部時區,代表時區,標準時間為 UTC-6。

這種縮寫的時區代表一片區域,但是由於沒有明確的標準,CST 概念來說在中國也代表中國標準時間。具體縮寫的定義可以檢視 ZoneId.SHORT_IDS 定義。

所以 java 提供了明確的時區表示方法,具體可用的時區字串 Id 可以通過 ZoneId.getAvailableZoneIds() 方法檢視。

ISO 8601

代表國際標準的日期和時間表示方法。

日期格式一般為:yyyy-MM-dd

帶時區的日期一般格式為:yyyy-MM-dd’T’HH:mm:ss.SSSZ

在 Java 中定義了一些參見的格式化表示方法,在

DateTimeFormatter 有具體的靜態格式化類表示。

UNIX 紀元(Epoch)

代表 1970 年 1 月 1 日 0 時 0 分 0 秒 GMT 午夜

Date

Date 類表示特定的時刻,精度為毫秒,該類反映的是協調世界時(UTC),不反映時區。

該類在使用時有幾點需要注意:

  • 該時刻表示的當前時間與紀元的差(以毫秒為單位);
  • 年代表與 1900 的差
  • 一個月由 0 到 11 之間的整數表示;0 是一月,1 是二月,以此類推;
  • 日期(一個月中的一天)由 1 到 31 之間的整數表示;
  • 小時用 0 到 23 之間的的整數表示;
  • 分鐘用 0 到 59 之間的整數表示;
  • 秒用 0 到 61 之間的整數表示;值 60 和 61 僅在閏秒出現;
  • 使用 SimpleDateFormat 格式化顯示,該類非執行緒安全。
  • 該類是可變類,執行緒不安全。

使用

建立

建立當前時刻:new Date(),相當於:new Date(System.currentTimeMillis()),使用紀元時間與當前時間毫秒差建立日期。

使用 Java8 Instant 轉換:Date.from(Instant instant),內部還是使用紀元時間差,其它日期物件轉換思想相同。

日期

Date 實現了 Comparable 介面,可以使用 compareTo 直接比較兩個物件;

使用 afterbefore 例項方法;

文字間轉換

使用 DateFormat 抽象類靜態方法轉換簡單的 ISO 格式:

該抽象類非執行緒安全,由於使用了成員變數 calendar 維護內部時間狀態,導致 format 等方法依賴狀態。

Date date = new Date();
DateFormat dateInstance = DateFormat.getDateInstance();
String format = dateInstance.format(date);
Date parse = dateInstance.parse(format);
System.out.println(parse + " DateInstance format:" + format);
// Sun May 31 00:00:00 CST 2020 DateInstance format:2020-5-31

DateFormat dateTimeInstance = DateFormat.getDateTimeInstance();
String format1 = dateTimeInstance.format(date);
Date parse1 = dateInstance.parse(format1);
System.out.println(parse1 + " DateTimeInstance format:" + format1);
// Sun May 31 00:00:00 CST 2020 DateTimeInstance format:2020-5-31 21:04:03

DateFormat instance = DateFormat.getInstance();
String format2 = instance.format(date);
Date parse2 = instance.parse(format2);
System.out.println(parse2 + " Instance format:" + format2);
// Sun May 31 21:04:00 CST 2020 Instance format:20-5-31 下午9:04

DateFormat timeInstance = DateFormat.getTimeInstance();
String format3 = timeInstance.format(date);
Date parse3 = timeInstance.parse(format3);
System.out.println(parse3 + " TimeInstance format:" + format3);
// Thu Jan 01 21:04:03 CST 1970 TimeInstance format:21:04:03

或使用 DateFormat 實現類 SimpleDateFormat 基於 Pattern 形式轉換:

該類非執行緒安全,原因和 DateFormat 相同。

字元日期或時間元素PresentationExamples
GEra 年代TextAD
yYear1996; 96
YWeek yearYear2009; 09
M一年中的月份 (上下文敏感)MonthJuly; Jul; 07
L一年中的月份 (獨立格式)MonthJuly; Jul; 07
w一年中的周Number27
W一月中的周Number2
D一年中的天Number189
d一月中的天Number10
F每月的星期幾Number2
EDay name in weekTextTuesday; Tue
u星期幾 (1 = 星期一, …, 7 = 星期天)Number1
aAm/pm 標記TextPM
H小時 (0-23)Number0
k小時 (1-24)Number24
K基於上午/下午表示的時間 am/pm (0-11)Number0
h基於上午/下午表示的時間 am/pm (1-12)Number12
m分鐘Number30
sNumber55
S毫秒Number978
z時區一般時區Pacific Standard Time; PST; GMT-08:00
Z時區RFC 822 時區-0800
X時區ISO 8601 時區-08; -0800; -08:00
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
String format = simpleDateFormat.format(new Date());
Date parse = simpleDateFormat.parse(format);

Calendar

該類是一個抽象類,和 Date 一樣,代表特定的時刻,該類提供了一些操作日曆欄位的方法,比如獲得下一週的日期。

該類使用時需要注意一下幾點:

  • 支援指定時區和區域構造日曆。最常見的實現是 GregorianCalendar,代表公曆;
  • 該時刻表示的當前時間與紀元的差(以毫秒為單位);
  • 年份不再從 1900 年開始,月份依舊從 0 開始計算;
  • 沒有專門的文字格式轉換,通過 Date 作為中轉
  • 該類是可變類,執行緒不安全。

使用

建立

  • 使用靜態工廠方法獲取基於系統時區的當前時刻:
Calendar rightNow = Calendar.getInstance();
  • 使用靜態內部類構建器構建時間日曆:
new Calendar.Builder()
        .setCalendarType("iso8601")
        .setWeekDate(2013, 1, MONDAY)
        .build();
  • DateCalendar
final Calendar calendar = new Calendar.Builder().setInstant(d).build();

比較

該類同樣實現了 Comparable 介面,可以使用 compareTo 直接比較兩個物件;

使用 afterbefore 例項方法;

修改欄位

通過 set(int field, int value) 修改當前日曆物件欄位,第一個引數為欄位型別,定義在類的靜態常量中:

ERA, YEAR, MONTH(JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER, UNDECIMBER), WEEK_OF_YEAR, WEEK_OF_MONTH, DATE, DAY_OF_MONTH, DAY_OF_YEAR, DAY_OF_WEEK(SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY), DAY_OF_WEEK_IN_MONTH, AM_PM, HOUR, HOUR_OF_DAY, MINUTE, SECOND, MILLISECOND, ZONE_OFFSET, DST_OFFSET

Calendar rightNow = Calendar.getInstance();
rightNow.set(Calendar.MONTH, Calendar.JANUARY);

或者 set(int year, int month, int date, int hourOfDay, int minute)set(int year, int month, int date) 等。

增加或降低欄位值:add(int field, int amount)

滾動增加或減小某個單位值,不影響前一個欄位值:roll(int field, boolean up)

// create a calendar
Calendar cal = Calendar.getInstance();

// displays the current calendar
System.out.println("Month is " + cal.get(Calendar.MONTH));
// Month is 4

// roll month
cal.roll(Calendar.MONTH, true);
System.out.println("Month is " + cal.get(Calendar.MONTH));
// Month is 5

// roll downwards
cal.roll(Calendar.MONTH, false);
System.out.println("Month is " + cal.get(Calendar.MONTH));
// Month is 4

new package java.time

該包中定義的類代表基本的日期時間概念,包括瞬間,持續時間,日期,時間,時區和時間段。它們基於 ISO 日曆系統(公曆)。所有的類都是不可變的並且是執行緒安全的

每個日期例項都是由方便獲取的欄位 API 組成。對於較低級別的欄位訪問,可以參考 java.time.temporal 包。每個類都支援列印和解析為各種日期和時間,具體參閱 java.time.format 包以獲取自定義選項。

java.time.chrono 包包含日曆中立 API ChronoLocalDateChronoLocalDateTimeChronoZonedDateTimeEra,這些類可以用來構建地區化的日曆系統,比如農曆等,系統內建提供了 4 中日曆系統,分別是:

  • ThaiBuddhistDate:泰國佛教歷
  • MinguoDate:中華民國曆
  • JapaneseDate:日本歷
  • HijrahDate:伊斯蘭曆

package java.time.temporal

我們可以使用欄位、單位和日期時間調節器來訪問日期和時間。

該程式包在基本程式包上擴充套件,以提供更多功能來滿足更強大的用例。支援包括:

  • 日期時間單位,例如年,月,日和小時
  • 日期時間欄位,例如一年中的某月,一週中的某日或一天中的某小時
  • 日期時間調整功能
  • 周的不同定義

欄位和單位

日期和時間以欄位和單位表示。單位用於測量時間量,例如年,天或分鐘。所有單位都實現 TemporalUnit。眾所周知的單位集在 ChronoUnit 中定義,例如 DAYS。該單位中也定義了一些常用的單位操作 API,例如:

欄位用於表示較大日期時間的一部分,例如年,一年中的月或一分鐘中的秒。所有欄位都實現 TemporalField。在 ChronoField 中定義了一組眾所周知的欄位,例如 HOUR_OF_DAY。其他欄位由 JulianFieldsWeekFieldsIsoFields 定義。該欄位也有一些常用的 API,例如:

欄位的一種用法是從時間物件中獲取沒有便利方法的欄位。例如,獲取月中的某天很常見,以至於 LocalDate 上有一個名為 getDayOfMonth() 的方法。但是,對於更特殊的欄位,必須使用該欄位。例如,獲取月中的週數,date.get(ChronoField.ALIGNED_WEEK_OF_MONTH)。

Temporal 作為支援欄位的日期時間型別的抽象。其方法支援獲取欄位的值、建立修改欄位值後的新日期時間以及查詢其他資訊(通常用於提取偏移量或時區)。

調整和查詢

日期時間問題中的關鍵部分是將日期調整為新的相關值,例如“每月的最後一天”或“下一個星期三”。這些被建模為調整基準日期時間的函式。該函式實現 TemporalAdjuster 並在 Temporal 上執行。TemporalAdjusters 中提供了一組常用功能。例如,要查詢給定日期後一週中某天的第一次出現,請使用 TemporalAdjusters.next(DayOfWeek),例如 date.with(next(MONDAY))。

TemporalAdjuster 函式介面有一些常見的靜態實現:

方法名描述
dayOfWeekInMonth建立一個新的日期,它的值為同一個月中每一週的第幾天
firstDayOfMonth建立一個新的日期,它的值為當月的第一天
firstDayOfNextMonth建立一個新的日期,它的值為下月的第一天
firstDayOfNextYear建立一個新的日期,它的值為明年的第一天
firstDayOfYear建立一個新的日期,它的值為當年的第一天
firstInMonth建立一個新的日期,它的值為同一個月中,第一個符合星期幾要求的值
lastDayOfMonth建立一個新的日期,它的值為當月的最後一天
lastDayOfNextMonth建立一個新的日期,它的值為下月的最後一天
lastDayOfNextYear建立一個新的日期,它的值為明年的最後一天
lastDayOfYear建立一個新的日期,它的值為今年的最後一天
lastInMonth建立一個新的日期,它的值為同一個月中,最後一個符合星期幾要求的值
next/previous建立一個新的日期,並將其值設定為日期調整後或者調整前,第一個符合指定星期幾要求的日期
nextOrSame/previousOrSame建立一個新的日期,並將其值設定為日期調整後或者調整前,第一個符合指定星期幾要求的日期,如果該日期已經符合要求,直接返回該物件

TemporalAmount 介面模擬相對時間量,比如常用的實現類 DurationPeriod

除了調整日期時間外,還提供了一個介面以啟用通過 TemporalQuery 進行查詢。查詢介面的最常見實現是方法引用。可以使用主要時間物件類上的 from(TemporalAccessor) 方法,例如 LocalDate::fromMonth::from。在 TemporalQueries 中作為靜態方法提供了進一步的實現。

final ZonedDateTime now = ZonedDateTime.now();

// Now: 2020-06-11T00:00:39.096326+08:00[Asia/Shanghai]
System.out.format("Now: %s%n", now);

final LocalDate localDate = now.query(TemporalQueries.localDate());
final LocalDate toLocalDate = now.toLocalDate();
final LocalDate fromLocalDate = now.query(LocalDate::from);

// true
System.out.println(localDate.isEqual(toLocalDate) && localDate.isEqual(fromLocalDate));

final TemporalUnit unit = now.query(TemporalQueries.precision());
final Chronology chronology = now.query(TemporalQueries.chronology());
final ZoneId zone = now.query(TemporalQueries.zoneId());
final ZoneOffset offset = now.query(TemporalQueries.offset());

// TemporalUnit: Nanos, Chronology: ISO, ZoneId: Asia/Shanghai, ZoneOffset: +08:00
System.out.format("TemporalUnit: %s, Chronology: %s, ZoneId: %s, ZoneOffset: %s %n", unit, chronology, zone, offset);

final Month month = now.query(Month::from);
final MonthDay monthDay = now.query(MonthDay::from);
final YearMonth yearMonth = now.query(YearMonth::from);
final DayOfWeek dayOfWeek = now.query(DayOfWeek::from);
final ZoneId zoneId = now.query(ZoneId::from);
final ZoneOffset zoneOffset = now.query(ZoneOffset::from);

// Month: JUNE, MonthDay: --06-11, YearMonth: 2020-06, DayOfWeek: THURSDAY, zoneId: Asia/Shanghai, zoneOffset: +08:00
System.out.format("Month: %s, MonthDay: %s, YearMonth: %s, DayOfWeek: %s, zoneId: %s, zoneOffset: %s %n", month, monthDay, yearMonth, dayOfWeek, zoneId, zoneOffset);

不同的語言環境對星期有不同的定義。例如,在歐洲,一週通常從星期一開始,而在美國,則從星期日開始。 WeekFields 類為這種區別建模。IOS 定義星期一為一週開始,一週至少需要四天。

ISO 日曆系統定義了一個額外的基於周的年劃分。這定義了基於從一年整個星期一到星期一的一年,它是在 IsoFields 中建模的。

日期和時間

日期和時間類的介面都提供了一致的設計,所以再很多 API 上都做了通用的處理,比如一致的方法字首:

  • of - 靜態工廠方法
  • parse - 側重於解析的靜態工廠方法
  • get - 獲取值或其它
  • is - 檢查是否為真
  • with - 等同於 setter 的不可變修改,返回新的副本
  • plus - 增加一定數量的單位
  • minus - 減少一定數量的單位
  • to - 將一個物件轉換為另一個物件
  • at - 將一個物件結合到該物件中,例如 date.atTime(time)

這裡主要以 Insant 展開介紹。

Instant

Instant 實質上是一個數字時間戳。可以從時鐘(Clock)中獲取當前的瞬時資訊。這對於時間點的記錄和持久化很有用,儲存的結果與 System.currentTimeMillis()` 相關聯。

建立

Date 獲取:new Date().toInstant()

使用靜態工廠方法:

  • 從系統時鐘(Clock)構建:Instant.now()Instant.now(Clock clock)
  • 根據紀元差毫秒構建構建:Instant.ofEpochMilli(long epochMilli)
  • 使用標準 ISO 格式字串構建:Instant.parse(CharSequence text),格式例如:2007-12-03T10:15:30.00Z
  • TemporalAccessor 實現類構建,該類可以為 Instant 或支援 INSTANT_SECONDSNANO_OF_SECOND 欄位的實現類,比如:ZonedDateTime

比較

使用前需明確該物件支援的單位或欄位型別,API 文件中有說明支援的型別;

或者使用:isSupported(TemporalUnit unit) 和 isSupported(TemporalUnit unit) 判斷

獲取

獲取時間的欄位值前同樣得確認是否支援,這些方法通常以 get* 單位的形式存在,或者直接通過:

獲取欄位的有效值範圍:range(TemporalField field)

調整

所有的修改方法都返回新的副本:

LocalDate

LocalDate 儲存沒有時間的日期,也不附帶時區資訊。它會儲存 yyyy-MM-dd 格式的日期,可以用來儲存生日。

LocalTime

LocalTime 儲存沒有日期的時間。它將儲存類似於 HH:mm:ss 格式的時間。

LocalDateTime

LocalDateTime 儲存日期和時間。它將儲存類似於 yyyy-MM-dd HH:mm:ss 格式的時間。

ZonedDateTime

ZonedDateTime 儲存帶有時區的日期和時間。如果要考慮到 ZoneId(例如“歐洲/巴黎”)對日期和時間進行準確的計算,這將很有用。如果可能,建議使用沒有時區的簡單類。時區的廣泛使用往往會增加應用程式的複雜性。

該類儲存類似於 yyyy-MM-dd HH:mm:ss.zzz 格式的時間。可通過上面所述時間類通過:atZone(ZoneId zone) 新增時區而來。

下圖對 ZoneDateTime 的組成部分進行了說明。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-z9iwQD2o-1612329693139)(…/…/images/java/zoneDateTime.jpg)]

持續時間和週期

Duration

持續時間是沿時間線以納秒為單位的時間的簡單度量。

此類以秒和納秒為單位對時間量進行建模。也可以使用其他基於持續時間的單位(例如分鐘和小時)來訪問它。

建立

目前為止, 我們看到的所有類都實現了 Temporal 介面, Temporal 介面定義瞭如何讀取和操縱為時間建模的物件的值。

Duration 類的靜態工廠方法 between 就可以為兩個 Temporal 物件建立持續時間。

Duration d1 = Duration.between(time1, time2); 
Duration d1 = Duration.between(dateTime1, dateTime2); 
Duration d2 = Duration.between(instant1, instant2);

也可以使用靜態工廠從其它時間單位建立:

Duration threeMinutes = Duration.ofMinutes(3); 
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);  

Period

週期以對人類有意義的單位表示時間量。例如年、月或天。

建立

使用靜態工廠方法建立:

Period tenDays = Period.between(LocalDate.of(2014, 3, 8), LocalDate.of(2014, 3, 18));

Period tenDays = Period.ofDays(10); 
Period threeWeeks = Period.ofWeeks(3); 
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

Duration 類和 Period 類共享了很多相似的方法,參見下表所示。

方 法 名是否是靜態方法方法描述
between建立兩個時間點之間的 interval
from由一個臨時時間點建立 interval
of由它的組成部分建立 interval 的例項
parse由字串建立 interval 的例項
addTo建立該 interval 的副本,並將其?加到某個指定的 temporal 物件
get讀取該 interval 的狀態
isNegative檢查該 interval 是否為負值,不包含零
isZero檢查該 interval 的時長是否為零
minus通過減去一定的時間建立該 interval 的副本
multipliedBy將 interval 的值乘以某個標量建立該 interval 的副本
negated以忽略某個時長的方式建立該 interval 的副本
plus以增加某個指定的時長的方式建立該 interval 的副本
subtractFrom從指定的 temporal 物件中減去該 interval

列印輸出及解析日期

處理日期和時間物件時,格式化以及解析日期-時間物件是另一個非常重要的功能。新的 java.time.format 包就是特別為這個目的而設計的。

這個包中,最重要的類是 DateTimeFormatter。建立格式器最簡單的方法是通過它的靜態工廠方法以及常量。

它包含了 IOS 格式定義的常見格式,像 BASIC_ISO_DATEISO_LOCAL_DATE 這樣的常量是 DateTimeFormatter 類的預定義例項。

所有的 DateTimeFormatter 例項都能用於以一定的格式建立代表特定日期或時間的字串。比如,下面的這個例子中,我們使用了兩個不同的格式器生成了字串:

LocalDate date = LocalDate.of(2014, 3, 18);  
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);	<-- 20140318
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);  <-- 2014-03-18

你也可以通過解析代表日期或時間的字串重新建立該日期物件。所有的日期和時間API 都提供了表示時間點或者時間段的工廠方法,你可以使用工廠方法parse達到重創該日期物件的目的:

LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE); 
LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);

和老的 java.util.DateFormat 相比較,所有的 DateTimeFormatter 例項都是執行緒安全的。所以,我們能夠以單例模式建立格式器例項,就像 DateTimeFormatter 所定義的那些常量,並能在多個執行緒間共享這些例項。DateTimeFormatter 類還支援一個靜態工廠方法,它可以按照某個特定的模式建立格式器,程式碼清單如下。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2014, 3, 18); 
String formattedDate = date1.format(formatter); 
LocalDate date2 = LocalDate.parse(formattedDate, formatter);