1. 程式人生 > >Java 8新特性之新的日期和時間API

Java 8新特性之新的日期和時間API

在Java 1.0中,對日期和時間的支援只能依賴java.util.Date類。這個類只能以毫秒的精度表示時間。這個類還有很多糟糕的問題,比如年份的起始選擇是1900年,月份的起始從0開始。這意味著你要想表示2018年8月22日,就必須建立下面這樣的Date例項:

Date date = new Date (118,7,22);

Wed Aug 22 00:00:00 CST 2018

甚至Date類的toString方法返回的字串也容易誤人。現在這個返回值甚至還包含了JVM的預設時區CST,但這不表示Date類在任何方面支援時區。

Java 1.1中,Date類中的很多方法被廢棄了,取而代之的是java.util.Calendar類。但是Calendar也有類似的問題和設計缺陷,導致使用這些方法寫出的程式碼非常容易出錯。比如月份依舊是從0開始計算,不過去掉了1900年開始計算年份這一設計。更糟的是同時存在Date和Calendar這兩個類,也增加了程式設計師的困惑。到底該使用哪一個類呢?此外,有的特性只有在某一個類有提供,比如用於以語言無關方式格式化和解析日期或時間的DateFormat方法就只有在Date類裡有。

DateFormat也有它自己的問題,首先他不是縣城安全的。這意味著兩個縣城如果嘗試使用同一個formatter解析日期,你可能無法得到預期的結果。

最後,Date和Calendar類都是可變的。

Java 8 在java.time包中整合了很多Joda-Time的特性,java.time包中提供了很多新的類可以幫助你解決問題:LocalDate、LocalTime、Instant、Duration和Period。

LocalDate

首先該類的例項是一個不可變物件,它只提供了簡單的日期,並不含當天的時間。另外,它也不附帶任何時區相關的資訊。 你可以通過靜態工廠of建立一個LocalDate例項,LocalDate提供了很多方法來讀取常用的值,比如年月日星期幾等:

        LocalDate date = LocalDate.of(2018,8,22);
        int year = date.getYear(); //2018
        Month month = date.getMonth(); //AUGUST(7)
        int day = date.getDayOfMonth(); //22
        DayOfWeek dow = date.getDayOfWeek(); //WEDNESDAY
        int len = date.lengthOfMonth(); //31
        boolean leap = date.isLeapYear(); //false

        LocalDate today = LocalDate.now(); //2018-08-22

傳遞TemporalField引數給 date的get方法也可以拿到同樣的資訊,TemporalField是一個藉口,它定義瞭如何訪問temporal物件某個欄位的值。ChronoField列舉實現了這一藉口,所以你可以很方便的使用get方法得到列舉元素的值:

        int yearofGet = date.get(ChronoField.YEAR); //2018
        int monthofGet = date.get(ChronoField.MONTH_OF_YEAR); //8
        int dayofGet = date.get(ChronoField.DAY_OF_MONTH); //22

LocalTime

與LocalDate類似,LocalTime表示時間,你可以使用of過載的兩個工廠方法建立LocalTime例項,第一個過載是小時和分鐘,第二個過載還接受秒。也提供了getter方法訪問這些變數的值:

        LocalTime time = LocalTime.of(17,55,55);
        int hour = time.getHour();//17
        int minute = time.getMinute();//55
        int second = time.getSecond();//55

 

LocalDate和LocalTime都支援從字串建立,使用靜態方法parse:

        LocalDate dateofString = LocalDate.parse("2018-08-22");
        LocalTime timeofString = LocalTime.parse("15:53:52");

如果格式不正確,無法被解析成合法的LocalDate或LocalTime物件。parse方法會丟擲一個繼承自RuntimeException的DateTimeParseException異常。

LocalDateTime

這個複合類是LocalDate何LocalTime的合體,同時表示了日期和時間。但不帶有時區資訊,你可以直接建立,也可以通過合併日期和時間物件構造。

        LocalDateTime dt1 = dateofString.atTime(timeofString);
        LocalDateTime dt2 = time.atDate(date);
        LocalDateTime dt3 = LocalDateTime.of(date,time);
        LocalDateTime dt4 = LocalDateTime.of(2018,8,22,17,55,55);
        //2018-08-22T17:55:55

也可以將LocalDateTime拆分為獨立的LocalDate 和LocalTime:

        LocalDate localDate = dt1.toLocalDate();
        LocalTime localTime = dt1.toLocalTime();

Instant

作為人,我們習慣於星期幾、幾號、幾點、幾分這樣的方式理解日期和時間,但是對於計算機而言並不容易理解。java.time.Instant類對時間的建模方式是 以Unix元年時間(UTC時區1970年1月1日午夜時分)開始所經歷的描述進行計算。

可以使用靜態工廠方法ofEpochSecond傳遞一個代筆哦啊秒數的值建立一個該類的例項。這個方法還有一個過載版本,它接受第二個以納秒為單位的引數值,對傳入作為秒數的引數進行調整。過載的版本會調整納秒引數,確保儲存的納秒分片在0到999999999之間,這意味著下面這些對ofEpochSecond工廠方法的呼叫返回幾乎同樣的Instant物件:

        Instant instant = Instant.ofEpochSecond(3);
        Instant instant2 = Instant.ofEpochSecond(3, 0);
        Instant instant3 = Instant.ofEpochSecond(2, 1_000_000_000);
        Instant instant4 = Instant.ofEpochSecond(4, -1_000_000_000);

1970-01-01T00:00:03Z

Instant類也支援靜態工廠方法now,它能夠幫你獲取當前時刻的時間戳。Instant的設計初衷是為了便於機器使用,它所包含的是由秒及納秒所構成的數字。所以,它無法處理那些我們非常容易理解的時間單位。

 

定義Duration或Period

計算日期時間差使用這兩個類

        Duration d1 = Duration.between(time1, time2);
        Duration d2 = Duration.between(datetime1, datetime2);
        Duration d3 = Duration.between(instant,instant2);

由於LocalDateTime和Instant是為不同的目的而設計的,一個是為了人閱讀的,一個是為了機器處理的,所以不能將二者混用。此外,由於Duration類主要用於以秒和納秒衡量時間的長短,所以不能向between方法傳遞LocalDate。

Duration和Period類都提供了很多非常方便的工廠類,直接建立對應的例項。

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

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

Period計算時間差

        LocalDate today = LocalDate.now();
        System.out.println("Today : " + today);
        LocalDate birthDate = LocalDate.of(1991, 1, 11);
        System.out.println("BirthDate : " + birthDate);

        Period p = Period.between(birthDate, today);
        System.out.printf("年齡 : %d 年 %d 月 %d 日", p.getYears(), p.getMonths(), p.getDays());

Today : 2018-08-22
BirthDate : 1991-01-11
年齡 : 27 年 7 月 11 日

Duration計算相差秒數

        Instant inst1 = Instant.now();
        System.out.println("Inst1 : " + inst1);
        Instant inst2 = inst1.plus(Duration.ofSeconds(10));
        System.out.println("Inst2 : " + inst2);

        System.out.println("Difference in milliseconds : " + Duration.between(inst1, inst2).toMillis());

        System.out.println("Difference in seconds : " + Duration.between(inst1, inst2).getSeconds());

Difference in milliseconds : 10000
Difference in seconds : 10

到目前為止,這些日期和時間都是不可修改的,這是為了更好的支援函數語言程式設計,確保執行緒安全。如果你想在LocalDate例項上增加3天,或者將日期解析和輸入 dd/MM/yyyy這種格式。 將在下一節講解。

操作、解析和格式化日期

對已存在的LocalDate物件,建立它的修改版,最簡單的方式是使用withAttribute方法。withAttribute方法會建立物件的一個副本,並按照需要修改它的屬性。以下所有的方法都返回了一個修改屬性的物件,他們不會影響原來的物件。

        LocalDate  dd = LocalDate.of(2018,8,23); //2018-08-23
        LocalDate dd1 = dd.withYear(2017); //2017-08-23
        LocalDate dd2 = dd.withDayOfMonth(22); //2018-08-22
        LocalDate dd4 = dd.withMonth(10); //2018-10-23
        LocalDate dd3 = dd.with(ChronoField.MONTH_OF_YEAR,9); //2018-09-23

除了withAttribute詳細的年月日,也可以採用通用的with方法,第一個引數是TemporalField物件,第二個引數是修改的值。它也可以操縱LocalDate物件:

        LocalDate dd5 = dd.plusWeeks(1); //加一週
        LocalDate dd6 = dd.minusYears(3); //減去三年
        LocalDate dd7 = dd.plus(6,ChronoUnit.MONTHS); //加6月

plus和minus方法和上面的with類似,他們都聲明於Temporal介面中。像LocalDate、LocalTime、LocalDateTime以及Instant這些表示日期和時間的類提供了大量的通用方法:

1、from 靜態方法 依據傳入的Temporal物件建立是例項

2、now 靜態方法 依據系統時鐘建立Temporal物件

3、of 靜態方法 由Temporal物件的某個部分建立物件的例項

4、 parse 靜態方法  由字串建立Temporal物件的例項

5、atOffset 將Temporal物件和某個時區偏移相結合

6、atZone 將Temporal 物件和某個時區相結合

7、format 使用某個指定的格式將Temporal物件轉換為字串(Instant類不提供此方法)

8、get 讀取Temporal物件的某一部分的值

9、minus 建立物件的一個副本,然後將當前的Temporal物件的值減去一定的時長建立該副本

10、plus 建立物件的一個副本,然後將當前Temporal物件的值加上一定的時長建立該副本

11、with 以該物件為模板,對某些狀態進行修改建立該物件的副本。

TemporalAdjuster

操縱更復雜的日期,比如將日期調整到下個週日、下個工作日,或者是本月的最後一天。這時可以使用with的過載版本,向其傳遞一個提供了更多定製化選擇的TemporalAdjuster物件,更加靈活的處理日期。

 

import static java.time.temporal.TemporalAdjusters.*;

        LocalDate dd = LocalDate.of(2018,8,23);

        LocalDate dd1 = dd.with(dayOfWeekInMonth(2,DayOfWeek.FRIDAY)); //同一個月中,第二個星期五 2018-08-10

        LocalDate dd2 = dd.with(firstDayOfMonth()); //當月的第一天 2018-08-01
        LocalDate dd3 = dd.with(firstDayOfNextMonth()); //下月的第一天 2018-09-01
        LocalDate dd4 = dd.with(firstDayOfNextYear()); //明年的第一天 2019-01-01
        LocalDate dd5 = dd.with(firstDayOfYear()); //當年的第一天 2018-01-01
        LocalDate dd6 = dd.with(firstInMonth(DayOfWeek.MONDAY)); //當月第一個星期一 2018-08-06

        LocalDate dd7 = dd.with(lastDayOfMonth()); //當月的最後一天 2018-08-31
        LocalDate dd8 = dd.with(lastDayOfYear()); //當年的最後一天 2018-12-31
        LocalDate dd9 = dd.with(lastInMonth(DayOfWeek.SUNDAY)); //當月最後一個星期日 2018-08-26

        LocalDate dd10 = dd.with(previous(DayOfWeek.MONDAY)); //將日期向前調整到第一個符合星期一 2018-08-20
        LocalDate dd11 = dd.with(next(DayOfWeek.MONDAY)); //將日期向後調整到第一個符合星期一 2018-08-27

        LocalDate dd12 = dd.with(previousOrSame(DayOfWeek.FRIDAY)); //將日期向前調整第一個符合星期五,如果該日期已經符合,直接返回該物件 2018-08-17
        LocalDate dd13 = dd.with(nextOrSame(DayOfWeek.FRIDAY)); //將日期向後調整第一個符合星期五,如果該日期已經符合,直接返回該物件 2018-08-24

TemporalAdjuster可以進行復雜的日期操作,如果沒有找到符合的預定義方法,可以自己建立一個,TemporalAdjuster介面只聲明瞭一個方法所以他說一個函式式介面:

@FunctionalInterface
public interface TemporalAdjuster {
    Temporal adjustInto(Temporal temporal);
}

這意味著TemporalAdjuster介面的實現需要定義如何將一個Temporal物件轉換為另一個Temporal物件。比如設計一個NextWorkingDay類,實現計算下一個工作日,過濾掉週六和週日節假日。

public class NextWorkingDay implements TemporalAdjuster {
    @Override
    public Temporal adjustInto(Temporal temporal) {
        DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
        int dayToAdd = 1;
        //如果當前日期 星期一到星期五之間返回下一天
        if(dow == DayOfWeek.FRIDAY) dayToAdd = 3;
        //如果當前日期 週六或週日 返回下週一
        else if(dow == DayOfWeek.SATURDAY) dayToAdd = 2;
        return temporal.plus(dayToAdd, ChronoUnit.DAYS);
    }
}

由於TemporalAdjuster是函式式介面,所以你只能以Lambda表示式的方式向Adjuster介面傳遞行為:

date.with(t->{

  //上面一坨

})

為了可以重用,TemporalAdjuster物件推薦使用TemporalAdjusters類的靜態工廠方法ofDateAdjuster:

 

        LocalDate dd = LocalDate.of(2018, 8, 23);

        TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(
                temporal -> {
                    DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
                    int dayToAdd = 1;
                    //如果當前日期 星期一到星期五之間返回下一天
                    if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
                        //如果當前日期 週六或週日 返回下週一
                    else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
                    return temporal.plus(dayToAdd, ChronoUnit.DAYS);
                }
        );
        LocalDate next = dd.with(nextWorkingDay);

列印輸出及解析日期

處理日期和時間物件時,格式化以及解析日期-時間物件是另一個非常重要的功能。新的java.time.format包就是為了這個目的而設計的。這個包中最重要的類是DateTimeFormatter,建立格式器最簡單的方式是通過他的靜態工廠方法及常量。

        String s1 = next.format(DateTimeFormatter.BASIC_ISO_DATE); //20180824
        String s2 = next.format(DateTimeFormatter.ISO_LOCAL_DATE); //2018-08-24

除了解析為字串外,還可以通過解析代表日期或時間的字串重新建立該日期物件。

        LocalDate date1 = LocalDate.parse("20180901",DateTimeFormatter.BASIC_ISO_DATE);
        LocalDate date2 = LocalDate.parse("2018-09-02",DateTimeFormatter.ISO_LOCAL_DATE);

與老的java.util.DateFormat想比較,所有的DateTimeFormatter例項都是執行緒安全的。DateTimeFormatter類還支援一個靜態工廠方法,它按照某個特定的模式建立格式器。

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
        LocalDate now = LocalDate.now();
        String formatterDate = now.format(formatter);
        LocalDate nowparse = LocalDate.parse(formatterDate,formatter);

ofPattern可以按照指定的格式進行解析成字串,然後又呼叫了parse方法的過載 將該格式的字串轉換成了 LocalDate物件。

ofPattern也提供了過載版本,使用它可以建立某個Locale的格式器:

        DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MMMMd號", Locale.CHINA);
        LocalDate chinaDate = LocalDate.parse("2018-08-21");
        String formatterDate2 = chinaDate.format(formatter2); //2018年八月21號
        LocalDate chinaDate2 = LocalDate.parse(formatterDate2,formatter2);

DateFormatterBuilder類還提供了更復雜的格式器和更強大的解析功能:

 

        DateTimeFormatter chinaFormatter = new DateTimeFormatterBuilder().appendText(ChronoField.YEAR)
                .appendLiteral("年")
                .appendText(ChronoField.MONTH_OF_YEAR)
                .appendText(ChronoField.DAY_OF_MONTH)
                .appendLiteral("號")
                .parseCaseInsensitive().toFormatter(Locale.CHINA);

處理不同的時區和曆法

之前所看到的日期和時間種類都不包含時區資訊。時區的處理是新版日期和時間API新增加的重要功能,新的 java.time.ZoneId 類是老版 java.util.TimeZone 的替代品。

時區是按照一定的規則將區域劃分成的標準時間相同的區間。在ZoneRules這個類中包含了40個這樣的例項。你可以使用ZoneId的getRules()得到指定時區的規則。每個特定的ZoneId物件都由一個地區標識:

ZoneId romeZone = ZoneId.of("Europe/Rome"); //格式 歐洲/羅馬

地區ID都為 “{區域}/{城市}”的格式,這些地區集合的設定都由英特網編號分配機構(IANA)的時區資料庫提供。你可以通過java 8的新方法toZoneId將一個老的時區物件轉換為ZoneId

ZoneId zoneId = TimeZone.getDefault().toZoneId();

一旦得到一個ZoneId物件,就可以將它與LocalDate、LocalDateTIme或者是Instant物件整合起來,構造為一個ZonedDateTime例項,它代表了相對於指定時區的時間點,

 

        LocalDate date = LocalDate.of(2018,8,22);
        ZonedDateTime zdt1 = date.atStartOfDay(romeZone);

        LocalDateTime dateTime = LocalDateTime.of(2018,8,23,13,48,00);
        ZonedDateTime zdt2 = dateTime.atZone(romeZone);

        Instant instant = Instant.now();
        ZonedDateTime zdt3 = instant.atZone(romeZone);

 

ZonedDateTime = LocalDateTime(LocalDate + LocalTime) + ZoneId

通過ZoneId,你還可以將LocalDateTime轉換為Instant

        LocalDateTime dateTime = LocalDateTime.of(2018,8,23,13,48,00);
        Instant instantFromDateTime = dateTime.toInstant(romeZone);

        Instant instant1 = Instant.now();
        LocalDateTime timeFromInstant = LocalDateTime.ofInstant(romeZone);

利用和 UTC/格林尼治時間的固定偏差計算時區

另一種比較常用的表達時區的方式就是利用當前時區和 UTC/格林尼治 的固定偏差,比如,紐約落後倫敦5小時。這種情況下,你可以使用ZoneOffset類,它是ZoneId的一個子類,表示的是當前時間和倫敦格林尼治子午時間的差異:

ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");

這種方式不推薦使用,因為 -05:00 的偏差實際上是對應的美國東部標準時間,並未考慮任何日光時的影響。

        LocalDateTime dateTime1 = LocalDateTime.now();
        OffsetDateTime dateTimeInNewYork1 = OffsetDateTime.of(dateTime1,newYorkOffset);

它使用ISO-8601的歷法系統,以相對於UTC時間的偏差方式表示日期時間。

使用別的日曆系統

ISO-8601日曆系統是世界文明日曆系統的事實標準。但是,java 8 中另外提供了4種其他的日曆系統。這些日曆系統中的每一個都有一個對應的日誌類,分別是ThaiBuddhistDate、MinguoDate、JapaneseDate以及HijrahDate。所有這些類以及LocalDate都實現了ChronoLocalDate介面,能夠對公曆的日期進行建模。利用LocalDate物件,可以建立這些類的例項。

小結:

  1、Java 8之前的java.util.Date類以及其他用於建模日期時間的雷有很多不一致及設計上的缺陷,包括易變性以及糟糕的偏移值、預設值和命名。

  2、新版的日期和時間API中,日期-時間物件是不可變的。

  3、新的API提供了兩種不同的時間表示方式,有效地區分了執行時人喝機器的不同需求。

  4、操縱的日期不會影響老值,而是新生成一個例項。

  5、TemporalAdjuster可以更精確的操縱日期,還可以自定義日期轉換器。

  6、他們都是執行緒安全的