2018年4月25日水曜日

[JavaSE8 Goldへの道] その13 Date/Time APIその1 基本クラス群

JavaSE8 Goldへの道(Upgrade to Java SE 8 Programmer 1Z0-810 試験対策)12回目です。

一連の記事は「JavaSE8Gold」ラベルを付けていきます。

Java8ではjava.util.Datejava.util.Calendarなどに代わる新しい「Date/Time API」が整備されました。
これを3回に分けて紹介していきます。



特徴

従来のクラス群は不変でない(ミュータブル)、スレッドセーフでないという問題がありました。
Date/TimeAPIの全てのクラスは不変(イミュータブル)でスレッドセーフに設計されています。

基本となるInstantクラス

時系列のある一時点を表します。
標準Javaエポック(1970-01-01T00:00:00Z)からの経過秒とナノ秒を保持しています。
注意として、タイムゾーンがUTC固定です。(ISO 8601形式で末尾"Z"はUTCを表す)

生成方法の例(いつも参考にしているこちらのサイトのサンプルコードより。以降同様。)
// 現在時刻(UTC)
Instant now = Instant.now();
System.out.println(now);

// Unixタイムスタンプから(2015-07-13T20:45:44.404Z)
Instant fromUnixTimestamp = Instant.ofEpochSecond(1436820344);

// 同じ時刻をミリ秒で指定
Instant fromEpochMilli = Instant.ofEpochMilli(1436820344404L);

// ISO 8601形式の文字列からパース
Instant fromIso8601 = Instant.parse("2015-07-10T12:00:00Z");
System.out.println(fromIso8601);

// toString()はISO 8601形式で返す
String toIso8601 = now.toString();
System.out.println(toIso8601);

// Javaエポック1970-01-01T00:00:00Zからの秒数
long toUnixTimestamp = now.getEpochSecond();
System.out.println(toUnixTimestamp);

// エポック1970-01-01T00:00:00Zからのミリ秒数
long toEpochMillis = now.toEpochMilli();
System.out.println(toEpochMillis);
実行結果
2018-04-23T08:15:12.371Z
2015-07-10T12:00:00Z
2018-04-23T06:35:26.015Z
1524465326
1524465326015

タイムゾーンが固定なので、そのまま使うと日本時間とずれます。

他にも加算・減算を行うplus,minusメソッド、比較を行うisAfter,isBeforeメソッド、ある属性を特定の値を指定(月の最初の日など)するwithメソッドなどがあります。
これらのメソッドは以降で紹介するクラスにも実装されています。
イミュータブルなので結果は全て新しいインスタンスを返します。

Instantを基本として、3つの日時を表すクラスが存在します。

(ただしInstantがスーパークラスというわけではありません)


3つの日時クラス

LocalDateTime

タイムゾーン情報を持たない日時クラスです。
内部にLocalDateLocalTimeクラスのインスタンスを持ちます。これらは独立してそれぞれ日付のみ/時刻のみを表すクラスとして存在します。

生成方法の例
// Date with time
LocalDateTime currentDateTime = LocalDateTime.now();
System.out.println("Current date and time : " + currentDateTime);

// 2015-09-15 10:15
LocalDateTime sept15th = LocalDateTime.of(2015, 9, 15, 10, 15);
System.out.println("September 15th : " + sept15th);

// 2015-12-25 12:00
LocalDateTime christmas2015 = LocalDateTime.of(2015, Month.DECEMBER, 25, 12, 0);
System.out.println("Christmas 2015 : " + christmas2015);

// Instantから生成
Instant n = Instant.now();
System.out.println(n);
System.out.println(LocalDateTime.ofInstant(n, ZoneId.systemDefault()));
実行結果
Current date and time : 2018-04-23T17:15:12.370
September 15th : 2015-09-15T10:15
Christmas 2015 : 2015-12-25T12:00
2018-04-23T08:15:12.371Z
2018-04-23T17:15:12.371

ofメソッドには他にもたくさんの引数パターンがあります。月にはMonth列挙型を渡すものとintで指定するものがあり、intの方は従来と違い1~12で指定します。


「タイムゾーン情報を持たない」と言いながらInstantから生成する際にZoneIdを引数に取るのはなんか変な感じがしますが、LocalDateTimeが従来のDateのようにシステムデフォルトのタイムゾーンで生成されるものと考えれば良いでしょう。
LocalDateTime#nowメソッドはデフォルトタイムゾーンで現在日時を生成します。



ZonedDateTime

タイムゾーン情報を持つ日時クラスです。タイムゾーンはZoneIdというクラスで保持しています。
ややこしいですが、ZonedDateやZonedTimeというクラスはありません。ZonedDateTimeのみです。

生成にはnowメソッドでフォルトのタイムゾーンかZoneIdを指定して生成するか、ofメソッドにLocalDateTimeZoneIdも追加で引数に渡します。

生成方法の例
ZoneId minsk = ZoneId.ofOffset("GMT", ZoneOffset.ofHours(+3));
ZoneId berlin = ZoneId.of("Europe/Berlin");

// Current date and time
LocalDateTime dateTime = LocalDateTime.now();
System.out.println("Here : " + dateTime);

ZonedDateTime minskDateTime = ZonedDateTime.of(dateTime, minsk);
System.out.println("Minsk : " + minskDateTime);

// Current date and time in Berlin, Germany
ZonedDateTime berlinDateTime = minskDateTime.withZoneSameInstant(berlin);
System.out.println("Berlin : " + berlinDateTime);
実行結果
Here : 2018-04-23T17:44:12.897
Minsk : 2018-04-23T17:44:12.897+03:00[GMT+03:00]
Berlin : 2018-04-23T16:44:12.897+02:00[Europe/Berlin]

withZoneSameInstantメソッドはインスタントを保持したまま別のタイムゾーンを使ってコピーを生成します。
結果示す日時は変更されます。

OffsetDateTime

試験範囲としては載っていないのですが、一応これも説明しておきます。
ゾーン情報は持たず、オフセットのみを保持します。これはZoneOffsetとして表されています。
生成時もofメソッドにはZoneOffsetを引数に取ります。

とてもややこしいことに、OffsetDateはないのにOffsetTimeだけ存在します。



時間量を表すクラス

Period

日付ベースの時間量を表すクラスです。

生成にはof~メソッドを使います。引数には年、月、日、もしくは週数を指定できます。nowメソッドはありません。

「量」なので、マイナスの値も取り得ます。

参考サイトのサンプルコード
LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.of(1974, Month.SEPTEMBER, 15);

Period p = Period.between(birthday, today);
long p2 = ChronoUnit.DAYS.between(birthday, today);

System.out.printf("You are %d years, %d months, and %d days old (%d days total)", p.getYears(), p.getMonths(), p.getDays(), p2);
実行結果
You are 43 years, 7 months, and 8 days old (15926 days total)

betweenメソッドは2つのDateTimeの差を求めます。

逆に、各日時クラスのplus(TemporalAmount),minus(TemporalAmount)の引数に渡して計算ができます。
(Periodと次に説明するDurationクラスはTemporalAmountインタフェースを実装しています)
タイムゾーンを考慮する場合、ZonedDateTime#plusなどを使う必要があります。

Period自身にもplus~,minus~,with~メソッドがあり、新たな時間量を計算できます。


Duration

時刻ベースの時間量を表すクラスです。秒とナノ秒で保持します。

Durationも生成にはof~メソッドを使います。nowメソッドはありません。
これもマイナスの値を取り得ます。

参考サイトのサンプルコード
// Current time
Instant now1 = Instant.now();

// Wait __appropximately__ 1 second
Thread.sleep(1000);

// Current time
Instant now2 = Instant.now();

// Calculate real duration
long ns = Duration.between(now1, now2).toNanos();
System.out.println("Duration (ns) : " + ns);
実行結果
Duration (ns) : 1001000000
「約」と強調している通り、自分のPCがでは1秒ぴったりではありませんでした。

前述の通り、各日時クラスのplus(TemporalAmount),minus(TemporalAmount)の引数に渡して計算ができます。
Duration自身の各計算メソッドがあるのもPeriodと同じです。


java.util.Dateとの相互変換

java.util.Dateとの相互変換ですが、Date/TimeAPIの方にはありません。
Dateクラスの方にfrom,toInstantメソッドが追加されています。
public static Date from(Instant instant)
public Instant toInstant()
また、CalendarクラスにtoInstantメソッドだけが追加されています。Instantにはタイムゾーン情報がないのでCalendarは作れないということでしょう。

と、い、う、わ、け、で

Date/Time APIその1は基本のクラス群の解説でした。
基本となるInstantと3つの日時クラスLocalDateTime,ZonedDateTime,OffsetDateTimeの関係、時間量を表す2つのクラスPeriod,Durationをしっかり押さえておきましょう。

今回も以下のサイトとGoldの通常試験の参考書を参考にしています。
一連の記事は「JavaSE8Gold」ラベルを付けていきます。

それではみなさまよきガジェットライフを(´∀`)ノ


▼こちらの記事もどうぞ

▼ブログを気に入っていただけたらRSS登録をお願いします!
▼ブログランキング参加中!応援よろしくお願いします。

スポンサーリンク