JavaのDate and Time APIの設計思想が面白い


これまでJava SE 8から入ったDate and Time API([JSR 310][])は使ってこなかったのですが([Joda-Time][]で特に不満はなかったので)、先日調べてみたところ、設計思想が面白いと感じたので、書いてみます。

# パッケージ概要

和暦などの暦が入っている [java.time.chrono][] が多いのでクラス数は多く見えます。

| パッケージ             | 概要                  | インタフェース | クラス | 列挙型 | 例外 |
| ---------------------- | --------------------- | -------------- | ------ | ------ | ---- |
| [java.time][]          | メインAPI             | 0              | 15     | 2      | 1    |
| [java.time.chrono][]   | 和暦などの暦[^chrono] | 6              | 11     | 4      | 0    |
| [java.time.format][]   | 解析・出力            | 0              | 3      | 4      | 1    |
| [java.time.temporal][] | 時刻を調整[^temporal] | 7              | 6      | 2      | 1    |
| [java.time.zone][]     | タイムゾーン          | 0              | 4      | 1      | 1    |
| 合計                   |                       | 13             | 39     | 13     | 4    |

[java.time][]以下のクラスは一見多いですが、普段使う言葉と対応してるものが多いので、すんなり飲み込めそうです。

- 年: [Year][]
- 月: [Month][]
- 年月: [YearMonth][]
- 月日: [MonthDay][]
- 曜日: [DayOfWeek][]
- 時刻
  - タイムゾーンなし: [LocalTime][]
  - タイムゾーン(オフセット)あり: [OffsetTime][]
  - ZonedTimeがない理由は、夏時間の考慮に日付が必須だからのようです[^2]。
- 日付: [LocalDate][]
- 日時
  - タイムゾーンなし: [LocalDateTime][]
  - タイムゾーン(オフセット)あり: [OffsetDateTime][]
  - タイムゾーン(ゾーンID)あり: [ZonedDateTime][]
- 期間
  - 日付: [Period][]
  - 時刻: [Duration][]
- 時計: [Clock][]
- 時点: [Instant][]
  - タイムスタンプ、あるいはUNIX時間を抽象化したものと考えるといいかも。
- タイムゾーン
  - 時分秒のオフセット: [ZoneOffset][]
  - ゾーンID(Asia/Tokyoなど): [ZoneId][]

# 不変

可変のクラスはただ1つ、[java.time.format.DateTimeFormatterBuilder][]だけです。[java.time.temporal.TemporalAdjuster][]など、不変でなくても構わないものがありますが、不変にしたほうがいいよと親切に(口を酸っぱくして?)教えてくれます。

> 実装要件:
> このインタフェースは実装が可変であることを制限しませんが、不変にすることを強くお薦めします。

# メソッドの命名規則

標準的な `get`, `to` 以外にも以下のような単語をメソッドの接頭辞として使用しています。 `with`, `plus`, `minus` は [Joda-Time][] にもありますね。

- インスタンス取得
  - `of`: intなどから
  - `from`: 他のオブジェクトから
- 別のインスタンスを作成
  - `at`: 組み合わせて別のインスタンスを作成(MonthDay + Year → LocalDateなど)
  - `with`: 一部を変更して別のインスタンスを作成(LocalDate#withYear()で年のみ変更など)
  - `plus`, `minus`: 加算、減算して別のインスタンスを作成

# 継承を可能な限り避ける

例外クラスは除いて、ほとんどのクラスは継承を使ってません(Objectを直接継承)。[Joda-Time][Joda-Time.javadoc]が継承を多用していたのとは対照的です。　継承を使っているケースでも、抽象基底クラスはObjectを直接継承し、継承したサブクラスはfinalにし、深い継承を避けています。

# 既存のクラスを徹底的に排除

Java SE 7から導入された[java.nio.Path][]は、既存の[java.io.File][]と`toFile()`, `toPath()`で相互変換ができるようになっています。一方で、旧APIとの相互変換は、旧API側のみに実装されています。

自分は、**お前ら(既存のクラス)はjava.timeの土を踏むことは許さん**という感じを受けました。当初は相互変換は一切用意されていなかったようで[^1]、旧APIに対する怨念すら感じました(ヽ´ω`)

# おわりに

Effective Javaに書いてあることでもあるのですが、不変にしないことで思わぬバグを発生したり、finalにしてなかったばかりに勝手に継承されて泣いたり、いろいろ痛い目にあってきたので、共感するところが多いです。今後は積極的に使っていこうと思います。

[^1]: [日本人のための Date and Time API Tips (ja) - notepad](http://www.coppermine.jp/docs/notepad/2013/12/jsr310-tips.html)
[^2]: [date - Java 8, why not a ZonedTime class? - Stack Overflow](https://stackoverflow.com/questions/26538102/java-8-why-not-a-zonedtime-class/26541249)
[^chrono]: オールドゲーマーは「クロノ・トリガー」の「クロノ」で覚えるといいかも?
[^temporal]: 音楽の「テンポ」と同じイメージ?

[java.time.format.DateTimeFormatterBuilder]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/format/DateTimeFormatterBuilder.html
[java.time.temporal.TemporalAdjuster]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/temporal/TemporalAdjuster.html
[java.time]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/package-summary.html
[java.time.chrono]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/chrono/package-summary.html
[java.time.format]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/format/package-summary.html
[java.time.temporal]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/temporal/package-summary.html
[java.time.zone]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/zone/package-summary.html
[Clock]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/Clock.html
[Duration]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/Duration.html
[Instant]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/Instant.html
[LocalDate]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/LocalDate.html
[LocalDateTime]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/LocalDateTime.html
[LocalTime]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/LocalTime.html
[MonthDay]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/MonthDay.html
[OffsetDateTime]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/OffsetDateTime.html
[OffsetTime]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/OffsetTime.html
[Period]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/Period.html
[Year]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/Year.html
[YearMonth]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/YearMonth.html
[ZonedDateTime]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/ZonedDateTime.html
[ZoneOffset]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/ZoneOffset.html
[ZoneId]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/ZoneId.html
[DayOfWeek]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/DayOfWeek.html
[Month]: https://docs.oracle.com/javase/jp/8/docs/api/java/time/Month.html
[java.nio.Path]: https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/Path.html
[java.io.File]: https://docs.oracle.com/javase/jp/8/docs/api/java/io/File.html
[java.sql.Timestamp]: https://docs.oracle.com/javase/jp/8/docs/api/java/sql/Timestamp.html
[Joda-Time.javadoc]: http://www.joda.org/joda-time/apidocs/
[Joda-Time]: http://www.joda.org/joda-time/
[JSR 310]: https://jcp.org/en/jsr/detail?id=310