Java时间和日期指南
長期以來,正確處理日期,時間,時區,夏時制,and年等一直是我的煩惱。 本文并不是一個全面的指南時域,請參閱日期和時間在Java中 -更詳細,但略有下降,ekhem,日期。 它仍然是相關的,但是沒有涵蓋Java 8中的java.time 。我想介紹每個初級Java開發人員都應該意識到的絕對最低要求。
事件何時發生?
除了哲學和量子物理學,我們還可以將時間視為一維度量標準,即實數值。 隨著時間的流逝,該值將不斷增長。 如果一個事件接連發生,我們將為該事件分配更多時間。 同時發生的兩個事件具有相同的時間值。 由于計算機系統中的實際原因,我們將時間存儲為離散整數,這主要是因為計算機時鐘離散地滴答。 因此,我們可以將時間存儲為整數值。 按照慣例,我們將time = 0分配給1970年1月1日,但是在Java中,該值每毫秒遞增一次,而不是像UNIX time那樣每秒遞增。 歷史上在UNIX時間中使用32位帶符號整數將導致2038年問題 。 因此,Java將時間存儲在64位整數中,即使您將其增加一千次也足夠了。 話雖這么說,但用Java存儲時間的最簡單但有效的方法是…… long原始語言:
long timestamp = System.currentTimeMillis();long存在的問題是,它太普遍了,以至于使用它來存儲時間會破壞類型系統。 它可以是ID,可以是哈希值,可以是任何值。 而且long沒有與時域相關的任何有意義的方法。 從Java 1.0開始就知道最早包裝long有意義的對象的方法是java.util.Date :
Date now = new Date();Date類具有許多缺陷:
有沒有想過為什么您的團隊中老舊且脾氣暴躁的開發人員對不變性如此興奮? 想象一下一段代碼,它將任何一分鐘添加到Date 。 簡單吧?
Date addOneMinute(Date in) {in.setTime(in.getTime() + 1_000 * 60);return in; }看起來不錯吧? 所有測試用例都通過了,因為在測試代碼后,到底誰會驗證輸入參數是否完整?
Date now = new Date(); System.out.println(now); System.out.println(addOneMinute(now)); System.out.println(now);輸出可能如下所示:
Tue Jul 26 22:59:22 CEST 2016 Tue Jul 26 23:00:22 CEST 2016 Tue Jul 26 23:00:22 CEST 2016你有沒有注意到now值將在一分鐘后實際上改變了嗎? 當您有一個使用Date并返回Date的函數時,您將永遠不會期望它修改其參數! 這就像具有一個接受x和y數字并重新調整它們的總和的函數一樣。 如果您發現x在加法過程中以某種方式被修改,則所有假設都將被破壞。 順便說一下,這就是java.lang.Integer不可變的原因。 或String 。 或BigDecimal 。
這不是人為的例子。 想象一個帶有單個方法的ScheduledTask類:
class ScheduledTask {Date getNextRunTime(); }如果我說:
ScheduledTask task = //... task.getNextRunTime().setTime(new Date());更改返回的Date是否對下一次運行時間有效? 還是ScheduledTask返回了您可以自由修改的內部狀態的副本? 也許我們會讓ScheduledTask處于某種不一致的狀態? 如果Date是不可變的,就不會出現這樣的問題。
有趣的是,如果您將Java與JavaScript混淆,那么每個Java開發人員都會大怒。 但是,請猜測一下, JavaScript中的Date具有與java.util.Date完全相同的缺陷,并且似乎是復制粘貼的不良示例。 JavaScript中的Date是可變的,具有誤導性的toString()并且不支持任何時區。
Date一個很好的替代方法是java.time.Instant 。 它恰如其名地做到了:及時存儲時間。 Instant沒有日期或日歷相關的方法,它的toString()在UTC時區使用熟悉的ISO格式(稍后會詳細介紹),最重要的是:它是不可變的。 如果您想記住特定事件何時發生, Instant是使用純Java所能獲得的最好的結果:
Instant now = Instant.now(); Instant later = now.plusSeconds(60);注意, Instant沒有plusMinutes() , plusHours()等。 分鐘,小時和天數是與日歷系統相關的概念,而“ Instant在地理和文化上均不可知。
具有
有時您確實需要即時的人為表示。 這包括月份,星期幾,當前時間等。 但這是一個主要的復雜問題:日期和時間因國家和地區而異。 Instant既簡單又通用,但對人類不是很有用,它只是一個數字。 如果您具有與日歷相關的業務邏輯,例如:
- …必須在辦公時間內發生…
 - 最多一天
 - ……兩個工作日……
 - …有效期長達一年…
 - …
 
那么您必須使用某些日歷系統。 java.time.ZonedDateTime是絕對糟糕的java.util.Calendar的最佳替代方案。 實際上, java.util.Date和Calendar在設計上已被破壞,以至于它們在JDK 9中被完全棄用 。您只能通過提供時區ZonedDateTime從Instant創建ZonedDateTime 。 否則,將使用您無法控制的默認系統時區。 在不提供顯式ZoneId情況下以任何方式將Instant轉換為ZonedDateTime可能是一個錯誤:
Instant now = Instant.now(); System.out.println(now);ZonedDateTime dateTime = ZonedDateTime.ofInstant(now,ZoneId.of("Europe/Warsaw"));System.out.println(dateTime);輸出如下:
2016-08-05T07:00:44.057Z 2016-08-05T09:00:44.057+02:00[Europe/Warsaw]請注意,“ Instant (為方便起見)以UTC格式顯示日期,而ZonedDateTime使用提供的ZoneId (夏季+2小時,以后更多)。
日歷誤解
關于時間和日歷有很多誤解和神話。 例如,有些人認為兩個位置之間的時差始終是恒定的。 至少有兩個原因不成立。 首先是夏令時,又稱夏令時:
LocalDate localDate = LocalDate.of(2016, Month.AUGUST, 5); LocalTime localTime = LocalTime.of(10, 21); LocalDateTime local = LocalDateTime.of(localDate, localTime); ZonedDateTime warsaw = ZonedDateTime.of(local, ZoneId.of("Europe/Warsaw"));ZonedDateTime sydney = warsaw.withZoneSameInstant(ZoneId.of("Australia/Sydney"));System.out.println(warsaw); System.out.println(sydney);輸出顯示華沙和悉尼之間的時差恰好是8小時:
2016-08-05T10:21+02:00[Europe/Warsaw] 2016-08-05T18:21+10:00[Australia/Sydney]還是? 將8月更改為2月,相差為10小時:
2016-02-05T10:21+01:00[Europe/Warsaw] 2016-02-05T20:21+11:00[Australia/Sydney]這是因為華沙在2月(冬季)不進行夏令時,而在悉尼是夏季,因此他們使用DST(+1小時)。 反之亦然。 為了使事情變得更加復雜,切換到DST的時間有所不同,并且總是在當地時間的晚上,因此必須有一個國家已經切換但另一個沒有切換的時間,例如10月:
2016-10-05T10:21+02:00[Europe/Warsaw] 2016-10-05T19:21+11:00[Australia/Sydney]相差9小時。 時間偏移不同的另一個原因是政治上的:
LocalDate localDate = LocalDate.of(2014, Month.FEBRUARY, 5); LocalTime localTime = LocalTime.of(10, 21); LocalDateTime local = LocalDateTime.of(localDate, localTime); ZonedDateTime warsaw = ZonedDateTime.of(local, ZoneId.of("Europe/Warsaw"));ZonedDateTime moscow = warsaw.withZoneSameInstant(ZoneId.of("Europe/Moscow"));System.out.println(warsaw); System.out.println(moscow);2014年2月5日華沙與莫斯科之間的時差為3小時:
2014-02-05T10:21+01:00[Europe/Warsaw] 2014-02-05T13:21+04:00[Europe/Moscow]但是一年后的同一天的差異是2小時:
2015-02-05T10:21+01:00[Europe/Warsaw] 2015-02-05T12:21+03:00[Europe/Moscow]那是因為俄羅斯瘋狂地改變了他們的DST政策和時區。
關于日期的另一個常見誤解是一天是24小時。 這又與夏時制有關:
LocalDate localDate = LocalDate.of(2017, Month.MARCH, 26); LocalTime localTime = LocalTime.of(1, 0); ZonedDateTime warsaw = ZonedDateTime.of(localDate, localTime, ZoneId.of("Europe/Warsaw"));ZonedDateTime oneDayLater = warsaw.plusDays(1);Duration duration = Duration.between(warsaw, oneDayLater); System.out.println(duration);您知道嗎,2017年3月26日凌晨1點與27點之間的差是……23小時( PT23H )。 但是,如果您將時區更改為Australia/Sydney您會在24小時內熟悉,因為那天悉尼沒有什么特別的事情發生。 悉尼那個特別的日子恰好是2017年4月2日:
LocalDate localDate = LocalDate.of(2017, Month.APRIL, 2); LocalTime localTime = LocalTime.of(1, 0); ZonedDateTime warsaw = ZonedDateTime.of(localDate, localTime, ZoneId.of("Australia/Sydney"));結果是一天等于…25小時。 但不在距悉尼以北一千公里的布里斯班( "Australia/Brisbane" )中,這里沒有DST。 為什么所有這些都很重要? 當您與客戶達成協議,假設某件商品需要一天而不是24小時才能完成時,這實際上可能會在某天產生很大的變化。 您必須非常精確,否則系統將每年兩次變得不一致。 而且不要讓我一second而就 。
在這里要學習的課程是,每次輸入日歷域時,都必須考慮時區。 有一些使用默認系統時區的便捷方法,但是在云環境中,您可能無法對此進行控制。 默認字符編碼也是如此,但是情況有所不同。
儲存和傳輸時間
默認情況下,您應該以時間戳( long值)或ISO 8601的形式存儲和發送時間,這基本上是Instant.toString()根據文檔進行的操作。 最好使用long值,因為它更緊湊,除非您需要在某些文本編碼(例如JSON)中需要更具可讀性的格式。 時區不可知還long因此您不假裝發送/存儲的時區具有任何意義。 這既適用于傳輸時間,又適用于將其存儲在數據庫中。
在某些情況下,您可能希望發送完整的日歷信息,包括時區。 例如,當您構建一個聊天應用程序時,如果您的朋友居住在不同的時區,您可能想告訴客戶發送消息的本地時間是什么。 否則,您會知道它是在您的時間上午10點發送的,但是您朋友所在的時間是幾點呢? 另一個例子是機票預訂網站。 您想告訴客戶何時飛機起飛并到達當地時間,只有服務器知道起飛和到達目的地的確切時區。
當地時間和日期
有時,您希望表達沒有特定時區的日期或時間。 例如我的生日是:
//1985-12-25 LocalDate.of(1985, Month.DECEMBER, 25)無論我身在何處,我都會慶祝生日。 這意味著聚會將從大約開始:
//20:00 LocalTime.of(20, 0, 0)與時區無關。 我什至可以說我今年的生日聚會正好在:
//2016-12-25T20:00 LocalDateTime party = LocalDateTime.of(LocalDate.of(2016, Month.DECEMBER, 25),LocalTime.of(20, 0, 0) );但是,只要我不為您提供位置,您就不會知道我所居住的時區,因此實際的開始時間是什么。 在沒有給出時區的情況下將LocalDateTime轉換為Instant或ZonedDateTime (這兩者都指向精確的時間點)是不可能的(或者非常愚蠢)。 因此,當地時間很有用,但實際上并不能代表任何時刻。
測試中
我只是弄清楚了陷阱的表面,以及一個約會可能會帶來的問題。 例如,我們沒有涵蓋leap年,which年可能會成為嚴重的錯誤來源。 我發現在測試日期時, 基于屬性的測試非常有用:
import spock.lang.Specification import spock.lang.Unrollimport java.time.*class PlusMinusMonthSpec extends Specification {static final LocalDate START_DATE =LocalDate.of(2016, Month.JANUARY, 1)@Unrolldef '#date +/- 1 month gives back the same date'() {expect:date == date.plusMonths(1).minusMonths(1)where:date << (0..365).collect {day -> START_DATE.plusDays(day)}}}此測試可確保在2016年的任何日期加上或減去一個月會返回相同的日期。 很簡單吧? 該測試失敗了幾天:
date == date.plusMonths(1).minusMonths(1) | | | | | | | | 2016-02-29 2016-01-29 | | 2016-01-30 | false 2016-01-30date == date.plusMonths(1).minusMonths(1) | | | | | | | | 2016-02-29 2016-01-29 | | 2016-01-31 | false 2016-01-31date == date.plusMonths(1).minusMonths(1) | | | | | | | | 2016-04-30 2016-03-30 | | 2016-03-31 | false 2016-03-31...years年會引起各種問題,并破壞數學定律。 另一個類似的示例是,向日期添加兩個月并不總是等于兩次添加一個月。
摘要
我們再一次勉強刮傷了表面。 如果我只想從本文中學到一件事,請注意時區!
翻譯自: https://www.javacodegeeks.com/2016/08/guide-time-date-java.html
總結
以上是生活随笔為你收集整理的Java时间和日期指南的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 食堂备案有哪些(食堂备案)
 - 下一篇: scrapy立面parse_立面设计模式