【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
この記事では次の順番で説明をしていきます。
🏚古い日時 API
Date Time API の内容について述べる前に、
もはや使うべきではなくなった古い API について述べます。
🚫Date
クラス
👎使うべきでない理由: ミュータブルである
ミュータブルなので、他所でも参照を持たれていると、いつの間にか書き換えられてしまう危険性があります。
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
クラスを使用している場合はこれを使いましょう。
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 のクラスは基本的にイミュータブルであり、スレッドセーフです。
✅時点
ある時点を表すクラスです。
タイムゾーンなどに依存しない、絶対的な時間を表します。
精度はナノ秒です。
内部ではエポックタイム(1970-01-01T00:00:00Z)からの時間を持ちます。
Instant instant0 = Instant.now(); // 現在の時点
Instant instant1 = Instant.ofEpochMilli(0); // 1970-01-01T00:00:00Z
✅現地時間
⏱LocalDateTime
クラス
⏱LocalDate
クラス
⏱LocalTime
クラス
「その場所がどのタイムゾーンなのかはわからないが、その現地での」日付や時刻を表すクラス群です。
Instant
に変換するには時差(もしくはタイムゾーン)の情報が必要です。
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
クラスはありません。)
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();
✅タイムゾーン付きの日時
タイムゾーン付きの日時を表すクラスです。
(ZonedDate
クラスや ZonedTime
クラスはありません。)
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
クラスを継承しています。
ZoneId zone = ZoneId.of("Asia/Tokyo"); // 日本のタイムゾーン
ZoneOffset offset = ZoneOffset.ofHours(9); // +9時間の時差
✅現在の時点とタイムゾーンの提供
現在の時点(Instant
)とタイムゾーンを提供するクラスです。
各日時クラスの now()
メソッドは now(Clock.systemDefaultZone())
の簡易版です。
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
✅期間
日付ベースの期間(1年と2ヶ月と3日、など)を表すクラスです。
時刻ベースの期間(1時間2分3秒、など)を表すクラスです。
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日をまたいでいるが、
// 日数ではなく「何年と何ヶ月と何日」という持ち方をしているので
// 同じ月日になる。
✅年・月・週
年を表すクラスです。
何年何月を表すクラスです。
日までは指定せず、ある出来事が何年の何月に起こったかを言うようなときに使えます。
月を表す列挙型です。
別の数値との取り違えを防ぐだけでなく、
13月になってしまったりすることも防げます。
何月何日を表すクラスです。
毎年やってくる記念日を表すようなときに使えます。
曜日を表す列挙型です。
(Day
もしくは DayOfMonth
というような型はありません。)
✅時間に関する単位
時間に関連する単位(年、時間、ミリ秒、など)を表す列挙型です。
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
✅フォーマット
日時オブジェクトと文字列を相互変換するためのフォーマッタークラスです。
パターン文字列から組み立てることもできますし、
ISO-8601 に基づいたインスタンスが定数定義されています。
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
を使います。
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
を差し替えることで
特定の日時・タイムゾーンでのテストを簡単に行うことができるようになります。
例えば、次のようなクラスを作成して…
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);
}
}
現在の日時を取得する際には必ず次のようにすれば…
ZonedDateTime.now(ClockManager.getClock());
次のように日時を指定してのテストを簡単に行えます。
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
インターフェイスを実装しているため、
比較演算子を用いて比較できます。
LocalDate.parse("2023-01-01") < LocalDate.parse("2023-01-02") // -> true
もちろん Java でも Comparable
同士の比較はできますが、
比較演算子は使えないのでコードが読みにくいです。
日時オブジェクトが持つ次のメソッドを使って比較する方がよいでしょう。
isBefore
isAfter
isEqual
LocalDate.parse("2023-01-01").isBefore(LocalDate.parse("2023-01-02")) // -> true
🔍【Kotlin/JVM】日時の範囲は ClosedRange
型で表す
「いつからいつまで」という日時の範囲は ClosedRange
型で表しましょう。
val dateRange: ClosedRange<LocalDate, LocalDate> =
yesterday..tomorrow // 昨日から明日まで
Date Time API にも期間を表すクラスはありますが、
いつからいつまでという期間を表すものではありません。
クラス | 表す期間 |
---|---|
Period |
何年と何ヶ月と何日間という日付ベースの期間 |
Duration |
何時間と何分と何秒という時刻ベースの期間 |
Pair
で日時の範囲を表すのは避けましょう。2
- 範囲を表しているということが型からは分からない。
- 始点・終点を取得するプロパティが
first
・second
となるため、始点・終点であることが読み取りにくい。
ClosedRange
であればstart
・endInclusive
となり明確。 - 終点(
second
プロパティ)の値の開始時と終了時のどちらまでが範囲に含まれるのかが明確でない。
たとえばyesterday to tomorrow
というPair
があったときに、tomorrow
の一日が始まるまでが範囲なのか終わるまでが範囲なのかが明確でない。
ClosedRange
であれば終点を範囲に含む(tomorrow
の一日が終わるまでが範囲である)ことが明確。
/以上