標籤:時間 string 中國 含義 mem 分類 field default
?本節繼續探討Java 8的新特性,主要是介紹Java 8對日期和時間API的增強,關於日期和時間,我們在之前已經介紹過兩節了,32節介紹了Java 1.8以前的日期和時間API,主要的類是Date和Calendar,由於它的設計有一些不足,業界廣泛使用的是一個第三方的類庫Joda-Time,關於Joda-time,我們在33節進行了介紹。Java 1.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-11LocalTime lt = ldt.toLocalTime(); // 20:45:05//LocalDate加上時間,結果為2017-07-11 21:18:39LocalDateTime ldt2 = ld.atTime(21, 18, 39);//LocalTime加上日期,結果為2016-03-24 20:45:05LocalDateTime ldt3 = lt.atDate(LocalDate.of(2016, 3, 24));
ZonedDateTime
ZonedDateTime表示特定時區的日期和時間,擷取系統預設時區的當前日期和時間,代碼為:
ZonedDateTime zdt = ZonedDateTime.now();
LocalDateTime.now()也是擷取預設時區的當前日期和時間,有什麼區別呢?LocalDateTime內部不會記錄時區資訊,只會單純記錄年月日時分秒等資訊,而ZonedDateTime除了記錄行事曆資訊,還會記錄時區,它的其他大部分構建方法都需要顯式傳遞時區,比如:
//根據Instant和時區構建ZonedDateTimepublic 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節內容進行簡要梳理。
(與其他章節一樣,本節所有代碼位於 https://github.com/swiftma/program-logic,位於包shuo.laoma.java8.c95下)
----------------
未完待續,查看最新文章,敬請關注公眾號“老馬說編程”(掃描下方二維碼),從入門到進階,深入淺出,老馬和你一起探索Java編程及電腦技術的本質。用心原創,保留所有著作權。
電腦程式的思維邏輯 (95) - Java 8的日期和時間API