Java語法--日期和時間的API
The Date and Time API
《Java Core》ed.11 讀書筆記
Java1.0有了Date
類(native方式)來處理時間相關
Java1.1有了Calendar
類(不完美,例項是可變的(mutable),無法處理潤秒(leap second)),Date
類中的大部分方法在這個版本過時
Java8的java.time
包解決了之前處理時間的類的缺點
時間線
秒的定義是源自地球自轉一圈(606024=86400s),因為地球自傳有輕微抖動(wobble),所以秒的精確定義根據銫-133原子的屬性(1967)。此後,原子鐘網路在保持官方時間。1972年開始,”潤秒“偶爾出現(出現修改時間系統的討論)。大多數計算機系統使用”更平滑“的方式來保持每天86400s。這是因為計算機的時間並不精確,且它是從外部時間服務來同步時間
Java的Date和Time API規定Java使用時間精度需要:
- 一天86400s
- 每天中午(noon)要匹配官方時間
- 在一個精度控制下,儘量在其它時間匹配官方時間
呼叫靜態方法Instant.now()
來獲取當前時刻,通過equals
compareTo
可以比較兩個instant,可以把instant當作時間戳來用
Instant start = Instant.now(); runAlgorithm(); Instant end = Instant.now(); // 獲取兩個instant的間隔 // Duration是兩個instant之間度過多少時間,可以通過呼叫toNanos, toMillis, toSeconds(Java8改為getSeconds), toMinutes, toHours, or toDays來獲取不同時間單位 Duration timeElapsed = Duration.between(start, end); long millis = timeElapsed.toMillis();
如果需要納秒精度,那麼要注意溢位問題。一個long值可以儲存300年的納秒,如果duration中的值小於這個數,可以直接轉換
Duration
Instant
都是immutable的,它們的方法都是返回一個新的物件
// 得到一個duration物件的不同部分,100s -> 1分40秒
int to(Nanos|Millis|Seconds|Minutes|Hours)Part() 9
long to(Days|Hours|Minutes|Seconds|Millis|Nanos)Part() 9
Local Date
在Java API中人類的時間有兩種
- local date/time
- zoned time
local date/time擁有一天的日期資訊和時間資訊,但是沒有時區相關的資訊。例如一個日期June 14, 1903,它沒有一天中的時間點也沒有時區資訊,所以它沒有相關的instant時間點。July 16, 1969, 09:32:00 EDT是一個zoned date/time,在時間線中代表一個準確時間
有很多計算是不需要帶時區的時間的,有時甚至帶時區時間會有負面影響(如果每週一10:00要開週會,那麼下一次的時間是(t+60*60*24*7),如果碰巧度過了夏令時(白天長的夏天會把時鐘調快1小時),那麼實際的開會時間可能會早1個小時
因此,除非必須要處理時區,否則使用local date/time即可
LocalDate today = LocalDate.now(); // Today's date
LocalDate alonzosBirthday = LocalDate.of(1903, 6, 14);
// Uses the Month enumeration Month.JUNE
// 月份不再是從0開始,年份也不是從1900年開始
alonzosBirthday = LocalDate.of(1903, Month.JUNE, 14);
local date的間隔是Period
,表示度過的年、月、日
birthday.plus(Period.ofYears(1))
birthday.plusYears(1)
// 如果是閏年,得不到明年生日的準確日期
birthday.plus(Duration.ofDays(365))
// 兩個local date的間隔
independenceDay.until(christmas)
independenceDay.until(christmas, ChronoUnit.DAYS) // 174 days
// 返回1,即當週的第幾天 DayOfWeek.MONDAY
LocalDate.of(1900, 1, 1).getDayOfWeek().getValue()
// DayOfWeek.TUESDAY
DayOfWeek.SATURDAY.plus(3)
LocalDate start = LocalDate.of(2000, 1, 1);
LocalDate endExclusive = LocalDate.now();
// java9新方法,得到一個LocalDate物件的流
Stream<LocalDate> allDays = start.datesUntil(endExclusive);
// 每個月的第一天
Stream<LocalDate> firstDaysInMonth = start.datesUntil(endExclusive, Period.ofMonths(1));
Date Adjusters
調節器是用來根據一個給定日期得到和它相關的某個日期
TemporalAdjusters
類提供了一些靜態方法來做一般的調節
// 一個月的第一個星期二
// 返回的LocalDate是一個新物件
LocalDate firstTuesday = LocalDate.of(year, month, 1)
.with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY));
通過實現TemporalAdjuster
介面,可以實現自己的調節器(adjuster)
// 計算下一個工作日
// w的型別是Temporal,必須要轉為LocalDate,也可以通過ofDateAdjuster(引數是UnaryOperator<LocalDate>型別的lambda表示式)來替代型別轉換
TemporalAdjuster NEXT_WORKDAY = w ->
{
var result = (LocalDate) w;
do
{
result = result.plusDays(1);
}
// 週日開始工作日
while (result.getDayOfWeek().getValue() >= 6);
return result;
};
LocalDate backToWork = today.with(NEXT_WORKDAY);
// 通過ofDateAdjuster
TemporalAdjuster NEXT_WORKDAY = TemporalAdjusters.ofDateAdjuster(w ->
{
LocalDate result = w; // No cast
do
{
result = result.plusDays(1);
}
while (result.getDayOfWeek().getValue() >= 6);
return result;
});
Local Time
LocalTime
代表一天中的時間,例如15:30:00
// 獲取時間
LocalTime rightNow = LocalTime.now();
LocalTime bedtime = LocalTime.of(22, 30); // or LocalTime.of(22, 30, 0)
// plus/minus 操作是在24小時內範圍
LocalTime wakeup = bedtime.plusHours(8); // wakeup is 6:30:00
LocalTime
本身不關心AM PM
,使用格式化類來處理這類問題
LocalDateTime
類代表一個日期+一個時間,這個類適合儲存在固定時區下的一個時間點。然而,如果要處理跨過夏令時或者處理不同時區的使用者,那麼應該使用ZonedDateTime
Zoned Time
時區是人為劃分的,所以比地球自轉帶來的複雜性更煩(!)。現實世界中,我們遵循格林威治時間(中國橫跨4個時區)
Internet Assigned Numbers Authority (IANA)擁有一個數據庫(www.iana.org/time-zones),它包括全世界所有的時區,每年都會更新幾次,這些更新是為了處理夏令時的規則。Java使用了IANA資料庫。每個時區有一個ID,例如America/New_York Europe/Berlin。為了找到所有的時區,呼叫ZoneId.getAvailableZoneIds
(寫書的時候有將近600個ID)(寫這篇的時候是601)。給定一個時區ID,呼叫ZoneId.of(id)
會產生一個ZoneId
物件,可以使用這個物件將LocalDateTime
物件改為一個ZonedDateTime
物件(呼叫local.atZone(zoneId)
),或者通過ZonedDateTime.of(year, month, day, hour, minute, second, nano, zoneId)
來構建一個ZonedDateTime
物件
// 1969-07-16T09:32-04:00[America/New_York]
ZonedDateTime apollo11launch = ZonedDateTime.of(1969, 7, 16, 9, 32, 0, 0, ZoneId.of("America/New_York"));
// 獲取instant
apollo11launch.toInstant()
// 如果有一個instant,那麼可以將其轉為某時區 ZonedDateTime,也可以傳不同的時區ID
instant.atZone(ZoneId.of("UTC"))
UTC時間是在Greenwich Royal Observatory的時間,沒有夏令時
ZonedDateTime
的大部分方法和LocalDateTime
類似(參考API)
夏令時問題:
在2013年,中歐在3月31號2:00調整了夏令時,如果想建立一個不存在時間 3月31號2:30,實際上會得到一個3:30
// Constructs March 31 3:30
ZonedDateTime skipped = ZonedDateTime.of(LocalDate.of(2013, 3, 31), LocalTime.of(2, 30), ZoneId.of("Europe/Berlin"));
相反,夏令時結束的時候,時候會被調回1個小時,那麼同一個local time會有兩個instant,在這個時間跨度中建立一個時間,會拿到這兩個時間點中早的那個
ZonedDateTime ambiguous = ZonedDateTime.of(LocalDate.of(2013, 10, 27), // End of daylight savings time
LocalTime.of(2, 30),
// 2013-10-27T02:30+02:00[Europe/Berlin]
ZoneId.of("Europe/Berlin"));
// 2013-10-27T02:30+01:00[Europe/Berlin]
ZonedDateTime anHourLater = ambiguous.plusHours(1);
一個小時後,時間有了固定的小時和分鐘數,但是時區的偏移值已經改變了。在調節度過夏令時的日期時,不要加7天,而是使用Period
// 不要使用這種方式
ZonedDateTime nextMeeting = meeting.plus(Duration.ofDays(7));
// OK
ZonedDateTime nextMeeting = meeting.plus(Period.ofDays(7));
OffsetDateTime
類表示UTC時間的偏移量(不算時區規則),這個類一般用於特殊應用(不需要那些時區規則的,例如一些網路協議)。對人類時間來說,使用ZonedDateTime
Formatting and Parsing
DateTimeFormatter
提供了三種formatter來列印date/time值
- 預定義的標準formatter
- 地區指定(locale-specific)formatter
- 自定義模式的formatter
要使用formatter,呼叫format
// 1969-07-16T09:32:00-04:00"
String formatted = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(launch)
預定義的formatter
formatter | 描述 | 例子 |
---|---|---|
BASIC_ISO_DATE | 年月日-時區偏移(offset),沒有分隔符 | 19690713-0500 |
ISO_LOCAL_DATE,ISO_LOCAL_TIME,ISO_LOCAL_DATE_TIME | 分隔符 - : T | 1969-07-16,09:32:00,1969-07-16T09:32:00 |
ISO_OFFSET_DATE,ISO_OFFSET_TIME,ISO_OFFSET_DATE_TIME | 和ISO_LOCAL_xx類似,但是帶時區偏移 | xx-05:00 |
ISO_ZONED_DATE_TIME | 帶有時區偏移和id | xx[America/New_York] |
ISO_INSTANT | UTC,Z的時區id | 1969-07-19T14:32:00Z |
ISO_DATE,ISO_TIME,ISO_DATE_TIME | 和ISO_OFFSET_DATE這些類似,但是時區資訊是可選的 | xx-05:00[America/New_York] |
ISO_ORDINAL_DATE | 年,和年的第多少天,對於LocalDate | 1969-197 |
ISO_WEEK_DATE | 年,周,該周第幾天,對於LocalDate | 1969-w29-3 |
RFC_1123_DATE_TIME | email的標準時間戳,RFC822整理的,在RFC1123把年更新成4位 | Wed, 16 Jul 1969 09:32:00 -0500 |
這些標準的formatter主要是為了機器易讀的時間戳。人類易讀的日期和時間使用locale-specific formatter。4中style SHORT MEDIUM LONG FULL
style | date | time |
---|---|---|
SHORT | 7/16/69 | 9:32 AM |
MEDIUM | Jul 16, 1999 | 9:32:00 AM |
LONG | July 16, 1969 | 8:32:00 AM EDT |
FULL | Wednesday, July 16, 1969 | 9:22:00 AM EDT |
使用ofLocalizedDate
ofLocalizedTime
ofLocalizedDateTime
來建立一個formatter
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
// July 16, 1969 9:32:00 AM EDT
String formatted = formatter.format(apollo11launch);
// 改變預設地區
// 16 juillet 1969 09:32:00 EDT
formatted = formatter.withLocale(Locale.FRENCH).format(apollo11launch);
// Prints Mon Tue Wed Thu Fri Sat Sun
for (DayOfWeek w : DayOfWeek.values())
System.out.print(w.getDisplayName(TextStyle.SHORT, Locale.ENGLISH) + " ");
// DateTimeFormatter是DateFormat的代替,如果需要後者,可以使用toFormat
formatter.toFormat().
通過指定pattern自己建立formatter,模式參考下表
和歷史程式碼的互操作
Java曾經的時間相關處理類包括java.util.Date
java.util.GregorianCalendar
java.sql.Date/Time/Timestamp
Instant
類和java.util.Date
類是最為相似的,在Java8中,java.util.Date
類添加了兩個方法(toInstant
將Date轉為Instant from
從別的位置轉過來)
ZonedDateTime
和java.util.GregorianCalendar
最為相似,Java8中,java.util.GregorianCalendar
使用toZonedDateTime
方法將GregorianCalendar轉為ZonedDateTime,from
方法做相反的轉換
java.sql
包中的日期、時間不允許使用(!)
可以把DateTimeFormatter
類傳入歷史程式碼需要java.text.Format
物件的地方