kagamihogeの日記

kagamihogeの日記です。

JEP 359: Records (Preview)をテキトーに訳した

http://openjdk.java.net/jeps/359

JEP 359: Records (Preview)

Author   Brian Goetz
Owner   Vicente Arturo Romero Zaldivar
Type    Feature
Scope   SE
Status  Candidate
Component   specification / language
Discussion  amber dash dev at openjdk dot java dot net
Effort  M
Duration    M
Reviewed by Alex Buckley
Created 2019/04/19 19:30
Updated 2019/08/27 23:17
Issue   8222777

Summary

Javaプログラミング言語recordsで機能拡張します。shallowlyなイミュータブルデータの透過的なホルダーとなるクラスを宣言するためのコンパクトなシンタックスがrecordです。

Motivation and Goals

Javaに対するよくある批判に"冗長すぎる(Java is too verbose)"とか"大仰(ceremony)"があります。その中で最悪なものの一つに、単なる集約をするだけの"データの入れ物(data carriers)"でしかないクラス、があります。入れ物クラスを適切に作るには、大量の低レベルの値・似たようなコードの繰り返し、間違えやすいコンストラクタ・アクセサ・equals()hashCode()toString()など、が必要です。開発者はこれら重要なメソッドなどを省略したり(結果奇妙な振る舞いやデバッグしにくいコードになる)、完全ではない代替クラスをサービスに入れたり(とりあえず動くので後でちゃんとしたクラスを作りたくなくなる)、などの誘惑に駆られます。

IDEの支援機能で入れ物クラスのコードを書くことは出来ますが、ボイラープレートから"このクラスはx,y,xの入れ物クラスです(I'm a data carrier for x, y, and z)"という設計意図を読むことは出来ません。正しく読み・書き・確認するための、Javaでシンプルな集約モデルを書けるようにします。

recordは表面的にはボイラープレート削減策として魅力的に映りますが、それよりも、セマンティックな強化が目的です。データとしてデータをモデリングする(modeling data as data)。(セマンティクスが正しければ、ポイラープレート自身が処理を行う) 。この機能は、shallowly-immutableで一般的なデータ集約を宣言するのが容易で明瞭簡潔にします。

Non-Goals

"ボイラープレート戦争(war on boilerplate)"を唱えることが目的ではありません。特に、JavaBean命名規約によるmutableクラスに関する問題は扱いません。プロパティ・メタプログラミングアノテーション駆動コード生成、などの機能追加は行いません。たとえ、これらが問題に対する"解決策(solutions)"として頻繁に提案されていたとしても、です。

Description

RecordsとはJava言語の新しい種類の型宣言です。enumのような、クラスに制限を加えた形式がrecordです。recordはそれが持つ表現(representation)を宣言し、その表現にマッチするAPIをコミットします*1。recordはクラスが通常持つ自由度を制限し、表現とAPIを分離します。その結果、recordにより簡潔さが得られます。

recordは名前と状態記述(state description)を持ちます。状態記述はrecordのコンポーネント(components)を宣言します。また、recordはボディを持つことも出来ます。以下は例です。

record Point(int x, int y) { }

recordは、データの透過的なホルダーという、シンプルなセマンティック上の意図を記述するので、recordは自動的に多数のメンバを作ります。

  • 状態記述コンポーネントそれぞれに対してprivate final fieldを付与。
  • 状態記述コンポーネントそれぞれに対してpublicなreadアクセサメソッドを作成、コンポーネントと同名で同一型。
  • 状態記述と同一シグネチャを持つpublicコンストラクタ、これは対応する引数で各フィールドを初期化する。
  • equalshashCodeの実装。2つのrecordが同一型かつ同一状態を持つ場合に等しい。
  • toStringの実装。コンポーネント名と文字列表現をすべて返す。

recordの表現は状態記述から機械的に生成し、construction, deconstruction(最初にアクセサ、次に、パターンマッチングが有ればdeconstruction patterns), 同値性(equality), 表示(display)、も同様です。

Restrictions on records

recordは、他のクラスをextends出来ず、状態記述コンポーネントのprivate finalフィールド以外のインスタンスフィールドを宣言出来ません。他のフィールドはstaticが必須です。この制約により状態記述だけが表現を定義していることを保証します。

recordは暗黙的にfinalでabstractに出来ません。recordのAPIは状態記述によってのみ定義され、あとから別クラスやrecordによる拡張が出来ないこと、をこの制約により強調しています。

recordのコンポーネントは暗黙的にfinalです。この制約は、集約データ用に広く受け入れられている、デフォルトでイミュータブル(immutable by default)を表します。

上記制限はありますが、recordは通常のクラスのようにも振る舞います。トップレベルやネスト・ジェネリック・interfaceの実装・newインスタンス化、が可能です。recordのボディに、staticメソッド・staticフィールド・static初期化子・コンストラクタ・インスタンスメソッド・インスタンス初期化子・ネストクラス、を宣言出来ます。recordと状態記述のコンポーネントにはアノテーションを付与出来ます。ネストしたrecordは暗黙的にstaticになり、これはエンクロージングインスタンスが暗黙的にrecordへ状態追加するのを避けるためです。

Explicitly declaring members of a record

状態記述から自動生成されるメンバは明示的な宣言も可能です。ただし、不用意にアクセサやequals/hashCodeを実装するとrecordの不変性を破るリスクがあります。

canonicalコンストラクタ(recordの状態記述と一致するシグネチャを持つコンストラクタ)の明示的な宣言には特別の注意が必要です。コンストラクタをformal parameter list(状態記述を指すと仮定)無しで宣言する場合、コンストラクタ正常終了時に未初期化(definitely unassigned)recordフィールドは、対応するformal parameterが有れば、暗黙的に初期化します(this.x = x)。これにより、明示的なcanonical constructorでパラメータに対してvalidationとnormalizationだけを書き、フィールド初期化を省略できます。

record Range(int lo, int hi) {
  public Range {
    if (lo > hi)  /* ここで暗黙的なコンストラクタパラメータを参照 */
      throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
  }
}

Grammar

RecordDeclaration:
  {ClassModifier} record TypeIdentifier [TypeParameters] 
    (RecordComponents) [SuperInterfaces] [RecordBody]

RecordComponents:
  {RecordComponent {, RecordComponent}}

RecordComponent:
  {Annotation} UnannType Identifier

RecordBody:
  { {RecordBodyDeclaration} }

RecordBodyDeclaration:
  ClassBodyDeclaration
  RecordConstructorDeclaration

RecordConstructorDeclaration:
  {Annotation} {ConstructorModifier} [TypeParameters] SimpleTypeName
    [Throws] ConstructorBody

Annotations on record components

レコードのコンポーネントアノテーションは、そのアノテーションがレコードコンポーネント・パラメータ・フィールド・メソッド、で使用可能であれば付与出来ます。これらに対するアノテーションは暗黙的に宣言されるメンバにも自動的に適用されます。

レコードコンポーネントの型をmodifyするtype annotationは暗黙的に宣言されるメンバ(コンストラクタ・パラメータ・フィールド宣言・メソッド宣言)の型にも自動的に適用されます。メンバの明示的な宣言をする場合、対応関係にあるレコードコンポーネントの型と一致する必要があります。なおtype annotationsは含みません。

Reflection API

以下のpublicメソッドをjava.lang.Classに追加します。

  • RecordComponent[] getRecordComponents()
  • boolean isRecord()

メソッドgetRecordComponents()は新規クラスjava.lang.reflect.RecordComponentの配列を返します。配列の要素はレコードコンポーネントに対応し、その順序はレコード宣言順です。配列のRecordComponentから取得可能な情報としては、名前・型・generic type・アノテーション・アクセサメソッド、です。

メソッドisRecord()は、そのクラスがレコードとして宣言されていた場合、trueを返します。(isEnum()と似た関係)

Alternatives

Recordsは名目上のtuplesと見なせます。recordではなくstructural tuplesを実装するという案もあります。

tuplesは軽量な集約(lighterweight means of expressing some aggregates)となり、劣化集約になりがちです。

  • Javaの中核思想では名前が重要(names matter)です。クラスとそのメンバは意味のある名前を持ちますが、タプルとタプルコンポーネントはそうではありません。つまり、匿名タプルのString, Stringよりも、PersonクラスがプロパティがfirstNameと``'lastName```を持つ方が明確で明白です。
  • クラスはコンストラクタで状態のvalidationが可能ですが、タプルはそうではありません。不変データを持つ何らかの集約(数値範囲など)を、コンストラクタで強制出来れば、以降はその事に依存できます。タプルにはそれが出来ません。
  • クラスはその状態に基づく振る舞いを持ちます。状態と振る舞いを一緒にすることで可読性が上がります。タプルはrawデータの関係上そうした事は出来ません。

Dependencies

recordsはsealed types (JEP 360)と組み合わせて使うと便利です。ある種の構造体を共に形成するrecordsとsealed typesは代数的データ型(algebraic data types)と呼ぶことがあります。また、recordはパターンマッチングを適用しやすいです。recordsがAPIと状態記述を対で持つので、recordでdeconstruction patternが使用可能となり、type patternやdeconstruction patternsを使うswitch式での網羅性チェックにsealed typeが使用可能となります。

*1:It declares its representation, and commits to an API that matches that representation. たぶん何か抽象度の高い言い回しなんだけどこの場合のcommit toて何と訳せば良いのやら