Java8 LocalDateTime と Java6 Date で最小値の取り扱いが難しい


JSON を経由して、Java8 LocalDateTime と、Java6 Date で、シリアライズ・デシリアライズをしようとしたときに表題の件でハマったのでメモ。

イメージとしては、プロジェクトをまたいで、以下の様なことをしようとしました。

Java8 DateTime -> (Serialize) -> JSON -> (Deserialize) -> Java6 Date

すると、 LocalDateTime と、Date での値の取りうる範囲の違いがあらわれてきます。具体的に何が起きるかというと、LocalDateTime.MIN などの Date で表現できない範囲の値をもったものをデシリアライズしたとき、もとの値が失われてしまいます。

より具体的には、以下の実行結果のようになります。

import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Date
import java.text.SimpleDateFormat

fun main(args: Array<String>) {
    val formatter = SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss")

    val minLocalDateTime = formatter.format(formatter.parse(
        DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.MIN)))
    val minDateText = formatter.format(Date(Long.MIN_VALUE))
    val zeroDateText = formatter.format(Date(0L))

    val zeroEpocLocalDateTime = formatter.format(formatter.parse(
        DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(
            LocalDateTime.ofEpochSecond(0L, 0, ZoneOffset.UTC))))

println (
"""Java8 DateTime | LocalDateTime.MIN     | ${LocalDateTime.MIN}
Java6 Date     | LocalDateTime.MIN     | $minLocalDateTime
Java6 Date     | Date(Long.MIN_VALUE)  | $minDateText
Java6 Date     | Date(0L)              | $zeroDateText
Java8 DateTime | ofEpochSecond(0L, 0,) | $zeroEpocLocalDateTime
""")
}

実行結果。

Java8 DateTime | LocalDateTime.MIN     | -999999999-01-01T00:00
Java6 Date     | LocalDateTime.MIN     | 169087565-03-15T04:51:43
Java6 Date     | Date(Long.MIN_VALUE)  | 292269055-12-02T04:47:04
Java6 Date     | Date(0L)              | 1970-01-01T12:00:00
Java8 DateTime | ofEpochSecond(0L, 0,) | 1970-01-01T12:00:00

実行結果でもすでに触れていますが、この差分を吸収するには、エポックタイムを利用します。
LocalDateTime を使用する側の最小値を、エポックタイムでの最小値に定めてしまうことで、互換性を維持するというわけです。

LocalDateTime.ofEpochSecond(0L, 0, ZoneOffset.UTC) を使用することで、エポックタイムの最小値を得られるので、LocalDateTime を使用する側では、これを使用すれば、Java6 を使用しているプロジェクトとの互換を取ることができるようになります。