計算機程序的思惟邏輯 (95) - Java 8的日期和時間API

本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html

本節繼續探討Java 8的新特性,主要是介紹Java 8對日期和時間API的加強,關於日期和時間,咱們在以前已經介紹過兩節了,32節介紹了Java 8之前的日期和時間API,主要的類是Date和Calendar,因爲它的設計有一些不足,業界普遍使用的是一個第三方的類庫Joda-Time,關於Joda-time,咱們在33節進行了介紹。Java 8學習了Joda-time,引入了一套新的API,位於包java.time下,本節,咱們就來簡要介紹這套新的API。java

咱們先從日期和時間的表示開始。git

表示日期和時間

基本概念

咱們在32節介紹過日期和時間的幾個基本概念,這裏簡要回顧下。github

  • 時刻:全部計算機系統內部都用一個整數表示時刻,這個整數是距離格林尼治標準時間1970年1月1日0時0分0秒的毫秒數,能夠理解時刻就是絕對時間,它與時區無關,不一樣時區對同一時刻的解讀,即年月日時分秒是不同的;
  • 時區:同一時刻,世界上各個地區的時間多是不同的,具體時間與時區有關,一共有24個時區,英國格林尼治是0時區,北京是東八區,也就是說格林尼治凌晨1點,北京是早上9點;
  • 年曆:咱們都知道,中國有公曆和農曆之分,公曆和農曆都是年曆,不一樣的年曆,一年有多少月,每個月有多少天,甚至一天有多少小時,這些可能都是不同的,咱們主要討論公曆。

Java 8中表示日期和時間的類有多個,主要的有:編程

  • Instant:表示時刻,不直接對應年月日信息,須要經過時區轉換
  • LocalDateTime: 表示與時區無關的日期和時間信息,不直接對應時刻,須要經過時區轉換
  • LocalDate:表示與時區無關的日期,與LocalDateTime相比,只有日期信息,沒有時間信息
  • LocalTime:表示與時區無關的時間,與LocalDateTime相比,只有時間信息,沒有日期信息
  • ZonedDateTime: 表示特定時區的日期和時間
  • ZoneId/ZoneOffset:表示時區

類比較多,但概念更爲清晰了,下面咱們逐個來看下。swift

Instant

Instant表示時刻,獲取當前時刻,代碼爲:安全

Instant now = Instant.now();
複製代碼

能夠根據Epoch Time (紀元時)建立Instant,好比,另外一種獲取當前時刻的代碼能夠爲:bash

Instant now = Instant.ofEpochMilli(System.currentTimeMillis());
複製代碼

咱們知道,Date也表示時刻,Instant和Date能夠經過紀元時相互轉換,好比,轉換Date爲Instant,代碼爲:微信

public static Instant toInstant(Date date) {
    return Instant.ofEpochMilli(date.getTime());
}
複製代碼

轉換Instant爲Date,代碼爲:函數

public static Date toDate(Instant instant) {
    return new Date(instant.toEpochMilli());
}
複製代碼

Instant有不少基於時刻的比較和計算方法,大多比較直觀,咱們就不列舉了。

LocalDateTime

LocalDateTime表示與時區無關的日期和時間信息,獲取系統默認時區的當前日期和時間,代碼爲:

LocalDateTime ldt = LocalDateTime.now();
複製代碼

還能夠直接用年月日等信息構建LocalDateTime,好比,表示2017年7月11日20點45分5秒,代碼能夠爲:

LocalDateTime ldt = LocalDateTime.of(2017, 7, 11, 20, 45, 5);
複製代碼

LocalDateTime有不少方法,能夠獲取年月日時分秒等日曆信息,好比:

public int getYear() public int getMonthValue() public int getDayOfMonth() public int getHour() public int getMinute() public int getSecond() 複製代碼

還能夠獲取星期幾等信息,好比:

public DayOfWeek getDayOfWeek() 複製代碼

DayOfWeek是一個枚舉,有七個取值,從DayOfWeek.MONDAY到DayOfWeek.SUNDAY。

LocalDateTime不能直接轉爲時刻Instant,轉換須要一個參數ZoneOffset,ZoneOffset表示相對於格林尼治的時區差,北京是+08:00,好比,轉換一個LocalDateTime爲北京的時刻,方法爲:

public static Instant toBeijingInstant(LocalDateTime ldt) {
    return ldt.toInstant(ZoneOffset.of("+08:00"));
}
複製代碼

給定一個時刻,使用不一樣時區解讀,日曆信息是不一樣的,Instant有方法根據時區返回一個ZonedDateTime:

public ZonedDateTime atZone(ZoneId zone) 複製代碼

默認時區是ZoneId.systemDefault(),能夠這樣構建ZoneId:

//北京時區
ZoneId bjZone = ZoneId.of("GMT+08:00")
複製代碼

ZoneOffset是ZoneId的子類,能夠根據時區差構造。

LocalDate/LocalTime

能夠認爲,LocalDateTime由兩部分組成,一部分是日期LocalDate,另外一部分是時間LocalTime,它們的用法也很直觀,好比:

//表示2017年7月11日
LocalDate ld = LocalDate.of(2017, 7, 11);

//當前時刻按系統默認時區解讀的日期
LocalDate now = LocalDate.now();

//表示21點10分34秒
LocalTime lt = LocalTime.of(21, 10, 34);

//當前時刻按系統默認時區解讀的時間
LocalTime time = LocalTime.now();
複製代碼

LocalDateTime由LocalDate和LocalTime構成,LocalDate加上時間能夠構成LocalDateTime,LocalTime加上日期能夠構成LocalDateTime,好比:

LocalDateTime ldt = LocalDateTime.of(2017, 7, 11, 20, 45, 5);
LocalDate ld = ldt.toLocalDate(); //2017-07-11
LocalTime lt = ldt.toLocalTime(); // 20:45:05

//LocalDate加上時間,結果爲2017-07-11 21:18:39
LocalDateTime ldt2 = ld.atTime(21, 18, 39);

//LocalTime加上日期,結果爲2016-03-24 20:45:05
LocalDateTime ldt3 = lt.atDate(LocalDate.of(2016, 3, 24));
複製代碼

ZonedDateTime

ZonedDateTime表示特定時區的日期和時間,獲取系統默認時區的當前日期和時間,代碼爲:

ZonedDateTime zdt = ZonedDateTime.now();
複製代碼

LocalDateTime.now()也是獲取默認時區的當前日期和時間,有什麼區別呢?LocalDateTime內部不會記錄時區信息,只會單純記錄年月日時分秒等信息,而ZonedDateTime除了記錄日曆信息,還會記錄時區,它的其餘大部分構建方法都須要顯式傳遞時區,好比:

//根據Instant和時區構建ZonedDateTime
public static ZonedDateTime ofInstant(Instant instant, ZoneId zone) //根據LocalDate, LocalTime和ZoneId構造 public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone) 複製代碼

ZonedDateTime能夠直接轉換爲Instant,好比:

ZonedDateTime ldt = ZonedDateTime.now();
Instant now = ldt.toInstant();
複製代碼

格式化/解析字符串

Java 8中,主要的格式化類是java.time.format.DateTimeFormatter,它是線程安全的,看個例子:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt = LocalDateTime.of(2016,8,18,14,20,45);
System.out.println(formatter.format(ldt));
複製代碼

輸出爲:

2016-08-18 14:20:45
複製代碼

將字符串轉化爲日期和時間對象,可使用對應類的parse方法,好比:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String str = "2016-08-18 14:20:45";
LocalDateTime ldt = LocalDateTime.parse(str, formatter);
複製代碼

設置和修改時間

修改時期和時間有兩種方式,一種是直接設置絕對值,另外一種是在現有值的基礎上進行相對增減操做,Java 8的大部分類都支持這兩種方式,另外,與Joda-Time同樣,Java 8的大部分類都是不可變類,修改操做是經過建立並返回新對象來實現的,原對象自己不會變

咱們來看一些例子。

調整時間爲下午3點20

代碼示例爲:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.withHour(15).withMinute(20).withSecond(0).withNano(0);
複製代碼

還能夠爲:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.toLocalDate().atTime(15, 20);
複製代碼

三小時五分鐘後

示例代碼爲:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.plusHours(3).plusMinutes(5);
複製代碼

LocalDateTime有不少plusXXX和minusXXX方法,用於相對增長和減小時間。

今天0點

能夠爲:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.with(ChronoField.MILLI_OF_DAY, 0);      
複製代碼

ChronoField是一個枚舉,裏面定義了不少表示日曆的字段,MILLI_OF_DAY表示在一天中的毫秒數,值從0到(24 * 60 * 60 * 1,000) - 1。

還能夠爲:

LocalDateTime ldt = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
複製代碼

LocalTime.MIN表示"00:00"

也能夠爲:

LocalDateTime ldt = LocalDate.now().atTime(0, 0);
複製代碼

下週二上午10點整

能夠爲:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.plusWeeks(1).with(ChronoField.DAY_OF_WEEK, 2)
    .with(ChronoField.MILLI_OF_DAY, 0).withHour(10);
複製代碼

下一個週二上午10點整

上面下週二指定是下週,若是是下一個週二呢?這與當前是周幾有關,若是當前是週一,則下一個週二就是明天,而其餘狀況則是下週,代碼能夠爲:

LocalDate ld = LocalDate.now();
if(!ld.getDayOfWeek().equals(DayOfWeek.MONDAY)){
    ld = ld.plusWeeks(1);
}
LocalDateTime ldt = ld.with(ChronoField.DAY_OF_WEEK, 2).atTime(10, 0);
複製代碼

針對這種複雜一點的調整,Java 8有一個專門的接口TemporalAdjuster,這是一個函數式接口,定義爲:

public interface TemporalAdjuster {
    Temporal adjustInto(Temporal temporal);
}
複製代碼

Temporal是一個接口,表示日期或時間對象,Instant,LocalDateTime,LocalDate等都實現了它,這個接口就是對日期或時間進行調整,還有一個專門的類TemporalAdjusters,裏面提供了不少TemporalAdjuster的實現,好比,針對下一個周幾的調整,方法是:

public static TemporalAdjuster next(DayOfWeek dayOfWeek) 複製代碼

針對上面的例子,代碼能夠爲:

LocalDate ld = LocalDate.now();
LocalDateTime ldt = ld.with(TemporalAdjusters.next(DayOfWeek.TUESDAY)).atTime(10, 0);
複製代碼

這個next方法是怎麼實現的呢?看代碼:

public static TemporalAdjuster next(DayOfWeek dayOfWeek) {
    int dowValue = dayOfWeek.getValue();
    return (temporal) -> {
        int calDow = temporal.get(DAY_OF_WEEK);
        int daysDiff = calDow - dowValue;
        return temporal.plus(daysDiff >= 0 ? 7 - daysDiff : -daysDiff, DAYS);
    };
}
複製代碼

它內部封裝了一些條件判斷和具體調整,提供了更爲易用的接口。

TemporalAdjusters中還有不少方法,部分方法以下:

public static TemporalAdjuster firstDayOfMonth() public static TemporalAdjuster lastDayOfMonth() public static TemporalAdjuster firstInMonth(DayOfWeek dayOfWeek) public static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek) public static TemporalAdjuster previous(DayOfWeek dayOfWeek) public static TemporalAdjuster nextOrSame(DayOfWeek dayOfWeek) 複製代碼

這些方法的含義比較直觀,就不解釋了,它們主要是封裝了日期和時間調整的一些基本操做,更爲易用。

明天最後一刻

代碼能夠爲:

LocalDateTime ldt = LocalDateTime.of(LocalDate.now().plusDays(1), LocalTime.MAX);
複製代碼

或者爲:

LocalDateTime ldt = LocalTime.MAX.atDate(LocalDate.now().plusDays(1));
複製代碼

本月最後一天最後一刻

代碼能夠爲:

LocalDateTime ldt =  LocalDate.now()
        .with(TemporalAdjusters.lastDayOfMonth())
        .atTime(LocalTime.MAX);
複製代碼

lastDayOfMonth()是怎麼實現的呢?看代碼:

public static TemporalAdjuster lastDayOfMonth() {
    return (temporal) -> temporal.with(DAY_OF_MONTH, temporal.range(DAY_OF_MONTH).getMaximum());
}        
複製代碼

這裏使用了range方法,從它的返回值能夠獲取對應日曆單位的最大最小值,展開來,本月最後一天最後一刻的代碼還能夠爲:

long maxDayOfMonth = LocalDate.now().range(ChronoField.DAY_OF_MONTH).getMaximum();
LocalDateTime ldt =  LocalDate.now()
        .withDayOfMonth((int)maxDayOfMonth)
        .atTime(LocalTime.MAX);
複製代碼

下個月第一個週一的下午5點整

代碼能夠爲:

LocalDateTime ldt = LocalDate.now()
        .plusMonths(1)
        .with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY))
        .atTime(17, 0);       
複製代碼

時間段的計算

Java 8中表示時間段的類主要有兩個,Period和Duration,Period表示日期之間的差,用年月日表示,不能表示時間,Duration表示時間差,用時分秒錶等表示,也能夠用天表示,一天嚴格等於24小時,不能用年月表示,下面看一些例子。

計算兩個日期之間的差

看個Period的例子:

LocalDate ld1 = LocalDate.of(2016, 3, 24);
LocalDate ld2 = LocalDate.of(2017, 7, 12);
Period period = Period.between(ld1, ld2);
System.out.println(period.getYears() + "年"
        + period.getMonths() + "月" + period.getDays() + "天");
複製代碼

輸出爲:

1年3月18天
複製代碼

根據生日計算年齡

示例代碼能夠爲:

LocalDate born = LocalDate.of(1990,06,20);
int year = Period.between(born, LocalDate.now()).getYears();
複製代碼

計算遲到分鐘數

假定早上9點是上班時間,過了9點算遲到,遲到要統計遲到的分鐘數,怎麼計算呢?看代碼:

long lateMinutes = Duration.between(
        LocalTime.of(9,0),
        LocalTime.now()).toMinutes(); 
複製代碼

與Date/Calendar對象的轉換

Java 8的日期和時間API沒有提供與老的Date/Calendar相互轉換的方法,但在實際中,咱們多是須要的,前面介紹了,Date能夠與Instant經過毫秒數相互轉換,對於其餘類型,也能夠經過毫秒數/Instant相互轉換。

好比,將LocalDateTime按默認時區轉換爲Date,代碼能夠爲:

public static Date toDate(LocalDateTime ldt){
    return new Date(ldt.atZone(ZoneId.systemDefault())
            .toInstant().toEpochMilli());
}
複製代碼

將ZonedDateTime轉換爲Calendar,代碼能夠爲:

public static Calendar toCalendar(ZonedDateTime zdt) {
    TimeZone tz = TimeZone.getTimeZone(zdt.getZone());
    Calendar calendar = Calendar.getInstance(tz);
    calendar.setTimeInMillis(zdt.toInstant().toEpochMilli());
    return calendar;
}
複製代碼

Calendar保持了ZonedDateTime的時區信息。

將Date按默認時區轉爲LocalDateTime,代碼能夠爲:

public static LocalDateTime toLocalDateTime(Date date) {
    return LocalDateTime.ofInstant(
            Instant.ofEpochMilli(date.getTime()),
            ZoneId.systemDefault());
}
複製代碼

將Calendar轉爲ZonedDateTime,代碼能夠爲:

public static ZonedDateTime toZonedDateTime(Calendar calendar) {
    ZonedDateTime zdt = ZonedDateTime.ofInstant(
            Instant.ofEpochMilli(calendar.getTimeInMillis()),
            calendar.getTimeZone().toZoneId());
    return zdt;
}
複製代碼

小結

本節簡要介紹了Java 8中的日期和時間API,相比之前版本的Date和Calendar,它引入了更多的類,但概念更爲清晰了,更爲強大和易用了,Java 8學習了Joda-Time的不少概念和實現,與咱們以前介紹的Joda-Time很像。

從91節討論Lambda表達式到本節,關於Java 8的主要內容,咱們就介紹完了。

同時,關於整個Java編程的基礎部分,經過共95節的內容,咱們也基本探討完了,下一節是本系列文章的最後一篇,咱們對所有95節內容進行簡要梳理

(與其餘章節同樣,本節全部代碼位於 github.com/swiftma/pro…,位於包shuo.laoma.java8.c95下)


未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),從入門到高級,深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。

相關文章
相關標籤/搜索