LoginSignup
43
45

Date and Time API 復習

Last updated at Posted at 2015-07-19

【Date and Time API】Java 8徹底再入門【ラムダ式ハンズオン】(大阪,7/11) - connpass で Date and Time API について学んだので復習。

ラムダはハンズオン形式で実際に手を動かせていたけれど、 Date and Time API の方は講義形式だったので、話を聞いて気になったところとかを実際に触ってみる。

要点整理

  • Date and Time API (JSR310)は ISO 8601 をモデリングして作られたもの。
    • ISO 8601 は、コンピュータ間でデータのやりとりをする際の、日付と時刻の書式に関する国際規格
    • ISO 8601 について知らないと、 Date and Time API を正しく扱えないことがある。
    • といっても、 ISO 8601 について知っておくべきことは、そんなに多くない。
    • 発表スライドに挙げられている表記法くらいを理解していれば大丈夫。
  • ISO 8601 は固定長の電文で使われることを想定しているので、桁数に対して厳格。
    • 2014-7-2 みたいな省略はダメ。
    • 区切り文字(:-)は省略可(むしろ、無いのが標準)。
    • 他システムとの連携を考えた場合、標準に従う方がベター。
  • 年月日の書式は、 yyyy-MM-dd2015-07-18)。
    • 時分秒の書式は、 HH:mm:ss12:11:02)。
    • 年月日の表記と時分秒以下の表記を結合するときは、 T で連結する(2015-07-13T23:50:00)。
  • ISO 8601 では、タイムゾーンの表現にオフセットを使う。
    • オフセットは、協定世界時からの差(±HH:mm)を時刻の後ろに付けることで表現する。
    • 日本は、協定世界時から9時間早いので、 +09:00 と表記する(2015-07-13T23:50:00+09:00)。
    • 協定世界時の場合は、 Z を付ける(2015-07-13T23:50:00Z)。
  • tz databse というものがあり、世界中のタイムゾーンがデータベース化されている。
    • tz database では、タイムゾーンに Asia/Tokyo のような名前を割り当てて管理している。
    • 夏時間も考慮されている。
    • 夏時間の期間は変更されることがあるので、データベースの内容は、しばしば更新されることがある。
  • Date and Time API には、時間を表すクラスが大きく3種類存在する。
クラス タイムゾーン 夏時間 ISO 8601
Local* なし 非サポート 準拠
Offset* オフセットで保持 非サポート 準拠
ZonedDateTime tz database て定義されている名称(Asia/Tokyo など)で保持 サポート 非準拠
  • Local*LocalDateTime, LocalDate, LocalTime)は、タイムゾーンが必要ないときに利用する。
  • Offset*OffsetDateTime, OffsetTime) は、タイムゾーンが必要なときに利用する。ISO 8601 に準拠している。夏時間の考慮はされない。
  • ZonedDateTime は、同じくタイムゾーンを持ち、夏時間が考慮される。
    ただし、 tz database が更新されたら、それに追随して JDK のバージョンも上げる必要がある。また、 ISO 8601 には準拠していない。
  • JDK のバージョンをおいそれと上げられない環境では、 ZonedDateTime の利用はリスキー。
  • 各クラスは、簡単に相互変換できる。
  • Date and Time API が提供するクラスはイミュータブル(不変)で設計されている。
    • withYear() などのメソッドは、指定した値でフィールドを書き換えた新しいインスタンスを返す。

Hello World

package sample.dta;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;

public class Main {
    
    public static void main(String[] args) {
        String msg =
            "LocalDate      : " + LocalDate.now() + "\n" +
            "LocalTime      : " + LocalTime.now() + "\n" +
            "LocalDateTime  : " + LocalDateTime.now() + "\n" +
            "OffsetTime     : " + OffsetTime.now() + "\n" +
            "OffsetDateTime : " + OffsetDateTime.now() + "\n" +
            "ZonedDateTime  : " + ZonedDateTime.now()
        ;
        
        System.out.println(msg);
        
    }
}
実行結果
LocalDate      : 2015-07-14
LocalTime      : 22:48:39.679
LocalDateTime  : 2015-07-14T22:48:39.679
OffsetTime     : 22:48:39.679+09:00
OffsetDateTime : 2015-07-14T22:48:39.679+09:00
ZonedDateTime  : 2015-07-14T22:48:39.680+09:00[Asia/Tokyo]
  • now() という static なファクトリメソッドが用意されており、現在時刻に対応するインスタンスを取得できる。
  • Local*Offset* クラスのインスタンスは、 toString() すると ISO 8601 に準拠した書式で文字列を返す。

日付文字列からインスタンスを生成する

package sample.dta;

import java.time.LocalDate;
import java.time.OffsetDateTime;

public class Main {
    
    public static void main(String[] args) {
        LocalDate localDate = LocalDate.parse("2015-07-15");
        System.out.println(localDate);
        
        OffsetDateTime offsetDateTime = OffsetDateTime.parse("2015-07-15T22:50:00+09:00");
        System.out.println(offsetDateTime);
    }
}
実行結果
2015-07-15
2015-07-15T22:50+09:00
  • parse() という static なメソッドが用意されており、 ISO 8601 の書式で書かれた日付文字列からインスタンスを生成できる。

厳密にフォーマットをチェックする

package sample.dta;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;

public class Main {
    
    public static void main(String[] args) {
        DateTimeFormatter formatter = DateTimeFormatter
                                        .ofPattern("yyyy-MM-dd")
                                        .withResolverStyle(ResolverStyle.STRICT);
        
        System.out.println(LocalDate.parse("2015-12-03", formatter));
    }
}
実行結果
Exception in thread "main" java.time.format.DateTimeParseException: Text '2015-12-03' could not be parsed: Unable to obtain LocalDate from TemporalAccessor: {MonthOfYear=12, DayOfMonth=3, YearOfEra=2015},ISO of type java.time.format.Parsed
	at java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:1918)
	at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1853)
	at java.time.LocalDate.parse(LocalDate.java:400)
	at sample.dta.Main.main(Main.java:12)
Caused by: java.time.DateTimeException: Unable to obtain LocalDate from TemporalAccessor: {MonthOfYear=12, DayOfMonth=3, YearOfEra=2015},ISO of type java.time.format.Parsed
	at java.time.LocalDate.from(LocalDate.java:368)
	at java.time.LocalDate$$Lambda$7/1023892928.queryFrom(Unknown Source)
	at java.time.format.Parsed.query(Parsed.java:226)
	at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1849)
	... 2 more
  • DateTimeFormatter を作り、 parse() メソッドの第二引数に渡す。
  • DateTimeFormatter は、 withResolverStyle(ResolverStyle.STRICT) で厳密なチェックを行うように指定できる。
    • DateTimeFormatter もイミュータブルなので、新しいフォーマッターが return される。
  • 厳密なフォーマットの場合、 yyyy という書式指定は間違いで、 Gyyyy が正しい。
Gyyyyを指定した場合
package sample.dta;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;

public class Main {
    
    public static void main(String[] args) {
        DateTimeFormatter formatter = DateTimeFormatter
                                        .ofPattern("Gyyyy-MM-dd")
                                        .withResolverStyle(ResolverStyle.STRICT);
        
        System.out.println(LocalDate.parse("西暦2015-12-03", formatter));
    }
}
実行結果
2015-12-03
  • 厳密なフォーマットの場合、西暦を単純な数値として扱うには uuuu とするのが正しい。
uuuuを指定した場合
package sample.dta;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;

public class Main {
    
    public static void main(String[] args) {
        DateTimeFormatter formatter = DateTimeFormatter
                                        .ofPattern("uuuu-MM-dd")
                                        .withResolverStyle(ResolverStyle.STRICT);
        
        System.out.println(LocalDate.parse("2015-12-03", formatter));
    }
}
実行結果
2015-12-03

ビルトインのフォーマッター

package sample.dta;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Main {
    
    public static void main(String[] args) {
        LocalDateTime localDateTime = LocalDateTime.parse("2015-07-15T10:00:00", DateTimeFormatter.ISO_DATE_TIME);
        
        System.out.println(localDateTime);
    }
}
  • DateTimeFormatter には、 static フィールドで複数のフォーマッターが定義されている。
  • DateTimeFormatterSimpleDateFormat とは違いイミュータブルでスレッドセーフなので、同じインスタンスを共有できる。

フォーマットを指定して文字列に変換する

package sample.dta;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class Main {
    
    public static void main(String[] args) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
        
        System.out.println(LocalDate.now().format(formatter));
    }
}
実行結果
2015/07/14
  • DateTimeFormatter.ofPattern() を使えば、書式指定でフォーマッターを作ることができる。
  • format() メソッドの引数にフォーマッターを渡すことで、日付オブジェクトを任意の書式の文字列に変換できる。

同時刻(日付)の比較

package sample.dta;

import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.chrono.JapaneseDate;
import java.time.chrono.JapaneseEra;

public class Main {
    
    public static void main(String[] args) {
        checkLocalDate();
        checkOffsetDateTime();
    }
    
    private static void checkLocalDate() {
        System.out.println("### checkLocalDate()");
        
        LocalDate a = LocalDate.parse("2015-07-01");
        LocalDate b = LocalDate.parse("2015-07-01");
        JapaneseDate jp = JapaneseDate.of(JapaneseEra.HEISEI, 27, 7, 1);
        
        System.out.println("a.isEqual(b)  : " + a.isEqual(b));
        System.out.println("a.equals(b)   : " + a.equals(b));
        System.out.println("a.isEqual(jp) : " + a.isEqual(jp));
        System.out.println("a.equals(jp)  : " + a.equals(jp));
    }
    
    private static void checkOffsetDateTime() {
        System.out.println("### checkOffsetDateTime()");
        
        OffsetDateTime a = OffsetDateTime.parse("2015-07-01T09:00:00+09:00");
        OffsetDateTime b = OffsetDateTime.parse("2015-07-01T04:00:00+04:00");
        OffsetDateTime c = OffsetDateTime.parse("2015-07-01T04:00:00+04:00");
        
        System.out.println("a.isEqual(b) : " + a.isEqual(b));
        System.out.println("a.equals(b)  : " + a.equals(b));
        System.out.println("b.equals(c)  : " + b.equals(c));
    }
}
実行結果
### checkLocalDate()
a.isEqual(b)  : true
a.equals(b)   : true
a.isEqual(jp) : true
a.equals(jp)  : false
### checkOffsetDateTime()
a.isEqual(b) : true
a.equals(b)  : false
b.equals(c)  : true
  • isEqual() メソッドは、暦の種類(西暦や和暦)やタイムゾーンに関係なく、時間軸上の同じ日時を表しているかどうかをチェックする。
  • equals() メソッドは、 Local* クラス同士なら、同じ日時かどうか比較できる。
  • 要は、同じ日時かどうかチェックしたい場合は isEqual() メソッドを使う。

Date との相互変換

Date -> LocalDateTime

package sample.dta;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;

public class Main {
    
    public static void main(String[] args) {
        Date date = new Date();
        Instant instant = date.toInstant();
        LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneOffset.ofHours(9));
        System.out.println(localDateTime);
    }
}
実行結果
2015-07-15T22:35:07.354

LocalDateTime -> Date

package sample.dta;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;

public class Main {
    
    public static void main(String[] args) {
        LocalDateTime localDateTime = LocalDateTime.now();
        Instant instant = localDateTime.toInstant(ZoneOffset.ofHours(9));
        Date date = Date.from(instant);
        System.out.println(date);
    }
}
実行結果
Wed Jul 15 22:36:49 JST 2015
  • Date and Time API には、 Date との直接的な変換方法が用意されていない。
  • 代わりに、 Date の方に Date and Time API が提供する Instant と相互変換するためのメソッド(from(Instant)toInstant())が追加されている。
  • Instant とは、時間軸上のある時点を表すクラスで、 1970年1月1日の0時0分からの経過時間(エポック秒)をナノ秒の精度で持つ。
  • 一方、 Date は 1970年1月1日の0時0分からの経過時間をミリ秒の精度で持つ。
  • Instant が最も Date に似ており、この Instant を経由して変換を行う。

TemporalAdjuster

package sample.dta;

import java.time.LocalDate;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjusters;

public class Main {
    
    public static void main(String[] args) {
        Temporal temporal = LocalDate.now();
        System.out.println(temporal);
        
        Temporal lastDayOfMonth = temporal.with(TemporalAdjusters.lastDayOfMonth());
        System.out.println(lastDayOfMonth);
    }
}
実行結果
2015-07-15
2015-07-31
  • TemporalAdjuster は、ある Temporal (時間的オブジェクト)から別の Temporal を取得(生成)するための方法(Strategy)を提供するためのインターフェース。
  • ストラテジーパターンの、ストラテジーに当たる。
  • よく使う TemporalAdjuster (月の最終日を取得する)は、 TemporalAdjusters の static メソッドで取得できる。

自作する

package sample.dta;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;

public class Main {
    
    public static void main(String[] args) {
        Temporal temporal = LocalDate.now();
        System.out.println(temporal);
        
        Temporal nextDay = temporal.with(temp -> {
            return temp.plus(1, ChronoUnit.DAYS);
        });
        System.out.println(nextDay);
    }
}
実行結果
2015-07-15
2015-07-16
  • 翌日を取得する TemporalAdjuster を自作してみている。
  • TemporalAdjuster は関数型インターフェースなので、ラムダ式で宣言できる。

夏時間に対応させる

担当するシステムが四半期に一度Javaとtzdbをきちんとアップデートするようスケジューリングできるか?またそのスケジューリングでシステムの挙動に支障はないか?ということ。このいずれかが「否」であればZonedDateTimeは使用すべきではないです。

対案としては、OffsetDateTimeを採用した上で、外部から夏時間等のシフト情報を供給されるTemporalAdjuster実装を作成し、それを適用することになるでしょう。

ということで、夏時間に対応した TemporalAdjuster を作ってみる。

EastAmericaSummerTimeAdjuster.java
package sample.dta;

import java.time.DayOfWeek;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;

/**
 * アメリカ東部のサマータイムを取得するためのアジャスター。
 */
public class EastAmericaSummerTimeAdjuster implements TemporalAdjuster {
    
    private static final int NORMAL_OFFSET = -5;
    private static final int SUMMER_TIME_OFFSET = -4;
    
    @Override
    public Temporal adjustInto(Temporal temporal) {
        if (!(temporal instanceof OffsetDateTime)) {
            throw new IllegalArgumentException("temporal is not OffsetDateTime.");
        }
        
        OffsetDateTime offset = (OffsetDateTime)temporal;
        
        if (this.isInSummerTime(offset)) {
            return offset.withOffsetSameInstant(ZoneOffset.ofHours(SUMMER_TIME_OFFSET));
        } else {
            return offset.withOffsetSameInstant(ZoneOffset.ofHours(NORMAL_OFFSET));
        }
    }
    
    /**
     * 夏時間の期間中であることをチェックする。
     */
    private boolean isInSummerTime(OffsetDateTime offset) {
        OffsetDateTime start = this.toStartDateTimeOfSummerTime(offset);
        OffsetDateTime end = this.toEndDateTimeOfSummerTime(offset);
        
        return (offset.isEqual(start) || offset.isAfter(start)) && offset.isBefore(end);
    }
    
    /**
     * 夏時間の開始日時を取得する。
     */
    private OffsetDateTime toStartDateTimeOfSummerTime(OffsetDateTime offset) {
        return offset.withOffsetSameInstant(ZoneOffset.ofHours(NORMAL_OFFSET))
                     .with(ChronoField.MONTH_OF_YEAR, 3) // 3月の
                     .with(TemporalAdjusters.firstInMonth(DayOfWeek.SUNDAY))
                     .with(TemporalAdjusters.next(DayOfWeek.SUNDAY)) // 第2日曜日の
                     .truncatedTo(ChronoUnit.DAYS)
                     .with(ChronoField.HOUR_OF_DAY, 2); // 午前2時
    }
    
    /**
     * 夏時間の終了日時を取得する。
     */
    private OffsetDateTime toEndDateTimeOfSummerTime(OffsetDateTime offset) {
        return offset.withOffsetSameInstant(ZoneOffset.ofHours(SUMMER_TIME_OFFSET))
                     .with(ChronoField.MONTH_OF_YEAR, 11) // 11月の
                     .with(TemporalAdjusters.firstInMonth(DayOfWeek.SUNDAY)) // 第1日曜日の
                     .truncatedTo(ChronoUnit.DAYS)
                     .with(ChronoField.HOUR_OF_DAY, 2); // 午前2時
    }
}
Main.java
package sample.dta;

import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAdjuster;

public class Main {
    
    public static void main(String[] args) {
        TemporalAdjuster adjuster = new EastAmericaSummerTimeAdjuster();
        
        OffsetDateTime a = OffsetDateTime.parse("2015-07-17T01:00:00+09:00");
        OffsetDateTime b = OffsetDateTime.parse("2015-11-30T05:00:00+09:00");
        
        System.out.println(a + " -> " + a.with(adjuster));
        System.out.println(b + " -> " + b.with(adjuster));
        
        
        ZonedDateTime A = a.toZonedDateTime();
        ZonedDateTime B = b.toZonedDateTime();
        
        System.out.println(A + " -> " + A.withZoneSameInstant(ZoneId.of("America/New_York")));
        System.out.println(B + " -> " + B.withZoneSameInstant(ZoneId.of("America/New_York")));
    }
}
実行結果
2015-07-17T01:00+09:00 -> 2015-07-16T12:00-04:00
2015-11-30T05:00+09:00 -> 2015-11-29T15:00-05:00
2015-07-17T01:00+09:00 -> 2015-07-16T12:00-04:00[America/New_York]
2015-11-30T05:00+09:00 -> 2015-11-29T15:00-05:00[America/New_York]
  • OffsetDateTime に対して自作した EastAmericaSummerTimeAdjuster を適用すると、夏時間を考慮したアメリカ東部での時刻が取得できる。
  • アメリカの夏時間は、「3月の第2日曜日の午前2時」から「11月の第1日曜日の午前2時」までが対象となる。
    • 厳密には、 2007 年からがこの期間で、それより前の年は夏時間の期間が異なる。
    • 今回は、とりあえず 2015 年現在の夏時間に対応させている。
  • 夏時間中は、オフセットが -04:00 となり、通常時は -05:00 になる。
  • EastAmericaSummerTimeAdjuster は、適用された OffsetDateTime の日付が夏時間の期間内かどうかをチェックし、結果に合わせてオフセットを調整した OffsetDateTime を返すようにしている。
  • 今回は、アジャスターの実装がどんな感じになるのかを試すのが目的なので、夏時間の開始・終了日時はハードコーディングしている。
  • しかし実際は、設定ファイルなどに期間の定義を保存しておき、動的に開始日時と終了日時を生成できるようにしておくことになると思う。

Clock で時間を固定する

Main.java
package sample.dta;

import java.time.Clock;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

public class Main {
    
    public static void main(String[] args) {
        test("systemDefaultZone()", Clock.systemDefaultZone());
        test("fixed()", Clock.fixed(OffsetDateTime.now().toInstant(), ZoneOffset.ofHours(9)));
    }
    
    private static void test(String tag, Clock clock) {
        System.out.println(tag + " : clock.class = " + clock.getClass().getSimpleName());
        System.out.println(LocalDateTime.now(clock));
        sleep(1000);
        System.out.println(LocalDateTime.now(clock));
        sleep(1000);
        System.out.println(LocalDateTime.now(clock));
    }
    
    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {}
    }
}
実行結果
systemDefaultZone() : clock.class = SystemClock
2015-07-19T08:54:14.292
2015-07-19T08:54:15.293
2015-07-19T08:54:16.294

fixed() : clock.class = FixedClock
2015-07-19T08:54:16.294
2015-07-19T08:54:16.294
2015-07-19T08:54:16.294
  • now()Clock のインスタンスを渡すと、 Clock から現在時刻が取得される。
  • Clock#systemDefaultZone() を使うと、システム時間を取得できる Clock インスタンスが返される。
  • Clock#fixed() を使うと、常に指定した時刻を返す Clock インスタンスを取得できる。
  • プロダクション環境では systemDefaultZone() を使うようにして、テストのときは fixed() を使うように切り替えることで、テストがしやすくなる。
  • 切り替えは、例えば Java EE を使っているなら CDI の Provider を使えば簡単に実現できる。

参考

所感等

  • ISO 8601 と tz database の説明が先にあったため、 Local*Offset*ZonedDateTime の違い・使い分けがすんなりと理解できました。
  • 自分も人に説明するときは、 ISO 8601 と tz database の話から説明するようにしたいと思います。
  • アジャスターを実際に触ってみた感じ、かなり柔軟に日付の操作ができるようになっているのだなぁと感じた。
  • Date and Time API いいね!
43
45
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
43
45