やり直しJava

日付と時刻

Java SE 8までは 日付・時刻の処理を行う場合は 主にjava.utilパッケージのDateクラス、Calendarクラス、SimpleDateFormatクラスなどが使われてきました。Java SE 8からDate and Time APIが追加され、今後はDate and Time APIを使うことが推奨されています。Date and Time APIの特徴は次の通りです。

  • 日付・時刻を保持するクラスとして、タイムゾーン情報の有無や日本の元号に対応したものなど、複数の中から選べるようになりました。また、一部のクラスについては、日付だけ または時刻だけを保持するクラスも選べるようになりました。
  • Java SE 8以前では Date、Calendar、SimpleDateFormatと複数のクラスを駆使して日付や時刻を処理する必要がありましたが、Date and Time APIでは 一つのクラスで日付・時刻の処理ができるようになりました(場合によっては複数のクラスを駆使します)。
  • 日付・時刻を保持するクラスは不変クラスです。(DateやCalendarは可変クラスで 使いまわしによるバグを引き起こす可能性がありました。)不変クラスであるため、日付・時刻を進めたり遅らせたりするメソッドは 自身を変更せずに新たなインスタンスを作成して返すことになります。
  • 細かいけれども重要な点ですが、月の指定が0オリジンではなく、1オリジンで指定できるようになりました(DateやCalendarは 0が1月、1が2月を表します)。

Date and Time APIを用いた日付と時刻の処理について解説していきます。

タイムゾーンとオフセット

日本における時刻を タイムゾーン情報を含めて表示すると次のようになります。

2018-07-12 12:34:56 +9:00 Asia/Tokyo

「+9:00」の部分は UTCからのオフセットを表し、「Asia/Tokyo」の部分は 地域に基づいたタイムゾーンを表します。タイムゾーンと言ったとき、このオフセットと地域に基づいたタイムゾーンの両方を指し示す場合がありますが、ここでは便宜上、 「+9:00」を「固定オフセット」、「Asia/Tokyo」を「タイムゾーン」と呼ぶことにします。

固定オフセットを用いると 日付・時刻を一意に定めることができます。一方で タイムゾーンだけだと 日付・時刻を一意に定めることができない場合があります。一意に定めることができない例として、サマータイムの切り替えの前後1時間が挙げられます。現在の日本では サマータイムが導入されていないので、ニューヨークを例に挙げます(過去には日本でも サマータイムが導入されていた時期がありました)。

ニューヨークは 2017年3月12日の2時に冬時間から夏時間に切り替わりました。冬時間から夏時間へ切り替わるときの時刻の変遷は 次のようになります。()内は固定オフセットです。

2017-03-12 01:59:58(-5:00)
2017-03-12 01:59:59(-5:00)
2017-03-12 03:00:00(-4:00)

1時59分59秒の次は 3時0分0秒になりますが、これは時刻が1時間進むと同時に 固定オフセットが1時間変更になることで辻褄が合う形になっています。

一方で、夏時間から冬時間に切り替わるときの時刻の変遷は 次のようになります。ニューヨークでは 2017年11月5日の2時に夏時間から冬時間に切り替わりました。()内は固定オフセットです。

2017-11-05 01:59:58(-4:00)
2017-11-05 01:59:59(-4:00)
2017-11-05 01:00:00(-5:00)

1時59分59秒の次は 1時0分0秒になり 同時に固定オフセットが1時間変更されます。固定オフセットを見ないと1:00:00~1:59:59が二度繰り返されることになります。2017年11月05日の1時0分0秒は2回刻まれるため、ニューヨークというタイムゾーン情報だけでは どちらか区別することができず、固定オフセットが-4:00なのか-5:00なのかの情報が必要になります。

タイムゾーン呪いの書」(Qiita)

主なクラス

Date and Time APIの中心となるクラスは次の通りです。

Date and Time APIの主なクラス
日付・時刻クラス日付のみ時刻のみ概要
LocalDateTimeLocalDateLocalTimeタイムゾーン情報を持たないクラスです。クラスメソッドnow()で現在の日付・時刻を取得すると、システムデフォルトのタイムゾーン情報に基づいた日付・時刻を取得しますが、生成されたオブジェクトにはタイムゾーン情報が含まれません。
OffsetDateTimeOffsetTime固定オフセット(UTCからの時差)情報を持つクラスです。日本の場合は”+09:00″です。
ZonedDateTimeタイムゾーン情報を持つクラスです。前述の通りタイムゾーン情報だけでは時刻を一意に特定できない場合があるので、固定オフセット情報も併せて持っています。日本の場合は”Asia/Tokyo”、”+9:00″です。
ZonedDateTimeはタイムゾーン情報ZoneIdを持っています。サマータイムを導入している国では サマータイム期間とそうでない期間で固定オフセット情報が変わりますが、ZoneIdから サマータイム等で固定オフセットがどのように変わったかといった情報を参照することもできます。ちなみに日本も過去にサマータイムを導入していた時期があり、ZoneIdから参照することができます。

LocalDateTimeは時刻を一意に特定できないので、例えば あるタイムゾーンでログを記録して、別タイムゾーンで そのログを解析しようとすると、どちらの時刻なのか分かりづらく混乱を招きま。そのため、使用メモリを抑えたいなどの特別な要件がない限りは ffsetDateTimeやZonedDateTimeを使う方が無難です

OffsetDateTimeは時刻を一意に特定できます。

ZonedDateTimeは クラスメソッドof()などで日付・時刻を指定する際に、サマータイムの切り替えの前後で存在しない時刻や 二度繰り返される時刻を指定することができてしまいますが、それぞれ次のように適切に対処されます。

  • 冬時間から夏時間の切り替わり(時刻が進む)のタイミングで 存在しない時刻を指定できてしまいますが その場合は存在する時刻に補正されます。例えば 前述のニューヨークの例で 存在しない時刻”2017-03-12 02:01:00″を指定すると “2017-03-12 03:01:00″に補正されます。
  • 逆に 夏時間から冬時間の切り替わり(時刻が戻る)のタイミングで 二度繰り返される時刻を指定した場合は 早い方の時刻を指定したとみなされます。同じ例で “2017-11-05 01:00:00″を指定すると 固定オフセット-4:00を指定したとみなされます。ZonedDateTimeオブジェクトのwithLaterOffsetAtOverlap()メソッドを使うと、遅い時刻の方(固定オフセット-5:00)に変更したオブジェクトを取得することができます。

サマータイムを考慮しなくて良いため、OffsetDateTimeの方がシンプルに扱うことができます。一方で、サマータイムを扱う場合は ZonedDateTimeを使う方便利です。 ZondedDateTimeを使うと サマータイムを考慮した時間計算を自動で行ってくれます。例えば 前述のニューヨークの例で”2017-03-12 01:59:59″を表すZonedDateTimeオブジェクトに対して 1秒先の時刻を返すplusSeconds(1) を呼び出すと、”2017-03-12 03:00:00″ と サマータイムを考慮した時刻を返してくれます。

2013-09-17(火) Java8日付時刻APIの使いづらさと凄さ」(きしだのはてな)

主な使い方

LocalDateTime・OffsetDateTime・ZonedDateTimeとどれを使っても時刻に関する操作は同じになりますので、コード例ではZonedDateTimeを扱っています。

現在時刻の取得

現在時刻を取得するには 各日付・時刻クラスのクラスメソッドnow()を使います。

ZonedDateTime dt1 = ZonedDateTime.now();

指定した日付・時刻のインスタンス取得

指定した日付・時刻のインスタンスを取得するには 各日付・時刻クラスのクラスメソッドof()を使います。

ZonedDateTime dt = ZonedDateTime.of(2017, 3, 12, 1, 59, 59, 0, ZoneId.of("Asia/Tokyo"));

各フィールドの取得

各フィールドを取得するには 各日付・時刻クラスのインスタンスメソッドgetXX()を使います。

ZonedDateTime dt = ZonedDateTime.now();
System.out.println(dt.getYear() + "年" + dt.getMonth() + "月" + dt.getDayOfMonth() + "日");
System.out.println(dt.getHour() + "時" + dt.getMinute() + "分" + dt.getSecond() + "秒" + dt.getNano() + "ナノ秒");

日付・時刻の加減算・一部変更

日付・時刻の加減算を行うには 各日付・時刻クラスのインスタンスメソッドplusXX()、minusXX()を使います。日付・時刻クラスは不変クラスなので、あくまで加減算が行われた別のインスタンスが返され 自分自身は変更されないことに注意してください。

ZonedDateTime dt = ZonedDateTime.of(2018, 5, 3, 12, 30, 0, 0, ZoneId.of("Asia/Tokyo"));
System.out.println(dt.plusDays(30));    // 2018-06-02T12:30+09:00[Asia/Tokyo]
System.out.println(dt.minusHours(15));  // 2018-05-02T21:30+09:00[Asia/Tokyo]

また、日付・時刻の一部のフィールドを変更したい場合は、withXX()メソッドを使います。「来月の1日」といった日付・時刻を算出するときに便利です。

ZonedDateTime dt = ZonedDateTime.of(2018, 5, 3, 12, 30, 0, 0, ZoneId.of("Asia/Tokyo"));
System.out.println(dt.plusMonth(1).withDayOfMonth(1));  // 2018-06-01T12:30+09:00[Asia/Tokyo]

日付・時間の間隔の算出

日付間隔を算出するには Periodクラスのbetween()クラスメソッドを使います。

ZonedDateTime past = ZonedDateTime.of(2017, 6, 3, 9, 10, 35, 0, ZoneId.systemDefault());
ZonedDateTime future = ZonedDateTime.of(2018, 7, 12, 23, 30, 40, 0, ZoneId.systemDefault());

Period period = Period.between(past.toLocalDate(), future.toLocalDate());
System.out.println(period);  // P1Y1M9D
System.out.println(period.getYears() + "年" + period.getMonths() + "月" + period.getDays() + "日");  // 1年1月9日

時間間隔を算出するには Durationクラスの同じくbetween()クラスメソッドを使います。

Duration duration = Duration.between(past, future);
System.out.println(duration);  // PT9710H20M5S
System.out.println(duration.toDaysPart() + "日" + duration.toHoursPart() + "時間"
        + duration.toMinutesPart() + "分" + duration.toSecondsPart() + "秒");  // 404日14時間20分5秒
System.out.println("日単位" + duration.toDays());  // 日単位404 (34957205 / (24 * 3600))
System.out.println("時単位" + duration.toHours());  // 時単位9710 (34957205 / 3600)
System.out.println("分単位" + duration.toMinutes());  // 分単位582620 (34957205 / 60)
System.out.println("秒単位" + duration.toSeconds());  // 秒単位34957205

何日何時間何分何秒の形で取得した場合は toXXPart()メソッドを使います。秒単位、分単位、時間単位で全体の間隔を知りたい場合は toXX()メソッドを使います。

出力形式の指定

java.util.DateのtoString()は「Thu Jul 4 12:30:45 JST 2018」という形式の文字列が返り そのままでは使いづらいものでした。Date and Time APIの各日付・時刻クラスは toString()メソッドを呼ぶと ISO 8601形式の文字列が返るようになりましたので、そのまま出力しても日付・時刻が分かり易くなりました

System.out.printl(ZonedDateTime.now()); // 2018-05-24T12:30+09:00[Asia/Tokyo]

toString()とは異なる形式で出力したい場合は DateTimeFormatterを使います。DateTimeFormatterでは幾つかの書式がクラス定数として定義されています。

ZonedDateTime dt = ZonedDateTime.of(2018, 5, 3, 12, 30, 0, 0, ZoneId.of("Asia/Tokyo"));
System.out.println(dt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)));
   // 2018年5月3日木曜日 12時30分00秒 日本標準時

独自の書式を指定したい場合は DateTimeFormatterクラスのofPattern()クラスメソッドを使って指定します。

ZonedDateTime dt = ZonedDateTime.of(2018, 5, 3, 12, 30, 0, 0, ZoneId.of("Asia/Tokyo"));
System.out.println(dt.format(DateTimeFormatter.ofPattern("yyyy/MM/dd hh:mm:ss"))); // 2018/05/03 12:30:00

文字列から日付・時刻クラスインスタンスの生成

文字列から日付・時刻クラスのインスタンスを生成するには DateTimeFormatterのofPatter()でDateTimeFormatterインスタンスを生成して、各日付・時刻クラスのクラスメソッドparse()を使います。

System.out.println(ZonedDateTime.parse("2018-06-24 20:35:42.123456789 +09:00 Asia/Tokyo",
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.nnnnnnnnn xxx VV"));

旧 Date、Calendarとの互換性

Date and Time APIは 既存のDateやCalendarを置き換えることを目標に導入されましたが、既存のライブラリが Dateを引数や戻り値としている場合などに変換が必要となる場面が出てきます。DateとDate and Time APIの日付・時刻クラスの相互変換は Instantクラスのオブジェクトを介して行います。Dateクラスにも Instantを返すtoInstant()メソッドと Instantを受け入れるfrom()メソッドが追加されました。

Date → 日付・時刻クラス

DateをDate and Time APIの日付・時刻クラスに変換するには DateのtoInstance()メソッドでInstantを得て、日付・時刻クラスのofInstant()クラスメソッドに渡します。

Date date = new Date();
ZonedDateTime dt = ZonedDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());

日付・時刻クラス → Date

反対に Date and Time APIの日付・時刻クラスをDateに変換するには 日付・時刻クラスのtoInstant()メソッドでInstantを得て、Dateクラスのfrom()メソッドに渡します。

ZonedDateTime dt = ZonedDateTime.now();
date = Date.from(dt.toInstant());

Java8の日時APIはとりあえずこれだけ覚えとけ」(Qiita)
続・今日から始めるJava8 – JSR-310 Date and Time API<」(Taste of Tech Topics)