🍇

20.2 Date and Time API(LocalDateTime、Period、ZonedDateTime等)Java Basic編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
  • 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
  • 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
  • 今後、フリーランスエンジニアとしてのキャリアを検討している方
  • Chat GPT」のエンジニアリングへの活用に興味のある方
  • Oracle認定Javaプログラマ」の資格取得を目指している方
  • IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

20.2 Date and Time API

チャプターの概要

このチャプターでは、古典的な日時APIの課題を解消するために導入された、Date and Time APIついて学びます。
なおこのチャプターでも「2022年11月5日」を「日付」、「15時20分30秒」を「時刻」、そして日付と時刻の2つを合わせた概念を「日時」という言い方で統一します。また「時間」というワードはhourを表すものとします。

20.2.1 Date and Time APIの概要

Date and Time APIの概要

Date and Time APIは、古典的な日時クラスの課題を解消するために導入された、新しい日付および時間を操作するためのクラス群です。Date and Time APIの主要なクラスは、java.timeパッケージに所属しています。
これらの主要なクラスは、タイムゾーンという概念がないローカル日時を表すものと、タイムゾーンありのものに分けられます。またタイムゾーンありのクラスも、時差情報のみを持つものと、時差情報と地域情報を持つものとに分かれます。さらにこれらのクラスは、日時(日付+時刻)を表すものもあれば、日付のみを表すもの、さらには時刻のみを表すものがあります。
Date and Time APIによって提供されるこれらの日時クラスを整理すると、以下の表のようになります。

【表20-2-1】Date and Time APIによって提供されるの日時クラス全体像

タイムゾーンの有無 表す日時 日時(日付+時刻) 日付のみ 時刻のみ
タイムゾーンなし ローカル日時 LocalDateTime LocalDate LocalTime
タイムゾーンあり 時差情報付の日時 OffsetDateTime OffsetTime
時差と地域付の日時 ZonedDateTime

Javaプログラムで日時を表したい場合、これら6つのクラスの中から、用途に応じて1つを選択して使用します。古典的な日時クラスでは、DateクラスとCalenderクラスの使い分けや相互変換が必要でしたが、Date and Time APIでは基本的にはクラス1つで完結します。またこれらのクラスはいずれもスレッドセーフなため、クラスのフィールドに型として定義しても問題はありません。

日時クラスの文字列表現

Date and Time APIの各クラスの特徴を理解するために、これらのクラスのオブジェクトを生成し、その文字列表現を確認してみましょう。これらのクラスでは、現在日時を表すオブジェクトはいずれもnow()メソッドで生成します。以下のように、それぞれのクラスの現在日時を表すオブジェクトを生成します。

snippet (pro.kensait.java.basic.lsn_20_2_1.Main)
LocalDateTime now1 = LocalDateTime.now();
LocalDate now2 = LocalDate.now();
LocalTime now3 = LocalTime.now();
OffsetDateTime now4 = OffsetDateTime.now();
OffsetTime now5 = OffsetTime.now();
ZonedDateTime now6 = ZonedDateTime.now();

これら6つの変数をコンソールに表示すると、以下のようになります。

LocalDateTime => 2022-11-05T18:47:09.044367200
LocalDate => 2022-11-05
LocalTime => 18:47:09.045369400
OffsetDateTime => 2022-11-05T18:47:09.045369400+09:00
OffsetTime => 18:47:09.045369400+09:00
ZonedDateTime => 2022-11-05T18:47:09.046369300+09:00[Asia/Tokyo]

これらの文字列表現は、「ISO 8601」という国際的な標準規格の仕様に準拠しています。「ISO 8601」では、まず日付については「2022-11-05」という形式で表します。また時刻については「18:47:09.044367200」といった具合に「時」「分」「秒」と、9桁の「ナノ秒」で表します。日付と時刻の間は"T"によって区切ります。

20.2.2 ローカル日時とLocalDateTimeクラス

ローカル日時のためのクラス

前述したようにDate and Time APIの日時クラスには幾つかの種類がありますが、実際のアプリケーション開発で特によく利用されるのが、ローカル日時を表すクラスです。
ローカル日時を表すクラスには、日付+時間を表すLocalDateTime、日付のみを表すLocalDate、時間のみを表すLocalTimeと、3種類あります。
この中でも、DateクラスやCalendarクラスの後継として特によく利用されているのがLocalDateTimeクラスです。LocalDateTimeクラスは、日時(日付+時間)としても日付としても時間としても使えるという意味で、汎用性の高さが特徴です。このレッスンでは、LocalDateTimeクラスのAPIについて、詳しく取り上げていくことにします。
LocalDateTimeクラスには非常に数多くのAPIがありますが、大きくは以下のように分類されます。

【表20-2-2】LocalDateTimeクラスのAPI全体像

APIの種類 メソッド名の特徴 本チャプターにおける説明箇所
オブジェクト生成のためのファクトリメソッド now()メソッド、of()メソッド レッスン20.2.2
日時を設定するためのAPI with〇〇()メソッド レッスン20.2.2
日時を取得するためのAPI get〇〇()メソッド レッスン20.2.2
日時を加算するためのAPI plus〇〇()メソッド レッスン20.2.2
日時を減算するためのAPI minus〇〇()メソッド レッスン20.2.2
日時の前後比較をするためのAPI is〇〇()メソッド レッスン20.2.2
LocalDateやLocalTimeに変換するためのAPI to〇〇()メソッド レッスン20.2.3
OffsetDateTimeやZonedDateTimeに変換するためのAPI at〇〇()メソッド レッスン20.2.5
日時と文字列を相互変換するためのAPI format()メソッド、parse()メソッド レッスン20.2.6

APIの全体像としては、CalendarクラスとSimpleDateFormatクラスのAPIを足し合わせ、より機能や表現力をリッチにしたようなイメージだと理解してもらえれば問題ないでしょう。

オブジェクト生成のためのファクトリメソッド

LocalDateTimeクラスのAPIの中で、ここではオブジェクト生成をするためのファクトリメソッドを説明します。
LocalDateTimeオブジェクトは、以下のようなAPIで生成します。

API(メソッド) 説明
static LocalDateTime now() (システムクロックによる)現在日時を表すLocalDateTimeオブジェクトを返す。
static LocalDateTime of(int, int, int, int, int) 指定された年、月、日、時間、および分から、LocalDateTimeオブジェクトを生成して返す。秒およびナノ秒はゼロに設定する。
static LocalDateTime of(int, int, int, int, int, int) 指定された年、月、日、時間、分、および秒から、LocalDateTimeオブジェクトを生成して返す。ナノ秒はゼロに設定する。
static LocalDateTime of(int, int, int, int, int, int, int) 指定された年、月、日、時間、分、秒、ナノ秒から、LocalDateTimeオブジェクトを生成して返す。

上記APIのうち3つあるof()メソッドは第二引数に月を取りますが、これはCalendarとは異なり1から数えます。
また月については、列挙型であるjava.time.Monthを指定することも可能です。Monthは月を表す列挙型で、JANUARY、FEBRUARY、MARCH、といった列挙子を持っています。
それでは具体的にコードを見ていきましょう。例えば「2022年4月10日15時20分」という日時を表すLocalDateTimeは、以下のように生成します。

snippet_1 (pro.kensait.java.basic.lsn_20_2_2.Main)
LocalDateTime date = LocalDateTime.of(2022, 4, 10, 15, 20);

月は1から数えるため、4月であれば4と指定します。

または列挙型Monthを使用して、以下のようにすることも可能です。

snippet_2 (pro.kensait.java.basic.lsn_20_2_2.Main)
LocalDateTime date = LocalDateTime.of(2022, Month.APRIL, 10, 15, 20);

日時を設定するためのAPI

生成されたLocalDateTimeに対して後から日時を設定するためには、with〇〇()という名前のAPIを使用します。

API(メソッド) 説明
LocalDateTime withYear(int) 年を指定された値に設定し、このLocalDateTimeのコピーを返す。
LocalDateTime withMonth(int) 月を指定された値(1~12)に設定し、このLocalDateTimeのコピーを返す。
LocalDateTime withDayOfMonth(int) 「月の日」を指定された値に設定し、このLocalDateTimeのコピーを返す。
LocalDateTime withHour(int) 時間を指定された値に設定し、このLocalDateTimeのコピーを返す。
LocalDateTime withMinute(int) 分を指定された値に設定し、このLocalDateTimeのコピーを返す。
LocalDateTime withSecond(int) 秒を指定された値に設定し、このLocalDateTimeのコピーを返す。
LocalDateTime withNano(int) ナノ秒を指定された値に設定し、このLocalDateTimeのコピーを返す。

LocalDateTimeはイミュータブルのため、オブジェクトを一度生成したら、後から属性を変更することができません。従って上記のAPIは、自身の属性を変更するわけではなく、指定された値を持つLocalDateTimeを新たに生成して返す、という挙動になる点に注意してください。
それでは「今日の15時20分」という日時を表すLocalDateTimeオブジェクトを生成してみましょう。

snippet_3 (pro.kensait.java.basic.lsn_20_2_2.Main)
LocalDateTime now = LocalDateTime.now();
LocalDateTime date = now.withHour(15).withMinute(20);

with〇〇()メソッドはLocalDateTimeが返されるため、このようにメソッドをチェーンして日時を設定することができます。またnow.withHour(15).withMinute(20)と呼び出しただけでは、変数nowの属性が変わるわけではありませんので、結果は別の変数に代入する必要があります。

日時を取得するためのAPI

生成されたLocalDateTimeから日時に関する属性を取得するためには、get〇〇()という名前のAPIを使用します。

API(メソッド) 説明
int getYear() 年フィールドを返す。
Month getMonth() 列挙型Monthを使用して、月フィールドを返す。
int getMonthValue() 月フィールド(1~12)を返す。
int getDayOfMonth() 「月の日」フィールドを返す。
DayOfWeek getDayOfWeek() 列挙型DayOfWeekである曜日フィールドを返す。
int getHour() 時間フィールドを返す。
int getMinute() 分フィールドを返す。
int getSecond() 秒フィールドを返す。
int getNano() ナノ秒フィールドを返す。

この中でgetDayOfWeek()メソッドの戻り値は、java.time.DayOfWeekになっています。DayOfWeekは曜日を表す列挙型で、SUNDAY、MONDAY、TUESDAY、といった列挙子を持っています。
例えば現在日時「11月5日 土曜日」を表すLocalDateTimeがあるものとします。このときLocalDateTimeから月、月の日、曜日を取得するためには、以下のようにします。

snippet_4 (pro.kensait.java.basic.lsn_20_2_2.Main)
LocalDateTime now = LocalDateTime.now();
int month = now.getMonthValue(); // 月=11
int day = now.getDayOfMonth(); // 月の日=5
DayOfWeek dayOfWeek = now.getDayOfWeek(); // 曜日=SATURDAY

日時を加減算するためのAPI

生成されたLocalDateTimeに対して日時を加算するためには、plus〇〇()という名前のAPIを使用します。

API(メソッド) 説明
LocalDateTime plusYears(long) 指定された年数を加算して、このLocalDateTimeのコピーを返す。
LocalDateTime plusMonths(long) 指定された月数を加算して、このLocalDateTimeのコピーを返す。
LocalDateTime plusWeeks(long) 指定された週数を加算して、このLocalDateTimeのコピーを返す。
LocalDateTime plusDays(long) 指定された日数を加算して、このLocalDateTimeのコピーを返す。
LocalDateTime plusHours(long) 指定された時間数を加算して、このLocalDateTimeのコピーを返す。
LocalDateTime plusMinutes(long) 指定された分数を加算して、このLocalDateTimeのコピーを返す。
LocalDateTime plusSeconds(long) 指定された秒数を加算して、このLocalDateTimeのコピーを返す。
LocalDateTime plusNanos(long) 指定されたナノ秒数を加算して、このLocalDateTimeのコピーを返す。

同じく日時を減算するためには、minus〇〇()という名前のAPIを使用します。

API(メソッド) 説明
LocalDateTime minusYears(long) 指定された年数を減算して、このLocalDateTimeのコピーを返す。
LocalDateTime minusMonths(long) 指定された月数を減算して、このLocalDateTimeのコピーを返す。
LocalDateTime minusWeeks(long) 指定された週数を減算して、このLocalDateTimeのコピーを返す。
LocalDateTime minusDays(long) 指定された日数を減算して、このLocalDateTimeのコピーを返す。
LocalDateTime minusHours(long) 指定された時間数を減算して、このLocalDateTimeのコピーを返す。
LocalDateTime minusMinutes(long) 指定された分数を減算して、このLocalDateTimeのコピーを返す。
LocalDateTime minusSeconds(long) 指定された秒数を減算して、このLocalDateTimeのコピーを返す。
LocalDateTime minusNanos(long) 指定されたナノ秒数を減算して、このLocalDateTimeのコピーを返す。

これらのAPIはwith〇〇()メソッドと同じように、指定された値を加減算した新しいLocalDateTimeを返す、という点に注意してください。
それでは、LocalDateTimeを加減算する例を見てみましょう。例えば現在日時が「2022年11月5日」の場合、「5年前における翌月の10日が、日曜日かどうか」を確認するためのコードは、以下のようになります。

snippet_5 (pro.kensait.java.basic.lsn_20_2_2.Main)
LocalDateTime now = LocalDateTime.now();
LocalDateTime date = now.minusYears(5).plusMonths(1).withDayOfMonth(10);
if (date.getDayOfWeek() == DayOfWeek.SUNDAY) {
    System.out.println("The day is SUNDAY");
}

「2022年11月5日」における「5年前における翌月の10日」は、実際のカレンダーでは「2017年12月10日」になります。この日は日曜日なので、このコードを実行すると"The day is SUNDAY"と表示されます。

日時の前後比較をするためのAPI

次にLocalDateTimeオブジェクト同士を比較し、時間的な前後関係を判定するためのAPIを示します。

API(メソッド) 説明
boolean isBefore(ChronoLocalDateTime<?>) このLocalDateTimeが、指定されたLocalDateTimeより前かどうかを判定する。
boolean isAfter(ChronoLocalDateTime<?>) このLocalDateTimeが、指定されたLocalDateTimeより後かどうかを判定する。
boolean isEqual(ChronoLocalDateTime<?>) このLocalDateTimeが、指定されたLocalDateTimeより等しいかどうかを判定する。

これらのAPIの引数であるChronoLocalDateTimeは、LocalDateTimeクラスが実装しているインタフェースなので、実質的にはLocalDateTimeと読み替えてもらって問題ありません。
なおこれらのAPIとは別にcompareTo()メソッドも用意されていますが、前後比較をするためには上記3つのAPIで事足りるのでここでは割愛しています。
それでは具体的にコードを見ていきましょう。例えば現在日時が「2022年11月5日」の場合、「2022年4月10日」と前後比較をするためのコードは以下のようになります。

snippet_6 (pro.kensait.java.basic.lsn_20_2_2.Main)
LocalDateTime now = LocalDateTime.now(); // 現在
LocalDateTime target = LocalDateTime.of(2022, 4, 12, 0, 0); // 2022年4月10日
boolean result = now.isAfter(target); // true

この例では変数now(2022年11月5日)の方が「2022年4月10日」よりも時間的に後ろなので、isAfter()メソッド呼び出しはtrueを返します。

20.2.3 ローカル日時を表すその他のクラス

LocalDateクラスとLocalTimeクラスの位置付け

ローカル日時を表すクラスの中では、前述したようにLocalDateTimeクラスが特に汎用性が高く、使い勝手に優れています。ただし日付のみを扱うこと、もしくは時間のみを扱うことが要件として明確な場合は、LocalDateクラスやLocalTimeクラスを使う方が良いでしょう。
LocalDateクラスやLocalTimeクラスにおけるAPIのラインアップは、LocalDateTimeクラスと大きくは変わりません。必然的にLocalDateクラスであれば日付に特化したもの、LocalTimeクラスであれば時間に特化したものが提供されています。
次項からは、LocalDateクラスおよびLocalTimeクラスのAPIについて取り上げていきます。

【図20-2-1】LocalDateクラスとLocalTimeクラス
image.png

LocalDateクラスのオブジェクト生成

まずはLocalDateクラスです。
LocalDateクラスのオブジェクトは、以下のようなファクトリメソッドで生成します。

API(メソッド) 説明
static LocalDate now() (システムクロックによる)現在日付を表すLocalDateオブジェクトを返す。
static LocalDate of(int, int, int) 指定された年、月、日から、LocalDateオブジェクトを生成して返す。

なおof()メソッドには、LocalDateTimeクラスと同じように、月として列挙型であるjava.time.Monthを指定することも可能です。
それでは具体的にコードを見ていきましょう。例えば「2022年4月10日」という日付を表すLocalDateオブジェクトは、以下のように生成します。

snippet_1 (pro.kensait.java.basic.lsn_20_2_3.Main)
LocalDate date = LocalDate.of(2022, 4, 10);

LocalTimeクラスのオブジェクト生成

次にLocalTimeクラスです。
LocalTimeクラスのオブジェクトは、以下のようなファクトリメソッドで生成します。

API(メソッド) 説明
static LocalTime now() (システムクロックによる)現在日付を表すLocalTimeオブジェクトを返す。
static LocalTime of(int, int) 指定された時間、および分から、LocalTimeオブジェクトを生成して返す。秒およびナノ秒はゼロに設定する。
static LocalTime of(int, int, int) 指定された時間、分、および秒から、LocalTimeオブジェクトを生成して返す。ナノ秒はゼロに設定する。
static LocalTime of(int, int, int, int) 指定された時間、分、秒、ナノ秒から、LocalTimeオブジェクトを生成して返す。

例えば「15時20分」という時刻を表すLocalTimeオブジェクトは、以下のように生成します。

snippet_2 (pro.kensait.java.basic.lsn_20_2_3.Main)
LocalTime time = LocalTime.of(15, 20);

LocalDateTimeとLocalDate/LocalTimeとの相互変換

LocalDateTimeクラスとLocalDateクラスおよびLocalTimeクラスは、相互に変換することが可能です。
まずLocalDateTimeクラスからLocalDateクラスおよびLocalTimeクラスへの変換は、LocalDateTimeの以下のAPIで変換します。

API(メソッド) 説明
LocalDate toLocalDate() このLocalDateTimeのLocalDate部分を返す。
LocalTime toLocalTime() このLocalDateTimeのLocalTime部分を返す。

例えば「2022年4月10日15時20分」という日時を表すLocalDateTimeから、LocalDateおよびLocalTimeにそれぞれ変換するためには、以下のようにします。

snippet_3 (pro.kensait.java.basic.lsn_20_2_3.Main)
LocalDateTime dateTime = LocalDateTime.of(2022, 4, 10, 15, 20);
LocalDate date = dateTime.toLocalDate(); // 2022年4月10日
LocalTime time = dateTime.toLocalTime(); // 15時20分

次にLocalDateクラスおよびLocalTimeクラスからLocalDateTimeクラスへの変換は、LocalDateTimeの以下のAPIで行います。

API(メソッド) 説明
static LocalDateTime of(LocalDate, LocalTime) 指定されたLocalDateおよびLocalTimeから、LocalDateTimeオブジェクトを生成して返す。

このようにLocalDateTimeへの変換では、必然的にLocalDateとLocalTimeが同時に必要になります。
例えば「2022年4月10日」を表すLocalDateと「15時20分」を表すLocalTimeから、LocalDateTimeに変換するためには、以下のようにします。

snippet_4 (pro.kensait.java.basic.lsn_20_2_3.Main)
LocalDate date = LocalDate.of(2022, 4, 10);
LocalTime time = LocalTime.of(15, 20);
LocalDateTime dateTime = LocalDateTime.of(date, time); // 2022年4月10日15時20分

これらのAPIと各クラスの関係は、以下の図のようになります。

【図20-2-2】LocalDateTime、LocalDate、LocalTimeの相互変換
image.png

TemporalAdjusterによる日付算出

日付の処理では「月末日」であったり「直後の金曜日」といった具合に、特定の要件に応じて日付の算出が必要になるケースがあります。このような日付の算出は、LocalDateクラスのwith()メソッドにjava.time.temporal.TemporalAdjusterクラスを指定することで、容易に実現可能です。
まずLocalDateクラスのwith()メソッドを、以下に示します。

API(メソッド) 説明
static LocalDate with(TemporalAdjuster) 指定されたTemporalAdjusterによって計算された日付を表すLocalDateオブジェクトを返す。

次にTemporalAdjusterクラスです。
このクラスのオブジェクトは、java.time.temporal.TemporalAdjustersクラスのファクトリメソッドによって生成します。以下に主要なものを示します。

API(メソッド) 説明
static TemporalAdjuster firstDayOfYear() 「年の最初の日」を表すTemporalAdjusterを返す。
static TemporalAdjuster firstDayOfMonth() 「月の最初の日」を表すTemporalAdjusterを返す。
static TemporalAdjuster firstInMonth(DayOfWeek) 「指定された曜日と一致する月の最初の日」を表すTemporalAdjusterを返す。(2022年11月の最初の日曜日など)
static TemporalAdjuster lastDayOfYear() 「年の最後の日」を表すTemporalAdjusterを返す。
static TemporalAdjuster lastDayOfMonth() 「月の最後の日」を表すTemporalAdjusterを返す。
static TemporalAdjuster lastInMonth(DayOfWeek) 「指定された曜日と一致する月の最後の日」を表すTemporalAdjusterを返す。(2022年11月の最後の日曜日など)
static TemporalAdjuster next(DayOfWeek) 「指定された曜日と一致する直後の日」を表すTemporalAdjusterを返す。
static TemporalAdjuster nextOrSame(DayOfWeek) 「指定された曜日と一致する直後の日」を表すTemporalAdjusterを返す。ただし当日を含む。
static TemporalAdjuster previous(DayOfWeek) 「指定された曜日と一致する直前の日」を表すTemporalAdjusterを返す。
static TemporalAdjuster previousOrSame(DayOfWeek) 「指定された曜日と一致する直前の日」を表すTemporalAdjusterを返す。ただし当日を含む。

それではLocalDateとTemporalAdjusterによる日付の算出を、具体的にコードで見ていきましょう。ここでは当日を「2022年11月5日 土曜日」とし、様々な要件に基づいて日付の算出を行います。

snippet_5 (pro.kensait.java.basic.lsn_20_2_3.Main)
LocalDate orgDate = LocalDate.of(2022, 11, 5);
LocalDate date1 = orgDate.with(
        TemporalAdjusters.firstDayOfMonth()); //【1】月の最初の日=11月1日
LocalDate date2 = orgDate.with(
        TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)); //【2】月の最初の月曜日=11月7日
LocalDate date3 = orgDate.with(
        TemporalAdjusters.lastDayOfMonth()); //【3】月の最後の日(月末日)=11月30日
LocalDate date4 = orgDate.with(
        TemporalAdjusters.next(DayOfWeek.FRIDAY)); //【4】直後の金曜日=11月11日
LocalDate date5 = orgDate.with(
        TemporalAdjusters.previous(DayOfWeek.SUNDAY)); //【5】直前の日曜日=10月30日

上から順番に説明します。
まず月の最初の日を算出し、結果として11月1日を求めます【1】。
次に月の最初の月曜日を算出し、結果として11月7日を求めます【2】。
次の月の最後の日、要は「月末日」を算出し、結果として11月30日を求めます【3】。特にこの処理は、利用されるケースが多いのではないでしょうか。
次に直後の金曜日を算出し、結果として11月11日を求めます【4】。
最後に直前の日曜日を算出し、結果として10月30日を求めます【5】。

以上、TemporalAdjusterクラスによる日付算出処理を見てきました。実際のJavaによるアプリケーション開発では、このような日付の算出処理は、非常に多くのユースケースで必要になります。そういった意味でも、TemporalAdjusterクラスはきわめて利便性の高い機能であると言えるでしょう。

20.2.4 日時の差分計算

日時の差分計算処理の全体像

日時の処理では、特定の要件に応じて2つの日時間の差分計算が必要になるケースがあります。例えば支払期日までの残日数を求めたり、入社日からの経過日数を求めたりする、といった処理です。
このような差分計算には、以下のような2つの方法があります。

(1)日時単位での計算
(2)日付量または時間量による計算

(1)は2つの日時間の差分を、特定の日時単位、例えば「日」や「分」などで表す、というものです。例としては現在が「2022年11月5日」、支払期日が「2023年3月末」だとして、支払期日まで何日残っているのかを求める、といったケース。または始業時刻が「9時0分」だとして、現在時刻までの経過時間を「分」で求める、といったケースなどが考えられます。このような差分計算には、java.time.temporal.ChronoUnit列挙型に備わっているAPIを利用します。

(2)は2つの日時間の差分を、日付量、つまり「〇年〇カ月〇日」と表したり、時間量、つまり「〇時間〇分〇秒」と表したりする、というものです。例としては、「2012年4月1日」に入社した社員の「2022年11月5日」時点での経過日数を「〇年〇カ月〇日」で求める、といったケース。またはタイムリミットが「21時30分」だとして、現在時刻からの残り時間を「〇時間〇分〇秒」で求める、といったケースなどが考えられます。このような差分計算には、日付量の場合はjava.time.Periodクラス、時間量の場合はjava.time.Durationクラスを利用します。

これらのクラスを使った差分計算の方法について、次項以降で順番に見ていきます。

ChronoUnit列挙型による差分計算

java.time.temporal.ChronoUnitは日時単位を表す列挙型で、差分計算を行うためのメソッドを保持しています。
前述したように2つの日時間の差分を特定の日時単位で行う場合は、この列挙型のAPIを利用します。
この列挙型には、主に以下のような列挙子が定義されています。

  • ChronoUnit.YEARS … 年
  • ChronoUnit.MONTHS … 月
  • ChronoUnit.WEEKS … 週
  • ChronoUnit.DAYS … 日
  • ChronoUnit.HOURS … 時
  • ChronoUnit.MINUTES … 分
  • ChronoUnit.SECONDS … 秒
  • ChronoUnit.NANOS … ナノ秒

またこの列挙型には、差分を計算するための以下のAPIが定義されています。

API(メソッド) 説明
long between(Temporal, Temporal) 指定された2つの日時間の差分を返す。

between()メソッドの引数であるTemporalインタフェースは、FQCNはjava.time.temporal.Temporalです。このインタフェースはLocalDateTime、LodalDate、LocalTimeなど既出の日時クラスが実装していますので、これらのどの日時クラスでも引数に渡すことが可能です。
それでは具体的にコードを見ていきましょう。例えば現在が「2022年11月5日」、支払期日が「2023年3月末」だとして、期日までの残日数を「日」で求めるためのコードは以下のようになります。

snippet_1 (pro.kensait.java.basic.lsn_20_2_4.Main)
LocalDate today = LocalDate.of(2022, 11, 5);
LocalDate dueDate = LocalDate.of(2023, 3, 31);
long remaining = ChronoUnit.DAYS.between(today, dueDate); //【1】

この例では日付単位の計算を行うため、ChronoUnit.DAYS列挙子のbetween()メソッドに、差分計算の対象になる2つの日付を渡します【1】。差分計算の結果、変数remainingには146が格納されます。つまり支払期日まで残りあと146日、ということが分かります。

次に例えば、現在時刻が「15時20分」、始業時刻が「9時0分」だとして、始業してからの経過時間を「分」で求めるためのコードは、以下のようになります。

snippet_2 (pro.kensait.java.basic.lsn_20_2_4.Main)
LocalTime start = LocalTime.of(9, 0);
LocalTime now = LocalTime.of(15, 20);
long elapsed = ChronoUnit.MINUTES.between(start, now); //【1】

この例では、分単位の計算を行うため、ChronoUnit.MINUTES列挙子のbetween()メソッドに、差分計算の対象になる2つの時刻を渡します【1】。差分計算の結果、変数elapsedには380が格納されます。つまり始業してから現在まで380分が経過した、ということが分かります。

Periodクラスによる差分計算

java.time.Periodは、日付の期間を、日付量、すなわち「〇年〇カ月〇日」という形で表すためのクラスです。前述したように2つの日付間の差分を日付量で算出する場合は、このクラスのAPIを利用します。
このクラスには数多くのAPIがありますが、差分計算に使われる主要なAPIを以下に示します。

API(メソッド) 説明
static Period between(LocalDate, LocalDate) 指定された2つの日付と日付の期間を、Periodで返す。
int getYears() この期間の年数を返す。
int getMonths() この期間の月数を返す。
int getDays() この期間の日数を返す。

それでは具体的にコードを見ていきましょう。例えば「2012年4月1日」に入社した社員の「2022年11月5日」時点での、入社からの経過日数を、「〇年〇カ月〇日」という形で求めるためのコードは、以下のようになります。

snippet_3 (pro.kensait.java.basic.lsn_20_2_4.Main)
LocalDate start = LocalDate.of(2012, 4, 1);
LocalDate today = LocalDate.of(2022, 11, 5);
Period period = Period.between(start, today); //【1】
System.out.println(period.getYears() + "年" +
        period.getMonths() + "カ月" +
        period.getDays() + "日"); //【2】

このようにbetween()メソッドに2つのLocalDateオブジェクトを渡すことで、Periodを取得します【1】。
続いてPeriodのgetYears()メソッドで年数を、getMonths()メソッドで月数を、getDays()メソッドで日数をそれぞれ取得し、それをコンソールに表示しています【2】。
このコードを実行すると、コンソールには「10年7カ月4日」と表示されます。この社員は入社から、10年7カ月と4日が経過した、ということが分かります。

Durationクラスによる差分計算

java.time.Durationは、時刻の期間を、時間量、すなわち「〇時間〇分〇秒」という形で表すためのクラスです。前述したように、2つの時刻間の差分を、時間量で算出する場合は、このクラスのAPIを利用します。
このクラスには数多くのAPIがありますが、差分計算に使われる主要なAPIを、以下に示します。

API(メソッド) 説明
static Duration between(Temporal, Temporal) 指定された2つの日時と日時の期間を、Durationで返す。
int toHoursPart() この期間の時間数を返す。
int toMinutesPart() この期間の分数を返す。
int toSecondsPart() この期間の秒数を返す。
int toNanosPart() この期間のミリ秒数を返す。

それでは具体的にコードを見ていきましょう。例えば、現在時刻が「15時20分」、タイムリミットが「21時30分」だとして、タイムリミットまでの残り時間を、「〇時間〇分」という形で求めるためのコードは、以下のようになります。

snippet_4 (pro.kensait.java.basic.lsn_20_2_4.Main)
LocalTime now = LocalTime.of(15, 20);
LocalTime timeLimit = LocalTime.of(21, 30);
Duration duration = Duration.between(now, timeLimit); //【1】
System.out.println(duration.toHoursPart() + "時間" +
        duration.toMinutesPart() + "分"); //【2】

このようにbetween()メソッドに2つのLocalTimeを渡すことで、Durationを取得します【1】。
続いてDurationのtoHoursPart()メソッドで時間数を、toMinutesPart()メソッドで分数をそれぞれ取得し、それをコンソールに表示しています【2】。
このコードを実行すると、コンソールには「6時間10分」と表示されます。タイムリミットまで残りあと6時間10分である、ということが分かります。

20.2.5 タイムゾーンありの日時クラス

タイムゾーンありの日時クラス概要

これまでのレッスンで扱ってきたローカル日時を表すクラスにはタイムゾーンという概念はありませんでしたが、Date and Time APIにはタイムゾーンありの日時クラスがあります。
タイムゾーンありの日時とは、国際的な基準時刻である「グリニッジ標準時刻(GMT)」を基準にした日時を意味します。

例えば東京におけるローカル日時「2022年11月5日 15時20分」をタイムゾーンありの日時で表すと、「2022年11月5日 15時20分+9時間」になります。東京というタイムゾーンでは、グリニッジ標準時刻とプラス9時間の時差があるため「+9時間」といった形で時差を表現します。
またニューヨークにおけるローカル日時「2022年11月5日 15時20分」をタイムゾーンありの日時で表すと、「2022年11月5日 15時20分-4時間」になります。ニューヨークというタイムゾーンでは、グリニッジ標準時刻とマイナス4時間の時差があるため「ー4時間」といった形で時差を表現します。なおグリニッジ標準時刻とニューヨークの時差は通常はマイナス5時間ですが、アメリカでは11月5日はぎりぎりサマータイム期間中なので、この例ではマイナス4時間になります。
いずれにしてもタイムゾーンありの日時では、同じ「2022年11月5日 15時20分」であっても、東京とニューヨークとでは異なる日時として扱われます。

このようにタイムゾーンありの日時クラスは、時差や地域といった情報を日時情報の一環として管理します。例えば1つのアプリケーションやサービスをグローバルに展開し、時差を意識した処理を行う必要がある場合には、これらのクラスを利用すると良いでしょう。

なおタイムゾーンありの日時クラスには、OffsetDateTimeクラス、OffsetTimeクラス、ZonedDateTimeクラスの3つがあります。このうちOffsetDateTimeクラス、OffsetTimeクラスは時差情報のみを保持し、ZonedDateTimeクラスは時差情報と地域情報を保持します。

ZonedDateTimeクラスのAPI

ここでは3つあるタイムゾーンありの日時クラスの中から、汎用性が高いZonedDateTimeクラスの主要なAPIについて説明します。OffsetDateTimeクラスとOffsetTimeクラスのAPIも基本的な使い方は同様なので、本コースでは割愛します。
まずZonedDateTimeオブジェクトは、以下のようなファクトリメソッドで生成します。

API(メソッド) 説明
static ZonedDateTime now() (システムクロックによる)現在日時を表すZonedDateTimeオブジェクトを返す。
static ZonedDateTime of(int, int, int, int, int, int, int,ZoneId) 指定された年、月、日、時間、分、秒、ナノ秒、ゾーンIDから、ZonedDateTimeオブジェクトを生成して返す。
static ZonedDateTime of(LocalDateTime, ZoneId) 指定されたローカル日時、ゾーンIDから、ZonedDateTimeオブジェクトを生成して返す。

of()メソッドに指定するゾーンIDは、java.time.ZoneIdクラスのオブジェクトです。
ZoneIdクラスのオブジェクトは、以下のようなファクトリメソッドによって生成します。

API(メソッド) 説明
static ZoneId systemDefault() システムのデフォルトタイムゾーンを表すZoneIdオブジェクトを返す。
static ZoneId of(String) 指定されたゾーンIDを表すZoneIdオブジェクトを返す。

例えばデフォルトタイムゾーンにおける日時を表すZonedDateTimeは、以下のようにオブジェクト生成します。

snippet_1 (pro.kensait.java.basic.lsn_20_2_5.Main)
ZonedDateTime dateTime = ZonedDateTime.of(2022, 11, 5, 15, 20, 0, 0,
        ZoneId.systemDefault());

次にタイムゾーンを明示する場合ですが、そのためにはof()メソッドを使います。of()メソッドには、ゾーンIDを表す所定の文字列を指定します。この文字列は、東京であれば"Asia/Tokyo"、ニューヨークであれば"America/New_York"になりますが、どのようなゾーンIDがあるのかは「APIリファレンス」を参照ください。
例えば東京のタイムゾーンにおける日時を表すZonedDateTimeは、以下のようにオブジェクト生成します。

snippet_2 (pro.kensait.java.basic.lsn_20_2_5.Main)
ZonedDateTime tkyDateTime = ZonedDateTime.of(2022, 11, 5, 15, 20, 0, 0,
        ZoneId.of("Asia/Tokyo"));

生成した東京のZonedDateTimeオブジェクトを文字列に変換すると、以下のようになります。

2022-11-05T15:20+09:00[Asia/Tokyo]

このように、時差(+09:00)と地域([Asia/Tokyo])を保持していることが分かります。
なおZonedDateTimeクラスには、LocalDateTimeクラスと同様のAPIが備わっています。そのため、年、月、日といった属性を取得したり、日時の加減算を行ったりすることも可能です。

ZonedDateTimeクラスによる時差の計算

前述したようにタイムゾーンありの日時では、「2022年11月5日 15時20分」であっても、東京とニューヨークとでは異なる日時として扱われます。具体的に両者の時差をコードで確認してみましょう。

snippet_3 (pro.kensait.java.basic.lsn_20_2_5.Main)
LocalDateTime dateTime = LocalDateTime.of(2022, 11, 5, 15, 20, 0, 0);
ZonedDateTime tkyDateTime =
        ZonedDateTime.of(dateTime, ZoneId.of("Asia/Tokyo")); //【1】
ZonedDateTime nycDateTime =
        ZonedDateTime.of(dateTime, ZoneId.of("America/New_York")); //【2】
long diff = ChronoUnit.HOURS.between(tkyDateTime, nycDateTime); //【3】

このように東京における「11月5日 15時20分」と、ニューヨークにおける「11月5日 15時20分」のZonedDateTimeオブジェクトをそれぞれ生成します【1、2】。ニューヨークの「11月5日 15時20分」を表すZonedDateTimeオブジェクトは、サマータイムも勘案された上で、グリニッジ標準時刻との時差はマイナス4時間に設定されます。
次にChronoUnit列挙型を利用して、時差を計算します【3】。すると変数diffには、東京とニューヨークの時差である13が格納されます。

ZonedDateTimeとローカル日時を表すクラスとの相互変換

ZonedDateTimeクラスは、必要に応じてローカル日時を表すクラスに変換することができます。
ZonedDateTimeクラスからローカル日時クラスへの変換は、ZonedDateTimeクラスの以下のAPIによって行います。

API(メソッド) 説明
LocalDateTime toLocalDateTime() このZonedDateTimeをLocalDateTimeに変換する。
LocalDate toLocalDate() このZonedDateTimeのLocalDate部分を返す。
LocalTime toLocalTime() このZonedDateTimeのLocalTime部分を返す。

またLocalDateTimeクラスからZonedDateTimeクラスへの変換は、ZonedDateTimeクラスの既出のof()メソッド(スタティックメソッド)に、変換対象のLocalDateTimeオブジェクトを指定することで生成します。

Date and Time APIと古典的な日時クラスとの相互変換

チャプター20.1でも触れたように、今後の新規開発で日時を扱う場合はDate and Time APIを使うべきですが、古典的な日時クラスとの互換性確保のために、相互に変換が必要なケースがあります。
ここではLocalDateTimeクラスとDateクラスとの、相互変換の方法を紹介します。なおこのレッスンで取り上げている理由は、変換にあたってはゾーンIDを意識する必要があるためです。
各クラスと変換APIの全体像は、以下の図のとおりです。

【図20-2-3】LocalDateTime、ZonedDateTime、Instant、Dateの相互変換
image.png

LocalDateTimeクラスとDateクラスを変換するためには、両者を中継するためにjava.time.Instantクラスが必要です。Instantクラスは「西暦1970年1月1日0時0分0秒(GMT)」からの経過時間を保持する、抽象度の高いクラスです。
まずLocalDateTimeクラスとInstantクラスを相互に変換するためのAPIから説明します。LocalDateTimeクラスからは直接Instantクラスに変換することはできず、いったんZonedDateTimeクラスへの変換が必要です。
ZonedDateTimeクラスからInstantクラスへの変換は、ZonedDateTimeクラスの以下のAPIによって行います。

API(メソッド) 説明
Instant toInstant() このZonedDateTimeをInstantに変換する。

またInstantクラスからLocalDateTimeクラスへの変換は、LocalDateTimeクラスの以下のAPIによって行います。

API(メソッド) 説明
static LocalDateTime ofInstant(Instant, ZoneId) 指定されたInstantとゾーンIDから、LocalDateTimeオブジェクトを生成して返す。

次にチャプター20.1では取り上げませんでしたが、Dateクラスにも、以下のようなInstantとの相互変換のためのAPIがあります。

API(メソッド) 説明
static Date from(Instant) 指定されたInstantから、Dateオブジェクトを生成して返す。
Instant toInstant() このDateをInstantに変換する。

それではこれらのAPIを使って、LocalDateTimeクラスからDateクラスへの変換を行います。以下のコードを見てください。

snippet_4 (pro.kensait.java.basic.lsn_20_2_5.Main)
LocalDateTime ldt = LocalDateTime.of(2022, 4, 10, 15, 20);
ZonedDateTime zdt = ZonedDateTime.of(ldt, ZoneId.systemDefault());
Instant instant = zdt.toInstant();
Date date = Date.from(instant);

このようにLocalDateTimeクラス→ZonedDateTimeクラス→Instantクラス→Dateクラスといった具合に、三段階の変換が必要です。

次にDateクラスからLocalDateTimeクラスへの変換です。以下のコードを見てください。

snippet_5 (pro.kensait.java.basic.lsn_20_2_5.Main)
Date date = new Date();
Instant instant = date.toInstant();
LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());

このようにDateクラス→Instantクラス→LocalDateTimeクラスと、二段階の変換を行います。

20.2.6 日時クラスと文字列の相互変換

日時と文字列を相互変換するためのAPI

日時を表す6つのクラス(LocalDateTimeクラス、LocalDateクラス、LocalTimeクラス、OffsetDateTimeクラス、OffsetTimeクラス、ZonedDateTimeクラス)には、日時と文字列を相互に変換するためのAPIがあります。
どのクラスにも同じ名前のAPIがありますが、ここではその中から代表としてLocalDateTimeクラスを取り上げます。
LocalDateTimeクラスにおいて、日時と文字列を相互に変換するためのAPIを以下に示します。

API(メソッド) 説明
String format(DateTimeFormatter) 指定されたフォーマッタに従って、このLocalDateTimeをフォーマッティングし、文字列として返す。
static LocalDateTime parse(CharSequence) 指定された文字列を、ISO標準のフォーマッタに従って解析し、LocalDateTimeのオブジェクトを生成し、返す。
static LocalDateTime parse(CharSequence, DateTimeFormatter) 指定された文字列を、指定されたフォーマッタに従って解析し、LocalDateTimeのオブジェクトを生成し、返す。

Date and Time APIでは、フォーマッタはjava.time.format.DateTimeFormatterクラスによって表します。
DateTimeFormatterのオブジェクトは、以下のようなファクトリメソッドによって生成します。

API(メソッド) 説明
static DateTimeFormatter ofPattern(String) 指定された日時フォーマットのパターン文字列を使用して、DateTimeFormatterオブジェクトを生成して返す。

ofPattern()メソッドには、"yyyy/M/d H:m:s:n"のような日時フォーマットのパターン文字列を指定します。パターン文字列の代表的な記法には、西暦年はy、月はM、月の日はd、時(24時間制)はH、時(12時間制)はh、分はm、秒はs、ナノ秒はn、といったものがあります。また、M、d、H、h、m、sといった記法を、MM、dd、HH、hh、mm、ssといった具合に二文字にすると、10未満のときに先頭に0が追加されます。
次項からは、これらのAPIの使用方法について具体的に説明していきます。

日時から文字列への変換(フォーマッティング)

ここでは、LocalDateTimeクラスを文字列に変換する処理を、具体的に見ていきます。以下は「2022年4月5日8時5分3秒123456789ナノ秒」を表すLocalDateTimeを、フォーマッティングして文字列に変換するためのコードです。

snippet_1 (pro.kensait.java.basic.lsn_20_2_6.Main)
LocalDateTime date = LocalDateTime.of(2022, 4, 5, 8, 5, 3, 123456789);
String dateStr1 = date.format(DateTimeFormatter.ofPattern("y年M月d日 H時m分s秒"));
String dateStr2 = date.format(DateTimeFormatter.ofPattern("y/MM/dd HH:mm:ss:n"));

変数dateStr1には、指定されたパターン文字列"y年M月d日 H時m分s秒"に従って、文字列"2022年4月5日 8時5分3秒"が格納されます。また同じように変数dateStr2には、指定されたパターン文字列"y/MM/dd HH:mm:ss:n"に従って、文字列"2022/04/05 08:05:05:123456789"が格納されます。

文字列から日時への変換(解析)

ここでは文字列を解析し、LocalDateTimeオブジェクトを生成する処理を、具体的に見ていきます。
文字列を解析してLocalDateTimeオブジェクトを生成するためには、parse()メソッドを呼び出します。
フォーマッタとしてISO標準を採用する場合は、parse()メソッドに文字列のみを指定します。以下は「2022年4月5日8時5分3秒」を表す文字列を、ISO標準のフォーマッタで表し、それを解析してLocalDateTimeオブジェクトを生成するコードです。

snippet_2 (pro.kensait.java.basic.lsn_20_2_6.Main)
LocalDateTime date = LocalDateTime.parse("2022-04-05T08:05:03");

また任意のフォーマッタを採用する場合は、parse()メソッドに文字列とフォーマッタを指定します。以下は「2022年4月5日8時5分3秒」を表す文字列を独自のフォーマッタで表し、それを解析してLocalDateTimeオブジェクトを生成するコードです。

snippet_3 (pro.kensait.java.basic.lsn_20_2_6.Main)
LocalDateTime date = LocalDateTime.parse("2022年4月5日 8時5分3秒",
        DateTimeFormatter.ofPattern("y年M月d日 H時m分s秒"));

なおparse()メソッドの呼び出しでは、指定された文字列のフォーマットが不正な場合にDateTimeParseExceptionが送出されますが、このクラスは非チェック例外のため例外ハンドリングは任意です。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. Date and Time APIによって提供されるの日時クラス全体像について。
  2. ローカル日時のためのクラス(LocalDateTime、LocalDate、LocalTime)の特徴やAPIについて。
    ・オブジェクト生成のためのファクトリメソッド
    ・日時を設定するためのAPI
    ・日時を取得するためのAPI
    ・日時を加減算するためのAPI
    ・日時の前後比較をするためのAPI
  3. 日時の差分計算をする方法について。
  4. タイムゾーンありの日時クラス(ZonedDateTime)の特徴やAPIについて。
  5. 日時と文字列を相互変換するためのAPIについて。

Discussion