1. 程式人生 > 實用技巧 >Java語法--日期和時間的API

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使用時間精度需要:

  1. 一天86400s
  2. 每天中午(noon)要匹配官方時間
  3. 在一個精度控制下,儘量在其它時間匹配官方時間

呼叫靜態方法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中人類的時間有兩種

  1. local date/time
  2. 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值

  1. 預定義的標準formatter
  2. 地區指定(locale-specific)formatter
  3. 自定義模式的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從別的位置轉過來)

ZonedDateTimejava.util.GregorianCalendar最為相似,Java8中,java.util.GregorianCalendar使用toZonedDateTime方法將GregorianCalendar轉為ZonedDateTime,from方法做相反的轉換

java.sql包中的日期、時間不允許使用(!)

可以把DateTimeFormatter類傳入歷史程式碼需要java.text.Format物件的地方