Javaの日付の扱い~LocalDateTime系とInstantを使い分ける~(Android)

Javaには日付を扱う型にはさまざまあります。どれを使ったらいいのでしょう。

今回は使い分けを自分なりにまとめました。

なおkotlinx.datetimeについては今回扱いません。
こちらはKMPやシリアライズで使われるKotlin製の日付型でありますが、Javaの型をKotlinで使えるようにしたものですので、考え方は基本同じだと思っています。

またAndroidアプリの開発で使うことを前提としています。

## java.util.Dateやjava.util.Calendarは基本使わない
Dateは一部のメソッドが非推奨となっています。
> The corresponding methods in Date are deprecated.
>
[Date \(Java SE 11 & JDK 11 \)](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Date.html)

DateやCalendarはレガシーとされており、APIも使いづらいです。
古いクラスとして、新規開発では基本使わないでおきましょう。
古いシステムでは`Date`や`Calendar`が使われているので、互換性をもつために使う、というのに限定しましょう。

また、DateFormatには`SimpleDateFormat`ではなく、`java.time.format.DateTimeFormatter`や`java.time.format.DateTimeFormatterBuilder`を使うとよいでしょう。


## java.time.LocalDateTime(と関連API)、java.time.Instantを使う
こちらは新しく作られた使いやすいAPIです。こちらを使いましょう。

`LocalDateTime`に関連するものとして、`LocalTime`や`LocalDate`があります。
これらは簡単で、単純に`LocalDateTime`の時間の部分と、日付の部分です。

また`ZonedDateTime`や、`OffsetDateTime`があります。`ZonedDateTime`と`OffsetDateTime`に関しては、`Instant`との違いを見ながら、説明していきます。

## LocalDateTimeとInstantの違い
こちらのStack Overflowの回答がすごくわかりやすいです。
[java \- What's the difference between Instant and LocalDateTime? \- Stack Overflow](https://stackoverflow.com/questions/32437550/whats-the-difference-between-instant-and-localdatetime)

回答の画像がわかりやすい。

![java \- What's the difference between Instant and LocalDateTime? \- Stack Overflow](https://i.sstatic.net/QPhGW.png)

`Instant`はタイムラインの一点を表します。タイムラインは絶対時間を表します。宇宙を流れる時間軸の一点を指します。（相対性理論とかは考えないでおきます。）例えば、聖徳太子が誕生した日はタイムラインの一点のはずです。`Instant`はクラス内部でエポック秒(1721964060742みたいなの)を保持しています。

一方`LocalDateTime`は絶対時間を表せません。地球上のどこにいるかによって、別の時間を指すことになります。

年月日時刻を保持するクラスであり、タイムゾーンもありません。
java docにも明記されています。
> A date-time without a time-zone in the ISO-8601 calendar system, such as 2007-12-03T10:15:30.
>
[LocalDateTime \(Java Platform SE 8 \)](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html)

これはどういうことかというと、カレンダーや時計を見たときに、表示されている年月日と、時刻を表すものになります。

イギリスと日本でカレンダーと時計を見る時、同じ値を見るには、時差があるのでイギリスは絶対時間で9時間経過後に見ないといけません。
逆に絶対時間で同時にイギリスと日本で`LocalDateTime.now()`をやると時差分だけ離れた別の値が出ます。

使い道として、日の出の時刻や、クリスマス、誕生日などを表すのに良いと思います。
クリスマス0時にイベントを起こしたい時、などに役に立つと思います。
Stack Overflowの回答を読んでみるとより詳しく理解できると思います。


## LocalDateTimeとZonedDateTime、OffsetDateTimeの違い
`ZonedDateTime`は`LocalDateTime`にタイムゾーンを加えたものになります。タイムゾーンが含まれるので、絶対時間を表すことができます。
`ZonedDateTime`は日本やイギリスなどのタイムゾーンを加えた概念で、`OffsetDateTime`はUTC(標準時)からの時差を加えた概念です。`OffsetDateTime`も絶対時間を表すことができます。

まとめると、

クラス|内容
--|--
Instant|宇宙を流れる時間軸の一点で、エポック秒を保持する
LocalDateTime|年月日付時刻を単に保有するクラスで、タイムゾーンはなし。宇宙を時間軸の一点を表さない。
ZonedDateTime、OffsetDateTime|LocalDateTimeにタイムゾーンや時差を加えたもので、宇宙を流れる時間軸の一点を表すことができる



## LocalDateTimeとInstantの使い分け
ここでは各クラスをどう使い分けていくかを見ていきます。

⚠ どれを使うかはプロジェクトや状況に応じて判断していくものになると思います。また以下は現時点での自分の考えであり、正直確たる自信もありません。ご指摘やご意見あればお願いします🙇

### 絶対時間を扱うビジネスロジックにはInstantを使う
例えば、タイマーアプリなど絶対時間をクライアントで扱う場合や、サーバーサイドで時間を処理する場合は、`Instant`を使うとスッキリすると思います。

[wear osのサンプルアプリ](https://github.com/android/wear-os-samples/blob/main/DataLayer/Application/src/main/java/com/example/android/wearable/datalayer/MainActivity.kt)でも`Instant`が使われていました。

タイマーだと設定した後タイムゾーンを変更しても鳴動時間は変わらないはずなので、絶対時間を使うべきだと思います。

また記事を作成した時刻、を記録する際も、これは絶対時間で記録しておくべきです。`OffsetDateTime`や`ZonedDateTime`も可能ですが、タイムゾーンの概念は必要ないので`Instant`がスッキリするでしょう。

### タイムゾーンのない年月日日付時刻を扱う場合はLocalDateTimeを使う
アラームを設定した後タイムゾーンを跨いだ場合、いつアラームを鳴らしたいか考えると良いと思います。

日本で7時にアラームを設定した後、イギリスのタイムゾーンに変更した場合、おそらく7時にアラームがなってほしいと思います。

ここで重要なのは絶対時間ではなく時刻なので、LocalDateTimeが適していると思います。

### 絶対時刻をDBに保存する場合
この場合は`Instant`やutcの`ZonedDateTime`、`OffsetDateTime`で保存すると良いと思います。（実際には文字列で保存するかもしれませんが、型を変換する前の持ち方という意味）

なぜutcかというと、後でユーザーがタイムゾーンを変更したときにややこしくなるからです。

DBに保存されるデータは、保存した地域に依存しないほうが良いと思います。


### サーバーのレスポンスに入っている絶対時刻を処理する場合
クライアントアプリだとよくあるシチュエーションかと思います。記事を取得した時、その作成時刻が入っているような場合ですね。

サーバーはUTC時刻を保存しておくべきだと思うので、その前提で話します。

レスポンスを受けてシリアライズする場合、`Instant`かutcの`ZonedDateTime`(または`OffsetDateTime`)に変換すると良いと思います。しかし私としては`ZonedDateTime`が良いと思います。理由としては、基本的にサーバーからのレスポンスにはISO8601形式の時刻(2014-10-10T04:50:40Zみたいなもの)が入っているからです。`Instant`を使うなら、サーバーはエポック秒を返すべきではないでしょうか？

サーバーが日付時刻を返しているのは人間にわかりやすいからだと思います。ログを出力する場合、エポック秒だとよくわかりません。`ZonedDateTime`を使うことで、ログも出力しやすくなります。

現地のタイムゾーンでなくutcの`ZonedDateTime`に変換するのは、DBに保存することも念頭に置いているためです。

最後に表示する段階で、`ZoneId.systemDefault()`を指定することで、世界各地で使われるアプリでも時刻を出し分けます。

ただ、サンプルを見ると、[now in android](https://github.com/android/nowinandroid)や[DroidKaigi203](https://github.com/DroidKaigi/conference-app-2023)では`Instant`にシリアライズしているようです。`Instant`なのか`ZonedDateTime`なのかは議論が分かれるところになりそうです。

まぁ、シリアライズせずに文字列で取り回しておいて、表示するときに`ZonedDateTime`を経由して現地の時刻を表示するのも良いと思います。


-------

総じて、タイムゾーンをユーザーが変更した時、を考慮するとどれを使うかを考えるヒントになると思います。

また、変換の過程では２つが混在することもあると思います。

上記方針で使っていって、なにか気づきがあればお伝えしたいと思います。


## 補足
また今回詳しくは触れませんが、java.timeのパッケージには`Clock`、`Duration`、`Period`、`ChronoUnit`などの便利なクラスがあります。

`Clock`は現在時刻を提供し、テスト用に固定した現在時刻を提供します。

`Duration`は時間、`Period`は日付の期間を処理できます。
時刻の日とか分とかの部分のみを抽出したりもできます。

`ChronoUnit`でも期間を計算できます。

くわしくは[こちらの記事様](https://qiita.com/tag1216/items/9cb33a39a6666983491d)が参考になります。