在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、他們都是線程安全的