終於,JDK8正式釋出,當中帶給了Java開發者新的日期時間處理API,也帶來程式開發時對日期時間處理最重要的一些思考。例如:在需要處理時間日期時,你應該採取哪個觀點?是機器的時間觀?還是人的時間觀?你的時間觀點與我的時間觀一致嗎?

其實無論使用哪種技術或程式庫,都該謹慎地思考不同場合該採用的時間觀點,才能避免衝突。

機器的連續時間觀

Java領域中,時間處理API歷經Date、Calendar、JodaTime與JSR310這幾個重要的程式庫,我在〈從JDK時間API演進看時間處理〉談過它們的演進。

無論是Date本身職責的轉變,或者是JodaTime、JSR310中剛好都有個同名的Instant類別,都在突顯一件事,機器本身需要有明確不糢糊的時間觀點,它有明確的紀元起點(epoch)與度量單位,大多數機器與程式庫,採取Unix epoch,也就是UTC時間1970年1月1日0時0分0秒起算至某個時間點歷經的毫秒數,俗稱epoch毫秒數,為了明確定義,JSR310也定了Java epoch為1970年1月1日0時0分0秒。

Java標準API中的Date一開始就混淆了機器的時間觀與人的時間觀,每個Date實例其實只內含epoch毫秒數,然而本身無法更換時區資訊卻又肩負轉換日期欄位的職責,後續藉著廢棄諸多方法,希望讓它單純代表機器的時間觀,然而Date這個名稱卻容易讓開發者誤會它代表日期,甚至相信它的toString方法傳回字串對時間的描述,而混淆機器與人類時間觀點會引發的問題之一,像是日光節約時間。例如:

calendar.set(1975, Calendar.MARCH, 31, 23, 59, 59); 
calendar.add(Calendar.SECOND, 1);     
        // 增加一秒
out.println(calendar.getTime().toString());   // Tue Apr 01 01:00:00 CDT 1975

臺灣時間1975年3月31日23時59分59秒的下一秒,是4月1日1時0分0秒,由於臺灣已經不實施日光節約時間了,許多開發者並不知道過去有過日光節約時間,這個結果就造成了他們的困惑。如果你取得Date實例,下一步該獲取時間資訊應該是透過Date的getTime()取得epoch毫秒數,如此方不致於混淆。例如臺灣時間1975年3月31日23時59分59秒的epoch毫秒數是165513599484,下一秒的毫秒數就是165513600484,機器觀點來看是連續無誤的時間。

為了避開開發者混淆,JodaTime或JSR310都試著以明確的類別名稱Instant,來表示機器的時間觀。

人類模糊的時間觀

現在你知道臺灣時間1975年4月1日0時0分0秒實際上是不存在的,然而,若使用calendar.set(1975, Calendar.APRIL, 1, 0, 0, 0)設定時,並不會拋出錯誤,而是自動轉換為epoch毫秒數165513600484,這機器時間轉為臺灣時間描述的話,實際上是1975年4月1日1時0分0秒,如果開發者使用Calendar的get(Calendar.HOUR)取回時數是1時,而他並不知道臺灣過去有過日光節約時間,就又感到困惑了,原因之一是Calendar也混淆了機器與人類的時間觀。

原因之二是人類對時間的觀點其實大多是模糊的,通常不會精確到秒,有時需要年月日,有時需要時分秒,有時需要年月日時分秒這種更完整的資訊。

方才的例子中,也許你只是想設定臺灣時間1975年4月1日,不過,你不能只撰寫calendar.set(1975, Calendar.APRIL, 1),因為時分秒的部份,會自動從系統目前時鐘來獲得補齊。JSR310中考慮到這種需求,可以使用LocalDate.of(1975, 4, 1)來表示,而LocalTime可以只用來表示時分秒,LocalDateTime則可以表示年月日時分秒。

同樣地,LocalDate、LocalTime、LocalDateTime名稱上明確表示出,它們只是本地對時間的一個描述,符合人類多數情況下,描述時間並不會特別聲明時區的需求,如果你要明確地表示臺灣時間1975年4月1日,在JSR310中應當使用ZonedDateTime,由於不同時區會有日光節約時間、偏移量調整問題,ZonedDateTime至少要指定至分的精確度。例如ZonedDateTime.of(LocalDateTime.of(1975, 4, 1, 1, 0), ZoneId.of("Asia/Taipei"))。

你的時間觀與我的時間觀

在臺灣,如果用戶輸入了1975年4月1日0時0分0秒,該怎麼處理比較好?回答前,先看另一個問題,如果有用戶在非閏年輸入了2月29日,該怎麼處理比較好?警告他這個時間不存在?直接以字串描述存下來?轉成epoch數?這兩個問題都是輸入的時間不存在,那麼你會選擇的答案各是為何?如果你對非閏年輸入2月29日時提出警告,對於輸入臺灣時間1975年4月1日0時0分0秒,也許就不能默不吭聲。

你的時間觀與我的時間觀不一致時,就會造成誤會。例如,你不知道臺灣沒有1975年4月1日0時0分0秒,就會誤認為calendar.set(1975, Calendar.APRIL, 1, 0, 0, 0)之後,透過get(Calendar.HOUR)取回時數為1是錯的,我知道有這回事,就會知道那是對的!你我都知道非閏年不該輸入2月29日,如果真的輸入了這個日期而被校正,你我就都能欣然接受這個結果,這說明了採取一致的時間觀很重要!

不一致的時間觀造成的誤會,有那麼嚴重嗎?可能會很嚴重!Facebook粉絲團的成立時間在顯示時分的部份,並沒有考量到時區,臺灣時間如果是2014年4月11日晚上8:37建立了粉絲團,網頁上顯示的專頁成立時間卻會是2014年4月11日早上5:37分,如果根據網頁原始碼中data-utime記錄的時間戳記1397219825,轉換為臺灣時間顯示確實是晚上8:37,然而這個因Bug導致的時間差,在學運期間造成了非常大的誤會。

如果允許用戶自行輸入時間,務必確認你們之間的時間觀一致,使用一個可信的日期時間選擇器(Datetime picker),只列出真正存在的日期與時間讓用戶挑選,不要讓客戶有機會自行輸入一個不存在的時間,會是比較好的作法。

雖然人類描述時間通常不會特別聲明時區,不過未聲明多半就是指當時所在時區,因此,如果系統在臺灣運行,記得使用LocalDate、LocalTime、LocalDateTime時,都暗示著時區就是在臺灣本地(Local),指定的時間應該也要是臺灣真正存在的時間,因此仍需避免LocalDateTime.of(1975, 4, 1, 0, 0)的情況,實際上你使用ZonedDateTime.of(LocalDateTime.of(1975, 4, 1, 0, 0), ZoneId.of("Asia/Taipei"))時,結果還是會被校正為臺灣時間1975年4月1日1時0分0秒。

你要哪種時間觀?

如果你的用戶可以自行輸入時間,那你應該採用人類觀點的時間,像是LocalDate、LocalTime、LocalDateTime,或者視需求更明確地採取ZonedDateTime,如果時間是由系統生成的呢?例如留言版在送出之後,由系統自動記錄留言時間,那麼應該採用機器觀點的時間,ingramchen曾經發表〈Java 8 LocalDateTime vs Instant〉就談到:「如果時間點沒有人為的介入,例如常見的建立時間、最後修改時間,或是到期時間等等,這時你的Model應該要使用Instant」。

實際上可採用時間觀不只這些,現在的時間程式庫多半夠聰明,可以在時間觀不同時以予以適當校正,像是這邊談到程式庫對日光節約時間的處理,然而有時程式庫也無法決定該如何處理,這時你就得決定採用哪個時間觀,yorkxin在〈關於Ruby TZInfo Gem、TZ Database與Ambiguous Time的研究〉就談到1895-12-31 23:54:00至1895-12-31 23:59:59有六分鐘無法轉換為UTC時間問題,你得自行決定要哪個UTC時間!無論如何要記得,時間處理沒有你想像中單純,確認要採用哪種時間觀,瞭解程式庫支援程度,才不致於踩到時間地雷。

專欄作者

熱門新聞

Advertisement