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

swiftma發表於2017-09-04

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

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

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

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

表示日期和時間

基本概念

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

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

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

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

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

Instant

Instant表示時刻,獲取當前時刻,程式碼為:

Instant now = Instant.now();
複製程式碼

可以根據Epoch Time (紀元時)建立Instant,比如,另一種獲取當前時刻的程式碼可以為:

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程式設計及計算機技術的本質。用心原創,保留所有版權。

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

相關文章