記事では、スタッフのドキュメンテーションの平凡な翻訳に手を出さないようにしますが、特に重要だと思われることにもっと集中したいと思います。
物語
時間の処理に関しては、長い間、標準Javaライブラリに関する苦情がありました。 批判されたバージョンのAPIは、かなり前に開発されたもので、その設計に重大な誤りがありました。 別の方法として、多くはサードパーティのJoda-timeライブラリを使用しました。 私自身は、いくつかの理由により、Joda-timeの大ファンではありません。
- それでもやはり、標準ライブラリのクラスは避けられません。99%のケースでは、その機能がタスクに対応しており、必要以上にエンティティを増やしたくありません。
- joda-timeライブラリはJVMのタイムゾーンの標準データベースを使用しないため、次回立法者が操作するときには、tzdataをJDKだけでなくjoda-timeライブラリでも更新する必要があることを覚えておく必要があります 。
比較
おそらく、古いAPIの多くに合わなかったものから始める価値があります。 そして、時間を無駄にしないために、新しいAPIが改善されたことをすぐに示します。
クラスをパッケージに分類:
- 古いAPIでは、時間の経過とともに動作するクラスはjava.utilおよびjava.sqlパッケージにあります-他の多くのクラスの中でも。 さらに、子孫を持つクラスjava.util.concurrent.TimeUnitおよびjava.text.DateFormatもあります。
- 時間を操作するための新しいAPIでは、別個のjava.timeパッケージが割り当てられます
クラス名:
- 古いAPIのクラス名は、起こっていることの本質を反映していません。 古いAPIには、時間軸上のポイントを示すことができる2つのクラス、java.util.Dateとjava.util.Calendarがあります。 java.util.Dateクラスは、日付ではなくミリ秒単位の時間をUnix時間で示します(/ bin / dateユーティリティがコマンドラインに時間を出力するのと同じ理由で名前が付けられます)。 java.util.Calendarクラスもカレンダーではありません。タイムゾーン、カレンダー、および時間フィールドの形式の状態があります。
- 新しいAPIでは、クラス名はより意味のあるものになりました。 すでに言及したクラスに類似したクラスがあります:java.time.Instantおよびjava.time.ZonedDateTime。 また、より専門的な用途向けのその他のクラスも多数あります。
不変性とスレッドの安全性:
- java.util.Dateクラスは不変ではなく、多数の不要なメソッドによって負担されます。これらのメソッドは、すでに廃止されているとマークされていますが、近い将来削除される可能性は低いです。 java.util.Dateの可変性により、一部のユーザーはjava.util.Dateのインスタンスのクローンを作成します。これにより、敵は通過できません。
public class UserBean { private final Date created; public UserBean(Date created) { this.created = (Date) created.clone(); } }
java.util.Calendarクラスも変更可能です。 これは特定の問題を引き起こすものではありませんが、ほとんどの人は内部状態が変化していることを理解しており、引数で渡すことはどういうわけかあまり受け入れられません。
古いAPIのクラスは可変であるため、マルチスレッド環境では注意して使用してください。 特に、廃止されたメソッドを呼び出さない場合、java.util.Dateは「効率的に」スレッドセーフと見なすことができます。
- 新しいAPIのすべてのクラスは不変であり、その結果、スレッドセーフです
精度:
- 時間の表現の精度は1ミリ秒です。 ほとんどの実用的なタスクではこれで十分ですが、場合によってはより高い精度が必要になります。
- 新しいAPIでは、時間表現は1ナノ秒であり、100万倍の精度です。
時刻と日付のスタンプの保存:
- 時刻および日付スタンプ(java.sql.Dateおよびjava.sql.Time)のクラスは時刻および日付スタンプの純粋な表現ではありません。これらはjava.util.Dateから継承され、何らかの形でこの部分を無視して完全なUnix-time値を格納するためです。値。
- 新しいAPIでは、対応するクラスjava.time.LocalDateおよびjava.time.LocalTimeはそれぞれ純粋なタプル(yyyy、MM、dd)および(HH、mm、dd)を格納し、これらのクラスには不要な情報やロジックはありません。 また、両方のタプルを格納するjava.time.LocalDateTimeクラスも導入されています。
タイムゾーン表示:
- 古いAPIでは、タイムゾーンが必要な多くのアクションを、指定せずに実行できます。 この場合、デフォルトのタイムゾーンが使用され、プログラマーは何かを見逃したことに気付かないかもしれません。
- 新しいAPIでは、タイムゾーンが必要なすべてのアクションで明示的にそれを必要とします。メソッドの引数として、またはタイムゾーンがメソッド名に直接表示されます。 つまり、デフォルトのタイムゾーンはデフォルトではどこでも使用されていません。
テスト:
- 古いAPIは、長期にわたってロジックの動作をテストする必要があるテストで使用することは非常に困難です(これについては、前の記事で詳しく説明しています)。
- 新しいAPIは、特別な抽象クラスjava.time.Clockを導入しました。このクラスの単一のインスタンスは、コンテキストに挿入するか、単にロジックに渡すことができます。 テスト用にこのクラスをオーバーライドすることにより、実行中のコードの時間の流れを制御できます。
月の番号付け:
- 古いAPIでは、月の数字は0から始まりますが、これは非常に直感的ではありません。
- 新しいAPIでは、月番号は1から始まります。新しいjava.time.Month列挙が登場しました 。
タグ付け:
- java.util.Calendarで、年-月-日-時-分-秒を設定しますが、ミリ秒をリセットするには、別の呼び出しを行う必要がありました。
- java.time.ZonedDateTimeでは、ナノ秒を含むすべてのフィールドが一度に設定されます。
期間指定:
- 古いAPIには、期間と期間を決定するためのクラスはありません。 通常、単純なlongが使用され、ミリ秒の形式の期間ストレージが使用されます。
- 新しいAPIは、期間と期間の特別なクラスを定義します。
恐怖
また、「劣化」とは絶対に呼べないものについて話す価値があるかもしれませんが、慎重に「恐怖」と呼んでいます。
- 以前に2つのアクティブに使用されるクラスjava.util.Dateとjava.util.Calendarがあった場合、さらに多くのクラスがあり、さらにインターフェースと抽象クラスの階層がそれらに追加されています。
- 新しいクラスの数が多かったため、数時間の研究でなんとか見つけた作業のニュアンスがありましたが、これについては後で説明します。
- 新しいAPIは、コンパイル時の正しい操作を制御しません。 タイムゾーンがないことに関する多くの問題は、ランタイムでのみ発生します-例で後述します。 柔軟性は低いかもしれませんが、より厳密な契約を希望します。
- 作業の過程で、より多くのオブジェクトが生成されます。 たとえば、java.time.Instanceインスタンス自体に加えて、Instance.now()を介して現在の時刻を取得すると、リクエストごとにjava.time.Clockも作成されますが、これはまったく必要ありません-現在の実装では、System.currentTimeMillis( ) また、中間オブジェクトは他の多くのアクション中に作成されます。 しかし、典型的なバックエンドでは、これがどんな種類の問題をも引き起こすとは思いません-メモリ消費量でも実行時間でもありません。 筋金入りの労働者は、まだ長い時間を保存している、またはintを詰め込んでいます。
- 問題(これは問題ですか?)、うるう秒を考慮して、彼らはそれをまったく解決しませんでした。 実際、新しいライブラリはまだUnixタイムから一歩も離れておらず、秒針の外部翻訳に依存しています。 同時に、過去も毎秒を失い、前進します。 正確な科学計算のためのライブラリはありませんでした。
これらの事例や、他の事例で私に警告を発した事例について詳しく説明します。
タイムゾーン
通常どおりタイムゾーンから始めましょう。 新しいjava.time.ZoneIdクラスはタイムゾーンを指定します。 2つのサブクラスjava.time.ZoneRegionおよびjava.time.ZoneOffsetは、2つのタイプのタイムゾーンを実装します。地理的タイムゾーンと、UTC、UTまたはGMTを基準とした単純なオフセットを持つタイムゾーンです。 矢印を変換するためのルールは、別個のクラスjava.time.zone.ZoneRulesで作成され、そのインスタンスはjava.time.ZoneId#getRulesメソッドを介して利用できます。
一般に、示されたリファクタリングを除いて、ここで重要な基本的な変更は見つかりませんでした。ただし、矢印を変換するためのルールが情報を要求するためのより多くのメソッドを提供することを除きます。 したがって、 古い記事で書かれたものはすべて、新しいクラスのタイムゾーンにも当てはまりますが、メソッドの名前が若干異なる点が異なります。
@Test public void testZoneId() throws Exception { // case-1 ZoneId zid1 = ZoneId.of("Europe/Moscow"); Assert.assertEquals("ZoneRegion", zid1.getClass().getSimpleName()); // case-2 ZoneId zid2 = ZoneId.of("UTC+4"); Assert.assertEquals("ZoneRegion", zid2.getClass().getSimpleName()); // case-3 ZoneId zid3 = ZoneId.of("+03:00:00"); Assert.assertEquals("ZoneOffset", zid3.getClass().getSimpleName()); // case-4 ZoneId zid4 = ZoneId.ofOffset("UTC", ZoneOffset.of("+03:00:00")); Assert.assertEquals("ZoneRegion", zid4.getClass().getSimpleName()); }
実際にケース3と同じを要求するケース4が、結果としてjava.time.ZoneOffsetではなくjava.time.ZoneRegionを作成する理由はあまり明確ではありません。
@Test public void testZoneUTC() throws Exception { ZoneId zid1 = ZoneOffset.UTC; Assert.assertEquals("ZoneOffset", zid1.getClass().getSimpleName()); ZoneId zid2 = ZoneId.of("Z"); Assert.assertEquals("ZoneOffset", zid2.getClass().getSimpleName()); Assert.assertSame(ZoneOffset.UTC, zid2); ZoneId zid3 = ZoneId.of("UTC"); Assert.assertEquals("ZoneRegion", zid3.getClass().getSimpleName()); }
UTCタイムゾーンに対して特別な定数java.time.ZoneOffset#UTCが設定されていますが、それにもかかわらず、新しいAPIのZoneId.of( "UTC")の要求は、この定数ではなくjava.util.ZoneRegionクラスのオブジェクトを既に発行しています。
時計
「 時間は時計です 」-一部の物理学者が主張するように。 そして、そのフレーズは、 java.time.Clockクラスが基盤となる新しいAPIの鍵となります。 時計の一部と同様に、時間は次のようになります:一定(非アクティブ)、遅い、さまざまな精度で実行、異なるタイムゾーンで矢印を動かす 一般に、新しいAPIでは、テストのテストを含め、ほぼすべての時間を使用(または自分で定義)できます。
標準のjava.time.Clockインスタンスは、ファクトリの静的メソッドによってのみ作成できます(クラス自体は抽象クラスです)。
java.time.Clockの標準インスタンスは、作成されたタイムゾーンを常に認識しています(これも不要です)。
ファクトリメソッドを見てみましょう。
- java.time.Clock#systemDefaultZone-このメソッドは、デフォルトでタイムゾーンにシステムクロックを作成します。
- java.time.Clock#systemUTC-メソッドは、UTCタイムゾーンでシステムクロックを作成します。
- java.time.Clock#system-メソッドは、指定されたタイムゾーンでシステムクロックを作成します。
- java.time.Clock#fixed-メソッドは一定時間のクロックを作成します。つまり、クロックは停止しますが、停止します。
- java.time.Clock#offset-メソッドは、指定された時間にわたってプロキシを作成し、指定された量だけ時間をシフトします。
- java.time.Clock#tickSeconds-このメソッドは、指定されたタイムゾーンでシステムクロックを作成し、その値は最も近い整数秒に丸められます。
- java.time.Clock#tickMinutes-このメソッドは、指定されたタイムゾーンでシステムクロックを作成し、その値は最も近い分に丸められます。
- java.time.Clock#tick-メソッドは指定された時間にわたってプロキシを作成し、指定された期間に時間値を丸めます。
- java.time.Clock#withZone-このメソッドは、異なるタイムゾーンで現在の時計のコピーを作成します。
java.time.Clockをオーバーライドして、時間を発行するための独自のロジックを作成できます。たとえば、リクエストごとにランダムな時間を提供するクロックなどです。
java.time.Clockオブジェクトには、次の3つの作業メソッドのみがあります。
- java.time.Clock#getZone-時計が機能するタイムゾーンを要求します。
- java.time.Clock#millis-Unix時間で現在の時間をミリ秒で要求する
- java.time.Clock#instant-最も一般的な意味で現在の時刻を要求します(実際には、Unix時間でナノ秒単位で)
今少し批判:
- きれいなjava.time.Clockインターフェースと別個のjava.time.Clocksファクトリーを取得しますが、私は主張しません。
- 何らかの理由で、時計には必ずタイムゾーンが課されます。 時計自体にはまったく必要ありません。java.time.Clock#millisもjava.time.Clock#instantもタイムゾーンを使用しません。 時計のタイムゾーンはファクトリメソッド{Zoned、Local、Offset} DateTimeで要求されますが、メソッドの個別のパラメーターとして渡され、java.time.Clockのバラストに保存されないことがありました。
- 残念ながら、テスト用の手動時間管理用のMockClockクラスはありません。自分で作成する必要があります-これは問題ではありませんが、すぐにできた方が良いでしょう。
- 時計には、タイムレスなナノ秒刻みを測定するためのjava.time.Clock#ticksメソッドがありません(java.lang.System#nanoTimeに類似)。 一方では、時間の計算には適用されないため、そのような方法がないことは理解できます。 しかし一方で、これは操作の継続時間の測定にも当てはまります。 したがって、テストでタイムレスティックを管理するには(それぞれ期間を測定します)、デフォルトでMockClockの時間を手動で進めると時間とティックの両方が進むため、ティックのメソッドもこのインターフェイスにあれば良いでしょう。
インスタント
java.time.Instantは新しいjava.util.Dateであり、ナノ秒の精度と正しい名前でのみ不変です。 内部では、 Unix時間を2つのフィールドの形式で保存します。長さは秒数で、整数は現在の秒内のナノ秒数です。
両方のフィールドの値は直接要求することができ、古いAPIのミリ秒のより馴染みのあるUnix時間表現を計算するように依頼することもできます。
@Test public void testInstantFields() throws Exception { Instant instant = Clock.systemDefaultZone().instant(); System.out.println(instant.getEpochSecond()); System.out.println(instant.getNano()); System.out.println(instant.toEpochMilli()); }
java.util.Dateと同様に(正しく使用した場合)、java.time.Instantクラスオブジェクトはタイムゾーンについて何も知りません。
それとは別に、java.time.Instant.toString()メソッドに言及する価値があります。 以前のjava.util.Date.toString()が現在のロケールとデフォルトのタイムゾーンを考慮して機能する場合、新しいjava.time.Instant.toString()は常にUTCタイムゾーンと同じISO-8601形式でテキスト表現を生成します-これが適用されますデバッグ時に変数をIDEに出力する:
@Test public void testInstantString() throws Exception { Instant instant1 = Clock.system(ZoneId.of("Europe/Paris")).instant(); System.out.println(instant1.toString()); Instant instant2 = Clock.systemUTC().instant(); System.out.println(instant2.toString()); Instant instant3 = Clock.systemDefaultZone().instant(); System.out.println(instant3.toString()); }
2016-01-06T15:22:53.403Z 2016-01-06T15:22:53.417Z 2016-01-06T15:22:53.423Z
基本的なインターフェース
基本インターフェースjava.time.temporal.TemporalAccessorを見てみましょう。 TemporalAccessorインターフェースは、現在のポイントまたはラベルに関する個別の部分情報を要求するための参照であり、新しいAPIのすべての一時クラスによって実装されます。
java.time.InstantからUnix-time値を聞いてみましょう。
@Test(expected = DateTimeException.class) public void testTemporalAccessor2() throws Exception { TemporalAccessor ta = Clock.systemUTC().instant(); // java.time.DateTimeException: Invalid value for InstantSeconds \ // (valid values -9223372036854775808 - 9223372036854775807): 1451983908 System.out.println(ta.get(ChronoField.INSTANT_SECONDS)); }
完全に説明できないメッセージで例外が発生します。
java.time.DateTimeException:InstantSecondsの無効な値\ (有効な値-9223372036854775808-9223372036854775807):1451983908
少しのお金を稼いだことで、例外の理由が明らかになりました:結果は理論的にはintの範囲に収まらないかもしれません(現在のところ)。 INSTANT_SECONDSフィールドは、長い間リクエストする必要があります。 リクエストを修正し、途中で追加のメタ情報をリクエストします:
@Test public void testTemporalAccessor3() throws Exception { TemporalAccessor ta = Clock.systemUTC().instant(); System.out.println(ta.getLong(ChronoField.INSTANT_SECONDS)); ValueRange vr = ta.range(ChronoField.INSTANT_SECONDS); System.out.println(vr.getMinimum()); System.out.println(vr.getMaximum()); System.out.println(ta.isSupported(ChronoField.INSTANT_SECONDS)); System.out.println(ta.isSupported(ChronoField.CLOCK_HOUR_OF_DAY)); }
1452094053 -9223372036854775808 9223372036854775807 本当 偽
CLOCK_HOUR_OF_DAYフィールドは、インスタントタイプではサポートされていません。 これは完全に予想されることです。ある時点で時間を見つけるために、java.time.Instantにないタイムゾーンを指定する必要があるからです。 この値をすべて同じように要求してみましょう。
@Test(expected = UnsupportedTemporalTypeException.class) public void testTemporalAccessor1() throws Exception { TemporalAccessor ta = Clock.systemUTC().instant(); // java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: ClockHourOfDay System.out.println(ta.getLong(ChronoField.CLOCK_HOUR_OF_DAY)); }
すべてが正しい-1日の1時間をリクエストすると、例外が発生します。 リクエストメソッドがデフォルトのタイムゾーンを使用しなかったのは素晴らしいことです(新しいAPIにはありません)。
個々のフィールドのクエリに加えて、 java.time.TemporalQueryインターフェースを継承するより複雑なアルゴリズム戦略を使用して値をクエリできます。
@Test public void testTemporalAccessor4() throws Exception { TemporalAccessor ta = Clock.systemUTC().instant(); ZoneId zoneId1 = ta.query(TemporalQueries.zone()); ZoneId zoneId2 = TemporalQueries.zone().queryFrom(ta); Assert.assertEquals(zoneId1, zoneId2); TemporalUnit unit1 = ta.query(TemporalQueries.precision()); TemporalUnit unit2 = TemporalQueries.precision().queryFrom(ta); Assert.assertEquals(unit1, unit2); }
java.time.temporal.Temporal-インターフェースはTemporalAccessorインターフェースの子孫です。 時刻/マークを前後に移動する操作、時刻情報の一部を置き換える操作、および別の時刻/マークまでの距離を計算する操作を紹介します。 これは、新しいAPIのほぼすべての「フルタイム」クラスによって実装されます。
ラベルを1日前にシフトして、差を計算しようとします。
@Test public void testTemporal1() throws Exception { Temporal t1 = Clock.systemUTC().instant(); Temporal t2 = t1.plus(1, ChronoUnit.DAYS); Assert.assertEquals(Duration.ofDays(1).getSeconds(), t2.getLong(ChronoField.INSTANT_SECONDS) - t1.getLong(ChronoField.INSTANT_SECONDS)); Assert.assertEquals(24, t1.until(t2, ChronoUnit.HOURS)); Assert.assertEquals(24, Duration.between(t1, t2).get(ChronoUnit.HOURS)); }
すべてのクラスが最終的に不変になったため、操作中に元の変数が変更されないため、操作の結果を別の変数に割り当てることを忘れないでください。すべてがjava.lang.Stringまたはjava.math.BigDecimalと同じです。
java.time.Instantで時刻を変更してみましょう。
@Test(expected = UnsupportedTemporalTypeException.class) public void testTemporal2() throws Exception { Temporal t = Clock.systemUTC().instant(); // java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: HourOfDay t.with(ChronoField.HOUR_OF_DAY, 2); }
この操作にはタイムゾーンが必要なので、私たちはそれを手に入れることが期待されています。
java.time.temporal.TemporalAdjuster-時点/ラベル修正戦略のインターフェース。たとえば、現在のコードの初日に移動します。 以前は、java.util.Calendarフィールドを操作するための独自のヘルパークラスを作成する必要がありました。必要なコードがまだ標準配信にない場合、すべてのコードを戦略の形式で作成できるようになりました。
@Test public void testTemporalAdjuster() throws Exception { ZonedDateTime zdt = ZonedDateTime.of(2005, 10, 30, 0, 0, 0, 0, ZoneId.of("Europe/Moscow")); ZonedDateTime zdt1 = zdt.with(TemporalAdjusters.firstDayOfYear()); ZonedDateTime zdt2 = (ZonedDateTime) TemporalAdjusters.firstDayOfYear().adjustInto(zdt); Assert.assertEquals(zdt1, zdt2); Assert.assertEquals(2005, zdt1.get(ChronoField.YEAR)); Assert.assertEquals(1, zdt1.get(ChronoField.MONTH_OF_YEAR)); Assert.assertEquals(1, zdt1.get(ChronoField.DAY_OF_MONTH)); }
これで一時クラスに行くことができます。
LocalTime、LocalDate、LocalDateTime
java.time.LocalTimeはタプル(時間、分、秒、ナノ秒)です
java.time.LocalDateはタプル(年、月、日)です
java.time.LocalDateTime-両方のタプルを一緒に
これらのクラスには、情報の一部を格納するための特定のクラスjava.time.MonthDay 、 java.time.Year 、 java.time.YearMonthも参照し ます
これらのクラスはすべて、タイムスタンプまたはその部分が含まれているという事実によって結合されていますが、タイムゾーンまたはオフセットさえ持っていないため、それらだけでは(LocalDateTimeでさえ)時間軸上のタイムポイントを決定できません。
これらのクラスは、他のすべてのクラスと同様にjava.lang.Comparableインターフェースをサポートしますが、これはタイムポイントではなくタイムスタンプの比較であることを理解する必要があります。
@Test public void testLocalDateTime() throws Exception { ZonedDateTime zdt1 = ZonedDateTime.of(2015, 1, 10, 15, 0, 0, 0, ZoneId.of("Europe/Moscow")); ZonedDateTime zdt2 = ZonedDateTime.of(2015, 1, 10, 14, 0, 0, 0, ZoneId.of("Europe/London")); Assert.assertEquals(-1, zdt1.compareTo(zdt2)); LocalDateTime ldt1 = zdt1.toLocalDateTime(); LocalDateTime ldt2 = zdt2.toLocalDateTime(); Assert.assertEquals(+1, ldt1.compareTo(ldt2)); }
java.time.LocalTimeとjava.sql.Timeの間、およびjava.time.LocalDateとjava.sql.Dateの間では避けられない類似性があるにもかかわらず、これらは完全に異なるクラスであると言わなければなりません。 古いAPIでは、クラスjava.sql.Timeとjava.sql.Dateはjava.util.Dateの継承者です。つまり、それらの解釈(たとえば、時間の値を取得する)は、このクラスのオブジェクトが作成されたタイムゾーンと時間に依存します。このオブジェクトが読み取られるゾーン。 新しいAPIでは、クラスjava.time.LocalTimeおよびjava.time.LocalDateは値の正直なタプルであり、時間帯は時間の値の書き込みおよび読み取りに関与しません。
ただし、時間帯の解釈は時間に依存するため、ある時点から作成する場合はタイムゾーンが必要です。
@Test(expected = DateTimeException.class) public void testLocalDateTimeCreate1() throws Exception { Clock clock = Clock.system(ZoneId.of("Europe/Moscow")); // java.time.DateTimeException: Unable to obtain LocalDateTime \ // from TemporalAccessor: 2016-01-11T15:15:03.180Z of type java.time.Instant LocalDateTime ldt = LocalDateTime.from(clock.instant()); }
タイムゾーンを取得する場所がないという事実のために例外がスローされます(インスタントではそうではありませんが、デフォルトではゾーンを取得しません)。 ただし、java.time.Clockクロックから取得するか、さらに転送することができます。
@Test public void testLocalDateTimeCreate2() throws Exception { Clock clock = Clock.system(ZoneId.of("Europe/Moscow")); LocalDateTime ldt1 = LocalDateTime.ofInstant(clock.instant(), ZoneId.of("UTC")); System.out.println(ldt1); LocalDateTime ldt2 = LocalDateTime.now(clock); System.out.println(ldt2); }
これですべてが機能するようになりましたが、間違いを犯しやすいのは少し不安です。
前の記事へのコメントで、彼らは本当の偏執病はカレンダー値での操作の際にカレンダーをも示すべきであると述べました(インスタントを除くすべての時間クラスのオブジェクトの作成を含む)。 新しいAPIには、年表と呼ばれるいくつかのカレンダーがあります。
@Test public void testChronology() throws Exception { Clock clock = Clock.system(ZoneId.of("Europe/Moscow")); ZonedDateTime zdt = ZonedDateTime.now(clock); ChronoLocalDateTime dt1 = IsoChronology.INSTANCE.localDateTime(zdt); System.out.println(dt1); // 2016-01-11T18:48:15.145 ChronoLocalDateTime dt2 = JapaneseChronology.INSTANCE.localDateTime(zdt); System.out.println(dt2); // Japanese Heisei 28-01-11T18:48:15.145 ChronoLocalDateTime dt3 = ThaiBuddhistChronology.INSTANCE.localDateTime(zdt); System.out.println(dt3); // ThaiBuddhist BE 2559-01-11T18:48:15.145 }
一般に、ISO-8601(グレゴリオ暦とほぼ同等)以外のIsoChronology年表が必要になる場合を想像するのは困難ですが、新しいAPIはこれをサポートします。
ZonedDateTime
java.time.ZonedDateTime -java.util.Calendarの類似物。これは、タイムゾーンを含む時間コンテキストに関する完全な情報を持つ最も強力なクラスです。したがって、このクラスは、シフトを含むすべての操作を正しく実行します。
LocalDateTimeからZonedDateTimeを作成してみましょう。
@Test(expected = DateTimeException.class) public void testZoned1() throws Exception { LocalDateTime ldt = LocalDateTime.of(2015, 1, 10, 0, 0, 0, 0); // java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: 2015-01-10T00:00 of type java.time.LocalDateTime ZonedDateTime zdt = ZonedDateTime.from(ldt); }
操作(LocalDateTime内)にタイムゾーンがなく、新しいAPIが再びデフォルトのタイムゾーンの使用を拒否するという事実をすぐに理解できます(これは非常に良いことです)。
正しいオプション:
@Test public void testZoned2() throws Exception { LocalDateTime ldt = LocalDateTime.of(2015, 1, 10, 0, 0, 0, 0); ZonedDateTime zdt = ZonedDateTime.of(ldt, ZoneId.of("Europe/Moscow")); }
誤って指定された日付に関して、ZonedDateTimeがどれほど厳密かを見てみましょう。java.util.Calendarには寛容なスイッチがあり、「厳格」モードと「ソフト」モードの両方に設定できます。新しいAPIにはそのような切り替えはありません。
うるう年ではない2月29日は過ぎません。
@Test(expected = DateTimeException.class) public void testLenient2() throws Exception { // java.time.DateTimeException: Invalid date 'February 29' as '2005' is not a leap year ZonedDateTime.of(2005, 2, 29, 2, 30, 0, 0, ZoneId.of("Europe/Moscow")); }
60秒は指定できません。
@Test(expected = DateTimeException.class) public void testLenient3() throws Exception { // java.time.DateTimeException: Invalid value for SecondOfMinute (valid values 0 - 59): 60 ZonedDateTime.of(2005, 2, 20, 2, 30, 60, 0, ZoneId.of("Europe/Moscow")); }
しかし、夏時間に針が切り替わった時点でのラベルの表示は正常に経過し、結果は予想とは異なります。strictモードでは、java.util.Calendarはこれを見逃していません(前の記事を参照)。
@Test public void testLenient1() throws Exception { ZonedDateTime zdt = ZonedDateTime.of(2005, 3, 27, 2, 30, 0, 0, ZoneId.of("Europe/Moscow")); Assert.assertEquals(3, zdt.getLong(ChronoField.HOUR_OF_DAY)); Assert.assertEquals(30, zdt.getLong(ChronoField.MINUTE_OF_HOUR)); }
ZonedDateTimeの操作については何も書きません。ドキュメントをご覧ください。
OffsetTime、OffsetDateTime
java.time.OffsetTimeはLocalTime + ZoneOffsetです
java.time.OffsetDateTimeはLocalDateTime + ZoneOffset
オフセットがタイムゾーンではない(タイムゾーンはオフセットの履歴であり、さらに多くの情報がある)ので、OffsetDateTimeも少ないZonedDateTimeよりも情報。 OffsetDateTimeは時間軸上の時点を完全に示すことができますが、このクラスは矢印の将来および過去の転送について何も知らないため、完全に正しいシフトを生成することはできません。
現在のユーザーオフセットのみが状況によって認識されている場合(たとえば、JavaScriptを介して、これらのクラスを使用できます。)完全に正しいシフト操作は許可されていないため、ユーザーのフルタイムゾーンを確認する方法がある場合は、ZonedDateTimeを使用することをお勧めします。一方、OffsetDateTimeの2つのインスタンス間では、常に秒単位の差を正常かつ正確に計算できます。
時間変更
新しいAPIのすべてのクラスのうち、時間軸上の時点を一意に決定するのは、java.time.Instant、java.time.ZonedDateTime、およびjava.time.OffsetTimeの3つだけです。
タイムゾーンについて知っているのは彼だけであるため、シフト操作と時刻変更操作は通常java.time.ZonedDateTimeでのみ正しく実行されます。
手を冬時間に切り替えた日の経過時間を計算して例を実行してみましょう。
@Test public void testWinterDay() throws Exception { ZonedDateTime zdt1 = ZonedDateTime.of(2005, 10, 30, 0, 0, 0, 0, ZoneId.of("Europe/Moscow")); // case #1 - ok ZonedDateTime zdt2 = zdt1.plusDays(1); Assert.assertEquals(25, Duration.between(zdt1, zdt2).toHours()); // case #2 - ok ZonedDateTime zdt3 = zdt1.plus(1, ChronoUnit.DAYS); Assert.assertEquals(25, Duration.between(zdt1, zdt3).toHours()); // case #3 - ok OffsetDateTime odt1 = zdt1.toOffsetDateTime(); OffsetDateTime odt2 = zdt2.toOffsetDateTime(); Assert.assertEquals(25, Duration.between(odt1, odt2).toHours()); // case #4 - ??? OffsetDateTime odt3 = zdt1.toOffsetDateTime(); OffsetDateTime odt4 = odt3.plus(1, ChronoUnit.DAYS); Assert.assertEquals(24, Duration.between(odt3, odt4).toHours()); // case #5 - ok Instant instant1 = Instant.from(zdt1); Instant instant2 = Instant.from(zdt2); Assert.assertEquals(25, Duration.between(instant1, instant2).toHours()); // case #6 - ??? Instant instant3 = Instant.from(zdt1); Instant instant4 = instant3.plus(1, ChronoUnit.DAYS); Assert.assertEquals(24, Duration.between(instant3, instant4).toHours()); // case #7 - ??? LocalDateTime localDateTime1 = LocalDateTime.from(zdt1); LocalDateTime localDateTime2 = localDateTime1.plus(1, ChronoUnit.DAYS); Assert.assertEquals(24, Duration.between(localDateTime1, localDateTime2).toHours()); // case #8 - ??? LocalDateTime localDateTime3 = LocalDateTime.from(zdt1); LocalDateTime localDateTime4 = LocalDateTime.from(zdt2); Assert.assertEquals(24, Duration.between(localDateTime3, localDateTime4).toHours()); }
ケースケース#1とケース#2は本格的なZonedDateTimeクラスで実行され、正しい結果を提供します。この日には矢印が戻ったため、結果として25時間になります。
ケースケース#3は、OffsetDateTimeが時間軸上のポイントに関する情報を完全に格納することを示していますが、ケース#4は、タイムゾーンが失われると、このクラスが異なる計算を実行することを示します。
ケース#5とケース#6でも同じです-Instantは時間軸上のポイントを完全に定義しているにもかかわらず、タイムゾーンなしで計算を実行します。
ケースケース#7およびケース#8-LocalDateTimeが時点を完全に反映せず、タイムゾーンなしで計算を実行できないことを示します。
これらの例が新しいAPIのエラーを示しているとは決して言いたくない(誰かがそう思った場合)。すべての効果は期待され、説明可能です。別のことは緊張します-この振る舞いはJava開発者の軍隊によってどれほど実現されるでしょうか。古いAPIでは、1つのjava.util.Calendarクラスのみがすべての計算に関係していたため、このような潜在的な問題は不可能でした。
おそらく、ZonedDateTimeを除くすべてのクラスで時間の経過とともにほとんどの操作を禁止する価値があったのは、矢印の翻訳の過程で彼だけだからです。タイムゾーンがないと時刻を定義しないため、LocalDateTimeを使用したDurationの計算を禁止する価値があったのでしょう。このようなソリューションの可能性や不可能性について、どういうわけか真剣に議論する準備はできていませんが、新しいAPIには危険を感じています。
期間、期間
新しいAPIには、期間を決定するための2つのクラスがあります。
java.time.Period-タプル(年、月、日)の形式でのカレンダー期間(期間)の説明。
java.time.Duration-正確な継続時間の説明(整数秒)およびナノ秒形式の現在の秒の小数部。
2つの違いは、例が冬時間に切り替えられた日の例に示されています。針が戻るため、この暦日は25時間です。
@Test public void testDuration() throws Exception { Period period = Period.of(0, 0, 1); Duration duration = Duration.of(1, ChronoUnit.DAYS); ZonedDateTime zdt1 = ZonedDateTime.of(2005, 10, 30, 0, 0, 0, 0, ZoneId.of("Europe/Moscow")); ZonedDateTime ztd2 = zdt1.plus(period); Assert.assertEquals(ZonedDateTime.of(2005, 10, 31, 0, 0, 0, 0, ZoneId.of("Europe/Moscow")), ztd2); ZonedDateTime ztd3 = zdt1.plus(duration); Assert.assertEquals(ZonedDateTime.of(2005, 10, 30, 23, 0, 0, 0, ZoneId.of("Europe/Moscow")), ztd3); }
Period.of(0、0、1)を追加すると、次の暦日に正しく移動します。Duration.of(1、ChronoUnit.DAYS)を追加する場合、実際には24時間を追加し、次の暦日に移動しません。
フォーマットと解析
古いAPIでは、java.text.SimpleDateFormatがスレッドセーフではないことは常に驚きでした。SimpleDateFormatは状態を保存する必要がないようであるため、直感的なスレッドセーフが期待されていました。
新しいAPIはこの問題を解決しました。
java.time.format.DateTimeFormatter-クラスは書式設定と解析設定を定義します。
@Test public void testFormat() throws Exception { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:dd z", Locale.ENGLISH); ZonedDateTime zdt1 = ZonedDateTime.of(2005, 10, 30, 0, 0, 0, 0, ZoneId.of("Europe/Moscow")); String text = zdt1.format(formatter); System.out.println(text); TemporalAccessor ta = formatter.parse(text); // java.time.format.Parsed ZonedDateTime zdt2 = ZonedDateTime.from(ta); Assert.assertEquals(zdt1, zdt2); }
JavaDocを見ると、新しいAPIにフォーマットのオプションが追加されていることがわかります。解析が特定の時間クラスを返さず、java.time.Temporal(実装としてjava.time.format.Parsed)を抽象化し、それから、スペアパーツのバッグから、クラスのオブジェクトを組み立てることができることも興味深い必要です。
クラス図
新しいAPIのクラス図を示します。いくつかのマイナークラス、およびjava.util.Serializableやjava.lang.Comparableなどのインターフェイスの実装は提供されていません。
基本的なインターフェース

時代

タイムゾーン

期間と期間

タイムライン


一時クラス


互換性
古いAPIと新しいAPIの間で情報を交換するために、いくつかのメソッドが実装されています。さらに、十分な能力で実装されています。古いAPIは新しいAPIを知っていますが、新しいAPIは古いAPIについて何も知りません。純粋に理論的には、これによりいつかすべての古いクラスを破棄することができますが、これは私たちの生涯で起こるとは思いません。

@Test public void testTimeZoneCompat() throws Exception { ZoneId zoneId1 = ZoneId.of("Europe/Moscow"); TimeZone timeZone = TimeZone.getTimeZone(zoneId1); ZoneId zoneId2 = timeZone.toZoneId(); Assert.assertEquals(zoneId1, zoneId2); } @Test public void testDateCompat() throws Exception { Instant instant1 = Clock.systemUTC().instant(); Date date = Date.from(instant1); Instant instant2 = date.toInstant(); Assert.assertEquals(instant1, instant2); }
繰り返しますが、ニュアンスがあります。java.util.Dateで時刻を駆動する場合、またはその逆の場合、古いAPIはミリ秒単位で動作し、新しいAPIはナノ秒単位で動作するため、精度は回復不能なほど失われます。 java.lang.System#currentTimeMillisの形式で現在時刻の単一ミリ秒ソースがある限り、これは重要ではありませんが、将来、特にテストの場合、これが問題になる可能性があります。
結論
私はまだ新しいAPIから複雑な気持ちを持っています。一方では、大幅な改善がありますが、他方では、2つの主な問題がありました:ZonedDateTime以外のクラスを使用する場合に予期しない誤ったタイムシフトを実行する機能、およびタイムゾーンが操作で利用できないときにランタイムで予期せず例外を取得する機能です。さらに、新しいAPIは古いAPIよりもやや複雑です。これがどれほど重要であるかは、大衆的実践によって示されます。