Java 8 (11) 新的日期和時間API

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

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類都是可變的。app

 

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

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方法獲得枚舉元素的值:ui

        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這些表示日期和時間的類提供了大量的通用方法:

一、from 靜態方法 依據傳入的Temporal對象建立是實例

二、now 靜態方法 依據系統時鐘建立Temporal對象

三、of 靜態方法 由Temporal對象的某個部分建立對象的實例

四、 parse 靜態方法  由字符串建立Temporal對象的實例

五、atOffset 將Temporal對象和某個時區偏移相結合

六、atZone 將Temporal 對象和某個時區相結合

七、format 使用某個指定的格式將Temporal對象轉換爲字符串(Instant類不提供此方法)

八、get 讀取Temporal對象的某一部分的值

九、minus 建立對象的一個副本,而後將當前的Temporal對象的值減去必定的時長建立該副本

十、plus 建立對象的一個副本,而後將當前Temporal對象的值加上必定的時長建立該副本

十一、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對象,能夠建立這些類的實例。

 

小結:

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

  二、新版的日期和時間API中,日期-時間對象是不可變的。

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

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

  五、TemporalAdjuster能夠更精確的操縱日期,還能夠自定義日期轉換器。

  六、他們都是線程安全的

相關文章
相關標籤/搜索