【Java】Date Time API

# 【Java】Date Time API

Java 標準ライブラリーの Date Time API は日付や時刻を扱うための API セットです。

[`java.time` パッケージ](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/package-summary.html)およびそのサブパッケージにより構成されます。

Java 8 から導入されました。

Android では API Level 26（Android 8.0 Oreo） からです。
それ以下の API Level でも Android Gradle プラグイン 4.0.0 以上を使用すれば一部が使えます。
→[Java 8 言語機能と API を使用する  \|  Android デベロッパー  |  Android Developers](https://developer.android.com/studio/write/java8-support?hl=ja)

この記事では次の順番で説明をしていきます。
1. [もはや使うべきでない古い日時 API](#古い日時-api)
2. [Date Time API の主要な型](#date-time-api)
3. [日時を扱う際の注意点やベストプラクティス・Kotlin での扱い方](#tips)

## 🏚古い日時 API

Date Time API の内容について述べる前に、
もはや使うべきではなくなった古い API について述べます。

### 🚫[`Date` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/util/Date.html)

#### 👎使うべきでない理由: ミュータブルである

ミュータブルなので、他所でも参照を持たれていると、いつの間にか書き換えられてしまう危険性があります。

```Java:Java
final var date = new Date(0);
System.out.println(date); // > Thu Jan 01 09:00:00 JST 1970
date.setTime(24 * 60 * 60 * 1000);
System.out.println(date); // > Fri Jan 02 09:00:00 JST 1970
```

#### 👎使うべきでない理由: クラス名が紛らわしい

日付だけではなく時刻まで扱うのに、
クラス名が `Date` です。

変数や関数の名前を `myDate` や `getMyDate()` のようにすると、
単に `Date` 型であることを示しているのか
日付だけを扱っていて時刻は考慮していないということなのかが
わからなくなります。

[^時刻まで扱う]: 正確に言えばミリ秒まで

#### 🔃代替

`Instant` クラスを使ってください。

`Date` クラスには `Intstant` と相互変換するためのメソッドもあります。
プロジェクトで使用しているライブラリーが `Date` クラスを使用している場合はこれを使いましょう。

```Java:Java
Date date1 = new Date(0);
Instant instant = date1.toInstant(); // Date を Instant に変換する。
Date date2 = Date.from(instant); // Instant を Date に変換する。
```

ただし、精度が異なることに注意してください。

| クラス | 精度 |
|---|---|
| `Date` | ミリ秒 |
| `Instant` | ナノ秒 |

### 🚫[`Calendar` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/util/Calendar.html)・[`GregorianCalendar` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/util/GregorianCalendar.html#%3Cinit%3E(int,int,int))

#### 👎使うべきでない理由: 月を表す数値が 0〜11

月を表す数値が 0〜11 なので間違いやすいです。
（日時の標準である ISO-8601 でも月は 1〜12 です。）

#### 🔃代替

`ZonedDateTime` クラスなどを使ってください。

プロジェクトで使用しているライブラリーが `Date` クラスを使用している場合は、
一旦 `Instant` などに変換してから扱うことで、
`Calendar` クラスを使わないようにできます。

### 🚫[`DateFormat` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/text/DateFormat.html)・[`SimpleDateFormat` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/text/SimpleDateFormat.html)

#### 👎使うべきでない理由: スレッドセーフでない

複数のスレッドから同じインスタンスを同時に利用すると結果が不正になります。[^スレッドセーフでない]

[^スレッドセーフでない]: 筆者も実際にハマりました。

#### 🔃代替

`DateTimeFormatter` クラスを使ってください。


## ✨Date Time API

ここから、本題である Date Time API の主要な型を紹介していきます。

Date Time API のクラスは基本的にイミュータブルであり、スレッドセーフです。

### ✅時点

⏱[`Instant` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/Instant.html)

ある時点を表すクラスです。
タイムゾーンなどに依存しない、絶対的な時間を表します。

精度はナノ秒です。

内部ではエポックタイム（1970-01-01T00:00:00Z）からの時間を持ちます。

```Java:Java
Instant instant0 = Instant.now(); // 現在の時点
Instant instant1 = Instant.ofEpochMilli(0); // 1970-01-01T00:00:00Z
```

### ✅現地時間

⏱[`LocalDateTime` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/LocalDateTime.html)
⏱[`LocalDate` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/LocalDate.html)
⏱[`LocalTime` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/LocalTime.html)

「その場所がどのタイムゾーンなのかはわからないが、その現地での」日付や時刻を表すクラス群です。

`Instant` に変換するには時差（もしくはタイムゾーン）の情報が必要です。

```Java:Java
LocalDateTime dateTime0 = LocalDateTime.now();
LocalDateTime dateTime1 = LocalDateTime.of(2023, 1, 2, 1, 23, 45);
LocalDateTime dateTime2 = LocalDateTime.parse("2023-01-02T01:23:45");

Instant instant = dateTime0.toInstant(ZoneOffset.ofHours(9));
```

### ✅時差付きの日時

⏱[`OffsetDateTime` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/OffsetDateTime.html)
⏱[`OffsetTime` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/OffsetTime.html)

時差付きの日付や時刻を表すクラス群です。

（`OffsetDate` クラスはありません。）

```Java:Java
OffsetDateTime dateTime0 = OffsetDateTime.now();
OffsetDateTime dateTime1 = OffsetDateTime.of(2023, 1, 2, 1, 23, 45, 0, ZoneOffset.ofHours(9));
OffsetDateTime dateTime2 = OffsetDateTime.parse("2023-01-02T01:23:45+09:00");

Instant instant = dateTime2.toInstant();
```

### ✅タイムゾーン付きの日時

⏱[`ZonedDateTime` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/ZonedDateTime.html)

タイムゾーン付きの日時を表すクラスです。

（`ZonedDate` クラスや `ZonedTime` クラスはありません。）

```Java:Java
ZonedDateTime dateTime0 = ZonedDateTime.now();
ZonedDateTime dateTime1 = ZonedDateTime.of(2023, 1, 2, 1, 23, 45, 0, ZoneId.of("Asia/Tokyo"));
ZonedDateTime dateTime2 = ZonedDateTime.parse("2023-01-02T01:23:45+09:00[Asia/Tokyo]");

Instant instant = dateTime2.toInstant();
```

### ✅タイムゾーン

⏱[`ZoneId` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/ZoneId.html)

タイムゾーンを表す抽象クラスです。

⏱[`ZoneOffset` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/ZoneOffset.html)

時差を表すクラスです。
`ZoneId` クラスを継承しています。

```Java:Java
ZoneId zone = ZoneId.of("Asia/Tokyo"); // 日本のタイムゾーン
ZoneOffset offset = ZoneOffset.ofHours(9); // +9時間の時差
```

### ✅現在の時点とタイムゾーンの提供

⏱[`Clock` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/Clock.html)

現在の時点（`Instant`）とタイムゾーンを提供するクラスです。

各日時クラスの `now()` メソッドは `now(Clock.systemDefaultZone())` の簡易版です。

```Java:Java
Clock clockDefault = Clock.systemDefaultZone();
var dateTimeDefault = ZonedDateTime.now(clockDefault); // ZonedDateTime.now() と等価

Clock clockUtc = Clock.system(ZoneOffset.UTC);
var dateTimeUtc = ZonedDateTime.now(clockUtc); // UTC での現在の日時

ZoneOffset offset = ZoneOffset.ofHours(9);
Instant instant = LocalDateTime.parse("2000-01-01T00:00:00")
        .toInstant(offset);
Clock clockFixed = Clock.fixed(instant, offset);
var dateTimeFixed = ZonedDateTime.now(clockFixed); // -> 2000-01-01T00:00+09:00
```

### ✅期間

⏱[`Period` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/Period.html)

日付ベースの期間（1年と2ヶ月と3日、など）を表すクラスです。

⏱[`Duration` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/Duration.html)

時刻ベースの期間（1時間2分3秒、など）を表すクラスです。

```Java:Java
Period period = Period.between(
        LocalDate.parse("2022-01-01"),
        LocalDate.parse("2023-03-04")
); // 1年と2ヶ月と3日
LocalDate.parse("2023-01-01")
        .plus(period); // -> 2024-03-04
// 閏年の2月29日をまたいでいるが、
// 日数ではなく「何年と何ヶ月と何日」という持ち方をしているので
// 同じ月日になる。
```

### ✅年・月・週

⏱[`Year` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/Year.html)

年を表すクラスです。

⏱[`YearMonth` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/YearMonth.html)

何年何月を表すクラスです。
日までは指定せず、ある出来事が何年の何月に起こったかを言うようなときに使えます。

⏱[`Month` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/Month.html)

月を表す列挙型です。
別の数値との取り違えを防ぐだけでなく、
13月になってしまったりすることも防げます。

⏱[`MonthDay` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/MonthDay.html)

何月何日を表すクラスです。
毎年やってくる記念日を表すようなときに使えます。

⏱[`DayOfWeek` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/DayOfWeek.html)

曜日を表す列挙型です。

（`Day` もしくは `DayOfMonth` というような型はありません。）

### ✅時間に関する単位

⏱[`ChronoUnit` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/temporal/ChronoUnit.html)

時間に関連する単位（年、時間、ミリ秒、など）を表す列挙型です。

```Java:Java
var oldDate = LocalDate.parse("2023-01-01");
var newDate = LocalDate.parse("2023-02-01");
oldDate.until(newDate, ChronoUnit.DAYS); // -> 31
oldDate.until(newDate, ChronoUnit.MONTHS); // -> 1
```

### ✅フォーマット

⏱[`DateTimeFormatter` クラス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/format/DateTimeFormatter.html)

日時オブジェクトと文字列を相互変換するためのフォーマッタークラスです。
パターン文字列から組み立てることもできますし、
ISO-8601 に基づいたインスタンスが定数定義されています。

```Java:Java
var zonedDateTime = ZonedDateTime.of(
        LocalDate.parse("2023-01-02"),
        LocalTime.parse("01:23:45"),
        ZoneId.of("Asia/Tokyo")
);

System.out.println(
        DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(zonedDateTime)
); // > 2023-01-02T01:23:45+09:00

System.out.println(
        DateTimeFormatter.ofPattern("uuuu年MM月dd日hh時mm分ss秒").format(dateTime)
); // > 2023年01月02日01時23分45秒
```

#### ⚠️年には `yyyy` ではなく `uuuu` を使う

参考：
[Java8のDate and Time APIではyyyyじゃなくてuuuuを使う - Mitsuyuki.Shiiba](https://bufferings.hatenablog.com/entry/2017/10/23/000057)

`DateTimeFormatter` のパターン文字列で年に `yyyy` を使うと、年が正でないときに期待と異なる結果になります。
代わりに `uuuu` を使います。

```Java:Java
var uuuu = DateTimeFormatter.ofPattern("uuuu年");
var yyyy = DateTimeFormatter.ofPattern("yyyy年");

var date = LocalDate.parse("2000-01-01"); // 2000年
System.out.println(
        uuuu.format(date)
); // > 2000年
System.out.println(
        yyyy.format(date)
); // > 2000年

date = date.minusYears(1999); // 1999年を引く
System.out.println(
        uuuu.format(date)
); // > 0001年
System.out.println(
        yyyy.format(date)
); // > 0001年

date = date.minusYears(1); // さらに1年を引く
System.out.println(
        uuuu.format(date)
); // > 0000年
System.out.println(
        yyyy.format(date)
); // > 0001年
// ^ 紀元前1年

date = date.minusYears(1); // さらに1年を引く
System.out.println(
        uuuu.format(date)
); // > -0001年
System.out.println(
        yyyy.format(date)
); // > 0002年
// ^ 紀元前2年
```


## 👀Tips

Date Time API を使用する際（や一般に日時を扱う際）の注意点やベストプラクティスなどについて述べます。
また Kotlin での扱い方についても最後に述べます。

### 🔍端数の丸めでは切り捨てる

時間の端数は切り上げてはいけません。

切り上げると、上位の単位の値が変わる可能性があります。
1桁ミリ秒を切り上げただけで年が変わってしまうこともあります。

1999-12-31T23:59:59.999
　↓1桁ミリ秒を切り上げる
2000-01-01T00:00:00.00

そうなると年単位での集計に影響が出るなどの問題が出るかもしれません。

### 🔍`now()` を避ける

テスト容易性のため、現在の日時を取得する際には、引数なしの `now()` メソッドは避けましょう。

`Clock` オブジェクトは、タイムゾーンを指定したインスタンスを生成したり、常に固定の時間を返したりすることができます。
`Clock` オブジェクトを返すメソッドを一つ用意しておき、  
現在の日時を取得する際には常にそこから取得した `Clock` を用いるようにしておけば、  
そのメソッドが返す `Clock` を差し替えることで
特定の日時・タイムゾーンでのテストを簡単に行うことができるようになります。

例えば、次のようなクラスを作成して…

```Java:Java
public class ClockManager {
    private static Clock clock = Clock.systemDefaultZone();

    public static Clock getClock() {
        return clock;
    }

    /**
     * テスト用。
     * getClock() が返す Clock を、指定された時間・タイムゾーンに固定する。
     */
    public static void fixClock(Instant fixedInstant, ZoneId zone) {
        clock = Clock.fixed(fixedInstant, zone);
    }
}
```

現在の日時を取得する際には必ず次のようにすれば…

```Java:Java
ZonedDateTime.now(ClockManager.getClock());
```

次のように日時を指定してのテストを簡単に行えます。

```Java:Java
void test() {
    // 常に日本時間で 2000-01-01T00:00:00 の Clock を返すように設定する。
    ZoneOffset offset = ZoneOffset.ofHours(9);
    Instant instant = LocalDateTime.parse("2000-01-01T00:00:00")
            .toInstant(offset);
    ClockManager.fixClock(instant, offset);

    // 現在の日時を使用する機能のテストを行う。
    // ...
}
```

### 🔍現在の日時を取得する箇所を絞る

「現在の日時」をバラバラに取得すると、
本質的には同じであるべき日時がそれぞれで少しずつズレてしまいます。

処理の根元で日時を取得しておいて、
引数で渡していくようにしましょう。

### 🔍システムの日時設定への依存

システムの日時設定が変更されると Date Time API から得られる現在の日時も変わってしまいます。
経過時間を得るために、過去のある時点で保存した日時と現在の日時の差を取る場合、
その間にシステムの日時設定が変更されていると、正しい結果が得られません。

[`System.nanoTime()` メソッド](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/lang/System.html#nanoTime()) を使えば、システムの日時設定が変更されても影響を受けずに経過時間を得ることができます。
（1回の JVM の起動中に限られますが。
　それ以上のことをしたければ、サーバーの日時を利用するなどが必要になるでしょう。）

### 🔍【Kotlin/JVM】日時オブジェクトは比較演算子で比較可能

日時オブジェクトは [`Comparable` インターフェイス](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/lang/Comparable.html)を実装しているため、
比較演算子を用いて比較できます。

```Kotlin:Kotlin
LocalDate.parse("2023-01-01") < LocalDate.parse("2023-01-02") // -> true
```

もちろん Java でも `Comparable` 同士の比較はできますが、
比較演算子は使えないのでコードが読みにくいです。
日時オブジェクトが持つ次のメソッドを使って比較する方がよいでしょう。

- `isBefore`
- `isAfter`
- `isEqual`

```Java:Java
LocalDate.parse("2023-01-01").isBefore(LocalDate.parse("2023-01-02")) // -> true
```

### 🔍【Kotlin/JVM】日時の範囲は `ClosedRange` 型で表す

「いつからいつまで」という日時の範囲は `ClosedRange` 型で表しましょう。

```Kotlin:Kotlin
val dateRange: ClosedRange<LocalDate, LocalDate> =
        yesterday..tomorrow // 昨日から明日まで
```

:::note info
Date Time API にも期間を表すクラスはありますが、
**いつからいつまで**という期間を表すものではありません。

| クラス     | 表す期間                                   |
| ---------- | ------------------------------------------ |
| `Period`   | 何年と何ヶ月と何日間という日付ベースの期間 |
| `Duration` | 何時間と何分と何秒という時刻ベースの期間   |
:::

:::note warn
`Pair` で日時の範囲を表すのは避けましょう。[^Pairで範囲]
- 範囲を表しているということが型からは分からない。
- 始点・終点を取得するプロパティが `first`・`second` となるため、始点・終点であることが読み取りにくい。  
  `ClosedRange` であれば `start`・`endInclusive` となり明確。
- 終点（`second` プロパティ）の値の開始時と終了時のどちらまでが範囲に含まれるのかが明確でない。  
  たとえば `yesterday to tomorrow` という `Pair` があったときに、`tomorrow` の一日が始まるまでが範囲なのか終わるまでが範囲なのかが明確でない。  
  `ClosedRange` であれば終点を範囲に含む（`tomorrow` の一日が終わるまでが範囲である）ことが明確。

[^Pairで範囲]: 同様の理由から、日時に限らず、範囲を `Pair` で表すのは避けましょう。
:::

／以上