6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Java】Date Time API

Last updated at Posted at 2023-05-14

【Java】Date Time API

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

java.time パッケージおよびそのサブパッケージにより構成されます。

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

Android では API Level 26(Android 8.0 Oreo) からです。
それ以下の API Level でも Android Gradle プラグイン 4.0.0 以上を使用すれば一部が使えます。
Java 8 言語機能と API を使用する | Android デベロッパー | Android Developers

この記事では次の順番で説明をしていきます。

  1. もはや使うべきでない古い日時 API
  2. Date Time API の主要な型
  3. 日時を扱う際の注意点やベストプラクティス・Kotlin での扱い方

🏚古い日時 API

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

🚫Date クラス

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

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

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 です。

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

🔃代替

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

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

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

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

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

🚫Calendar クラスGregorianCalendar クラス

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

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

🔃代替

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

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

🚫DateFormat クラスSimpleDateFormat クラス

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

複数のスレッドから同じインスタンスを同時に利用すると結果が不正になります。1

🔃代替

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

✨Date Time API

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

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

✅時点

Instant クラス

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

精度はナノ秒です。

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

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

✅現地時間

LocalDateTime クラス
LocalDate クラス
LocalTime クラス

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

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

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 クラス
OffsetTime クラス

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

OffsetDate クラスはありません。)

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 クラス

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

ZonedDate クラスや ZonedTime クラスはありません。)

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 クラス

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

ZoneOffset クラス

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

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

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

Clock クラス

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

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

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 クラス

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

Duration クラス

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

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 クラス

年を表すクラスです。

YearMonth クラス

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

Month クラス

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

MonthDay クラス

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

DayOfWeek クラス

曜日を表す列挙型です。

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

✅時間に関する単位

ChronoUnit クラス

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

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 クラス

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

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

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

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
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
ZonedDateTime.now(ClockManager.getClock());

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

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() メソッド を使えば、システムの日時設定が変更されても影響を受けずに経過時間を得ることができます。
(1回の JVM の起動中に限られますが。
 それ以上のことをしたければ、サーバーの日時を利用するなどが必要になるでしょう。)

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

日時オブジェクトは Comparable インターフェイスを実装しているため、
比較演算子を用いて比較できます。

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

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

  • isBefore
  • isAfter
  • isEqual
Java
LocalDate.parse("2023-01-01").isBefore(LocalDate.parse("2023-01-02")) // -> true

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

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

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

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

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

Pair で日時の範囲を表すのは避けましょう。2

  • 範囲を表しているということが型からは分からない。
  • 始点・終点を取得するプロパティが firstsecond となるため、始点・終点であることが読み取りにくい。
    ClosedRange であれば startendInclusive となり明確。
  • 終点(second プロパティ)の値の開始時と終了時のどちらまでが範囲に含まれるのかが明確でない。
    たとえば yesterday to tomorrow という Pair があったときに、tomorrow の一日が始まるまでが範囲なのか終わるまでが範囲なのかが明確でない。
    ClosedRange であれば終点を範囲に含む(tomorrow の一日が終わるまでが範囲である)ことが明確。

/以上

  1. 筆者も実際にハマりました。

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

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?