在Java 1.0中,對日期和時間的支持只能依賴java.util.Date類。同時這個類還有兩個很大的缺點:年份的起始選擇是1900年,月份的起始從0開始。
在Java 1.1中,Date類中的很多方法被廢棄,取而代之的是java.util.Calendar類。然而Calendar類也有類似的問題和設計缺陷,導致使用這些方法寫出的代碼非常容易出錯。
DateFormat方法也有它自己的問題。比如,它不是線程安全的。這意味著兩個線程如果嘗試使用同一個formatter解析日期,你可能會得到無法預期的結果。
1. 使用LocalDate 和LocalTime
1.1 LocalDate
Java 8提供新的日期和時間API,LocalDate類實例是一個不可變對象,只提供簡單的日期并且不含當天時間信息。此外也不附帶任何與時區相關的信息。
通過靜態工廠方法of創建一個LocalDate實例。LocalDate實例提供了多種方法來讀取常用的值,比如年份、月份、星期幾等,如下所示。
LocalDate localDate = LocalDate.of(2014, 3, 18);
int year = localDate.getYear();
Month month = localDate.getMonth();
int day = localDate.getDayOfMonth();
DayOfWeek dow = localDate.getDayOfWeek();
int len = localDate.lengthOfMonth();
boolean leap = localDate.isLeapYear();
// 使用工廠方法從系統時鐘中獲取當前的日期
LocalDate today = LocalDate.now();
System.out.println(String.format("year:%s\nmonth:%s\nday:%s\ndow:%s\nlen:%s\nleap:%s", year, month, day, dow, len, leap));
System.out.println(today);
結果:
year:2014
month:MARCH
day:18
dow:TUESDAY
len:31
leap:false
2019-03-27
Java 8日期-時間類都提供了類似的工廠方法。通過傳遞TemporalField參數給get方法拿到同樣的信息。TemporalField接口定義了如何訪問temporal對象某個字段的值。ChronoField枚舉實現TemporalField接口,可以使用get方法得到枚舉元素的值。
int year = localDate.get(ChronoField.YEAR);
int month = localDate.get(ChronoField.MONTH_OF_YEAR);
int day = localDate.get(ChronoField.DAY_OF_MONTH);
1.2 LocalTime
使用LocalTime類表示時間,可以使用of重載的兩個工廠方法創建LocalTime的實例。
第一個重載函數接收小時和分鐘
第二個重載函數同時還接收秒。
LocalTime類也提供了一些get方法訪問這些變量的值,如下所示。
LocalTime localTime = LocalTime.of(13, 45, 20);
int hour = localTime.getHour();
int minute = localTime.getMinute();
int second = localTime.getSecond();
System.out.println(String.format("hour:%s\nminute:%s\nsecond:%s", hour, minute, second));
打印結果:
hour:13
minute:45
second:20
LocalDate和LocalTime都可以通過解析代表它們的字符串創建。使用靜態方法parse可以實現:
LocalDate date = LocalDate.parse("2019-03-27");
LocalTime time = LocalTime.parse("20:17:08");
可以向parse方法傳遞一個DateTimeFormatter。該類的實例定義了如何格式化一個日期或者時間對象。用來替換老版java.util.DateFormat。
如果傳遞的字符串參數無法被解析為合法的LocalDate或LocalTime對象,這兩個parse方法都會拋出一個繼承自RuntimeException的DateTimeParseException異常。
2. 合并日期和時間
復合類LocalDateTime,是LocalDate和LocalTime的合體。它同時表示了日期和時間,不帶有時區信息。可以直接創建,也可以通過合并日期和時間對象構造。
LocalTime time = LocalTime.of(21, 31, 50);
LocalDate date = LocalDate.of(2019, 03, 27);
LocalDateTime dt1 = LocalDateTime.of(2017, Month.NOVEMBER, 07, 22, 31, 51);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(22, 21, 14);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
創建LocalDateTime對象
直接創建
通過atTime方法向LocalDate傳遞一個時間對象
通過atDate方法向LocalTime傳遞一個時間對象
也可以使用toLocalDate或者toLocalTime方法,從LocalDateTime中提取LocalDate或者LocalTime組件:
LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();
3. 機器的日期和時間格式
從計算機的角度來看,"2019年03月27日11:20:03"這樣的方式是不容易理解的,計算機更加容易理解建模時間最自然的格式是表示一個持續時間段上某個點的單一大整型數。新的java.time.Instant類對時間建模的方式,基本上它是以Unix元年時間(傳統的設定為UTC時區1970年1月1日午夜時分)開始所經歷的秒數進行計算。
3.1 創建Instant
靜態工廠方法ofEpochSecond傳遞一個代表秒數的值創建一個該類的實例。
靜態工廠方法ofEpochSecond還有一個增強的重載版本,它接收第二個以納秒為單位的參數值,對傳入作為秒數的參數進行調整。重載的版本會調整納秒參數,確保保存的納秒分片在0到999 999999之間。
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
// 2 秒之后再加上100萬納秒(1秒)
Instant.ofEpochSecond(2, 1_000_000_000);
// 4秒之前的100萬納秒(1秒)
Instant.ofEpochSecond(4, -1_000_000_000);
3.2 工廠方法now
Instant類也支持靜態工廠方法now,它能夠獲取當前時刻的時間戳。
Instant now = Instant.now();
System.out.println(now);
2019-03-27T03:26:39.451Z
Instant的設計初衷是為了便于機器使用,它包含的是由秒及納秒所構成的數字。因此Instant無法處理那些我們非常容易理解的時間單位。
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);
它會拋出下面這樣的異常:
Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth
但是你可以通過Duration和Period類使用Instant,接下來我們會對這部分內容進行介紹。
4. Duration和Period
4.1 Duration
所有類都實現了Temporal接口,該接口定義如何讀取和操縱為時間建模的對象的值。如果需要創建兩個Temporal對象之間的duration,就需要Duration類的靜態工廠方法between。
可以創建兩個LocalTimes對象、兩個LocalDateTimes對象,或者兩個Instant對象之間的duration:
LocalTime time1 = LocalTime.of(21, 50, 10);
LocalTime time2 = LocalTime.of(22, 50, 10);
LocalDateTime dateTime1 = LocalDateTime.of(2019, 03, 27, 21, 20, 40);
LocalDateTime dateTime2 = LocalDateTime.of(2019, 03, 27, 21, 20, 40);
Instant instant1 = Instant.ofEpochSecond(1000 * 60 * 2);
Instant instant2 = Instant.ofEpochSecond(1000 * 60 * 3);
Duration d1 = Duration.between(time1, time2);
Duration d2 = Duration.between(dateTime1, dateTime2);
Duration d3 = Duration.between(instant1, instant2);
// PT1H 相差1小時
System.out.println("d1:" + d1);
// PT2H 相差2小時
System.out.println("d2:" + d2);
// PT16H40M 相差16小時40分鐘
System.out.println("d3:" + d3);
LocalDateTime是為了便于人閱讀使用,Instant是為了便于機器處理,所以不能將二者混用。如果在這兩類對象之間創建duration,會觸發一個DateTimeException異常。
此外,由于Duration類主要用于以秒和納秒衡量時間的長短,你不能僅向between方法傳遞一個LocalDate對象做參數。
4.2 Period
使用Period類以年、月或者日的方式對多個時間單位建模。使用該類的工廠方法between,可以使用得到兩個LocalDate之間的時長。
Period period = Period.between(LocalDate.of(2019, 03, 7), LocalDate.of(2019, 03, 17));
// 相差10天
System.out.println("Period between:" + period);
Duration和Period類都提供了很多非常方便的工廠類,直接創建對應的實例。
Duration threeMinutes = Duration.ofMinutes(3);
Duration fourMinutes = Duration.of(4, ChronoUnit.MINUTES);
Period tenDay = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
Duration類和Period類共享了很多相似的方法,有興趣的可以參考官網的文檔。
截至目前,我們介紹的這些日期-時間對象都是不可修改的,這是為了更好地支持函數式編程,確保線程安全,保持領域模式一致性而做出的重大設計決定。
當然,新的日期和時間API也提供了一些便利的方法來創建這些對象的可變版本。比如,你可能希望在已有的LocalDate實例上增加3天。除此之外,我們還會介紹如何依據指定的模式,
比如dd/MM/yyyy,創建日期-時間格式器,以及如何使用這種格式器解析和輸出日期。
5. 操縱、解析和格式化日期
如果已經有一個LocalDate對象,想要創建它的一個修改版,最直接也最簡單的方法是使用withAttribute方法。withAttribute方法會創建對象的一個副本,并按照需要修改它的屬性。
// 這段代碼中所有的方法都返回一個修改了屬性的對象。它們都不會修改原來的對象!
LocalDate date1 = LocalDate.of(2017, 12, 15);
LocalDate date2 = date1.withYear(2019);
LocalDate date3 = date2.withDayOfMonth(25);
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);
它們都聲明于Temporal接口,所有的日期和時間API類都實現這兩個方法,它們定義了單點的時間,比如LocalDate、LocalTime、LocalDateTime以及Instant。更確切地說,使用get和with方法,我們可以將Temporal對象值的讀取和修改區分開。如果Temporal對象不支持請求訪問的字段,它會拋出一個UnsupportedTemporalTypeException異常,比如試圖訪問Instant對象的ChronoField.MONTH_OF_YEAR字段,或者LocalDate對象的ChronoField.NANO_OF_SECOND字段時都會拋出這樣的異常。
// 以聲明的方式操縱LocalDate對象,可以加上或者減去一段時間
LocalDate date1 = LocalDate.of(2014, 10, 19);
LocalDate date2 = date1.plusWeeks(1);
LocalDate date3 = date2.minusYears(3);
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);
與我們剛才介紹的get和with方法類似最后一行使用的plus方法也是通用方法,它和minus方法都聲明于Temporal接口中。通過這些方法,對TemporalUnit對象加上或者減去一個數字,我們能非常方便地將Temporal對象前溯或者回滾至某個時間段,通過ChronoUnit枚舉我們可以非常方便地實現TemporalUnit接口。
6. 使用TemporalAdjuster
有時需要進行一些更加復雜的操作,比如,將日期調整到下個周日、下個工作日,或者是本月的最后一天。可以使用重載版本的with方法,向其傳遞一個提供了更多定制化選擇的TemporalAdjuster對象,更加靈活地處理日期。
// 對于最常見的用例,日期和時間API已經提供了大量預定義的TemporalAdjuster。可以通過TemporalAdjuster類的靜態工廠方法訪問。
LocalDate date1 = LocalDate.of(2013, 12, 11);
LocalDate date2 = date1.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
LocalDate date3 = date2.with(TemporalAdjusters.lastDayOfMonth());
使用TemporalAdjuster可以進行更加復雜的日期操作,方法的名稱很直觀。如果沒有找到符合預期的預定義的TemporalAdjuster,可以創建自定義的TemporalAdjuster。TemporalAdjuster接口只聲明一個方法(即函數式接口)。實現該接口需要定義如何將一個Temporal對象轉換為另一個Temporal對象,可以把它看成一個UnaryOperator。
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
7. 打印輸出及解析日期-時間對象
新的java.time.format包就是特別為格式化以及解析日期-時間對象而設計的。其中最重要的類是DateTimeFormatter。創建格式器最簡單的方法是通過它的靜態工廠方法以及常量。所有的DateTimeFormatter實例都能用于以一定的格式創建代表特定日期或時間的字符串。
LocalDate date = LocalDate.of(2013, 10, 11);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
20131011
2013-10-11
通過解析代表日期或時間的字符串重新創建該日期對象,也可以使用工廠方法parse重新創建。
LocalDate date2 = LocalDate.parse("20141007", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date3 = LocalDate.parse("2014-10-07", DateTimeFormatter.ISO_LOCAL_DATE);
DateTimeFormatter實例是線程安全的,老的java.util.DateFormat線程不安全。單例模式創建格式器實例,在多個線程間共享實例是沒有問題的。也可以通過ofPattern靜態工廠方法,按照某個特定的模式創建格式器。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
String formattedDateStr = date.format(formatter);
LocalDate date1 = LocalDate.parse(formattedDateStr, formatter);
ofPattern方法也提供了一個重載的版本,可以傳入Locale創建格式器。
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
LocalDate date = LocalDate.of(2015, 11, 14);
String formattedDate = date.format(italianFormatter);
LocalDate date1 = LocalDate.parse(formattedDate, italianFormatter);
DateTimeFormatterBuilder類還提供了更復雜的格式器,以提供更加細粒度的控制。同時也提供非常強大的解析功能,比如區分大小寫的解析、柔性解析、填充,以及在格式器中指定可選節等等。
通過DateTimeFormatterBuilder自定義格式器
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(". ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.YEAR)
.parseCaseInsensitive()
.toFormatter(Locale.ITALIAN);
LocalDate now = LocalDate.now();
String s = now.format(italianFormatter);
8. 處理不同的時區和歷法
新版日期和時間API新增加的重要功能是時區的處理。新的java.time.ZoneId類替代老版java.util.TimeZone。跟其他日期和時間類一樣,ZoneId類也是無法修改的。是按照一定的規則將區域劃分成的標準時間相同的區間。在ZoneRules這個類中包含了40個時區實例,可以通過調用ZoneId的getRules()得到指定時區的規則,每個特定的ZoneId對象都由一個地區ID標識。
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
Java 8的新方法toZoneId將一個老的時區對象轉換為ZoneId。地區ID都為“{區域}/{城市}”的格式,地區集合的設定都由英特網編號分配機構(IANA)的時區數據庫提供。
ZoneId zoneId = TimeZone.getDefault().toZoneId();
ZoneId對象可以與LocalDate、LocalDateTime或者是Instant對象整合構造為成ZonedDateTime實例,它代表了相對于指定時區的時間點。
LocalDate date = LocalDate.of(2019, 03, 27);
ZonedDateTime zdt1 = date.atStartOfDay(shanghaiZone);
LocalDateTime dateTime = LocalDateTime.of(2015, 12, 21, 11, 11, 11);
ZonedDateTime zdt2 = dateTime.atZone(shanghaiZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(shanghaiZone);
通過ZoneId,你還可以將LocalDateTime轉換為Instant:
LocalDateTime dateTime = LocalDateTime.of(2016, 10, 14, 15, 35);
Instant instantFromDateTime = dateTime.toInstant(shanghaiZone);
你也可以通過反向的方式得到LocalDateTime對象:
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, shanghaiZone);
另一種比較通用的表達時區的方式是利用當前時區和UTC/格林尼治的固定偏差。使用ZoneId的一個子類ZoneOffset,表示的是當前時間和倫敦格林尼治子午線時間的差異:
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
9. 總結
Java 8之前老版的java.util.Date類以及其他用于建模日期時間的類有很多不一致及設計上的缺陷,包括易變性以及糟糕的偏移值、默認值和命名。
新版的日期和時間API中,日期-時間對象是不可變的。
新的API提供了兩種不同的時間表示方式,有效地區分了運行時人和機器的不同需求。
你可以用絕對或者相對的方式操縱日期和時間,操作的結果總是返回一個新的實例,老的日期時間對象不會發生變化。
TemporalAdjuster讓你能夠用更精細的方式操縱日期,不再局限于一次只能改變它的一個值,并且你還可按照需求定義自己的日期轉換器。
你現在可以按照特定的格式需求,定義自己的格式器,打印輸出或者解析日期?時間對象。這些格式器可以通過模板創建,也可以自己編程創建,并且它們都是線程安全的。
你可以用相對于某個地區/位置的方式,或者以與UTC/格林尼治時間的絕對偏差的方式表示時區,并將其應用到日期?時間對象上,對其進行本地化。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。