Javaプログラミング言語の生産性とパフォーマンスに対して継続的に探求してきた成果を用いて、オラクルのJava言語アーキテクトであるBrian Goetz氏はデータクラスの実験的な概念を紹介した。これは将来言語に統合される可能性が高い。彼の研究はバリュータイプやパターンマッチングのような予定されている機能とデータクラスが自然にフィットすることを証明している。しかしこの概念がJava言語の一部となる準備を整えるには、なされるべき作業は多い。Goetz氏は時として"データは単にデータである"ことを前提としてデータクラスの課題とトレードオフを調査している。

動機

Javaクラスは今まで大量のボイラープレートコードを必要とした。クラスが単純か複雑かどうかにかかわらずだ。このことでJavaは"冗長すぎる"という評価であった。Goetz氏はこう説明している。

ごく普通のデータキャリアクラスを責任を持って書くにあたり、価値の低い、退屈なコードを大量に書かなければならない。コンストラクタ、アクセサ、 equals() 、 hashCode() 、 toString() などだ。そして開発者はこうした重要なメソッドを書かないという手抜きへ誘惑されることもある。こうしたことがびっくりするような振る舞いや、デバッグがとてもしづらいといったことにつながる。もしくは代わりにまったく適切でないクラスをサービスに押し込んでしまう。それは"正しい形"をしているし、開発者は別のクラスにもう定義したくないからだ。 IDEはあなたがこうしたコードをほとんど書く手助けをしてくれるが、コードを書くことはこの問題の小さな一部分に過ぎない。IDEはコードを読む人がボイラープレートコードの大量の行から"これはxとy、zのごく普通のデータキャリアクラスです"という設計意図を汲み取る手助けは何もしてくれない。そして退屈なコードはバグが潜む格好の場所だ。もしできるなら隠れられる場所を完全に排除するのが一番だ。

Scala ( case )やKotlin ( data )、C# ( record )で定義されている、コンパクトになるよう設計されたクラス宣言と同様に、Javaクラスが最小のオーバーヘッドでごく普通のデータキャリアとなるというのが真実である可能性は高い。ごく普通のデータキャリアについて公式な定義がないまま、ほとんどのJava開発者がそれを認識できる可能性はかなり高いだろう。Javaコミュニティが言語にデータクラスのメカニズムが入ることを本当に喜んでいる一方で、ごく普通のデータキャリアということへの個々の解釈というのは大いに異なることもあり得る。Goetz氏は群盲象を評すのたとえ話を説明に使っている。

AlgebraicのAnnieは"データクラスは単なる代数の積型だ"と言う。Scalaのケースクラスのように、パターンマッチングとペアになってやって来て、イミュータブルにするのが一番おいしいものだ (そしてデザートにAnnieはsealedインタフェースを注文するだろう)。 ボイラープレートのBillyは"データクラスは単によりよいシンタックスがある普通のクラスだ"と言う。変更可能性や拡張性、カプセル化の点で制約があることに苛立つ可能性が高い (Billyの弟であるJavaBeanのJerryは"JavaBeansのためのものでなければならない。もちろんgettersとsettersもね"と言う。そして妹であるPOJOのPattyは自分がエンタープライズのPOJOの中に埋もれていると言い、彼女はHibernateのようなフレームワークがプロキシにできるようなものをほしがっているのだと私たちは気づく) 。 TupleのTommyは"データクラスは単にnominalなタプルだ"と言う。それらがコアのオブジェクトメソッド以外のメソッドを持てるとさえ思わないかもしれない。単に集合のもっとも単純なものである (彼は名前も取り除かれると思うかもしれない。同じ"形"である2つのデータクラスが自由に変換できるようになるように。)。 ValueのVictorは"データクラスは実のところより透過的な単なるバリュークラスだ"と言う。 この登場人物全員が"データクラス"を支持して結束している。しかしデータクラスが何であるかに関して考えが異なる。1つの解決策で彼ら全員を満足させるものはないかもしれない。

問題を理解する

データクラスの概念は、単にボイラープレートコードの削減以上のことをする。Goetz氏が主張していることは、カプセル化のコストがすべてのJavaクラス間で共有されることにある"より深い問題へのほんの兆し"である。抽象化とカプセル化のオブジェクト指向の原則により、Java開発者は多くの境界を越えて堅牢で安全なコードを書ける。

メンテナンス境界

セキュリティと信頼の境界

完全性境界

バージョニング境界

SocketInputStream のようなクラスにとって、その固有の複雑さが原因でこれらの境界線が不可欠である。しかし、2つの数値コンポーネントのための、ごく普通のデータキャリアクラスは次のように宣言する。

record Point(int x,int y) { ... }

こうした境界への関心というのは本当に必要なのか？Goetz氏はこう説明する。

これらの境界 (コンストラクタ引数がどのように状態にマッピングされるか、状態からどのように等価契約を作るのか、など) を確立しそれを守るコストはクラスを越えて一定ではあるが、利点はそうではなく、コストが利点に見合わないことがあるかもしれない。これはJava開発者が"形式ばったことが多すぎる"という言葉で言っていることだ。その言葉の意味は、形式張ったことには価値がないのではなく、逆にそれが十分な価値を提供しないときでさえ使用を強制させられる、ということなのだ。 Javaが提供するカプセル化のモデル -- 構築や状態アクセス、等価性から表現が完全に切り離されている場所 -- は多くのクラスが必要とするものにとどまらない。それらの境界とより単純な関係を持つクラスは、クラスをその状態への薄いラッパーとして定義し、それから状態や構築、等価性、状態アクセス間の関係を導出する単純なモデルから恩恵を得られる。 さらにAPIから表現を切り離すコストはボイラープレートであるメンバーを宣言するオーバーヘッドより大きい。カプセル化は、その本質として情報を失わせるものだ。

データクラスの必要条件

上記の Point の宣言を使って、ごく普通のデータキャリア定義をデシュガーしたとしよう。

final class Point extends java.lang.DataClass { public final int x; public final int y; public Point(int x,int y) { this.x = x; this.y = y; } // destructuring pattern for Point(int x,int y) // state-based implementations of equals(), hashCode(), and toString() // public read accessors x() and y() }

ごく普通のデータキャリアの設計についてさらに調査するため、Goetz氏は"安全に、コンストラクタやパターン抽出子、アクセサ、 equals() 、 hashCode() 、 toString() 、さらにそれ以上のものに対するボイラープレートの機械的な生成"への一連の要求 (もしくは制約) を定義した。

以下のことを満たすとき、クラス C が状態ベクトル S に対する透過的なキャリアであるとしよう。 関数ctorがある。 S -> C 、これは状態ベクトルのインスタンスを C のインスタンスにマップする (コンストラクタはいくつかの状態ベクトルを無効なものとして拒絶するかもしれない。たとえば分母が0の有理数)。

-> 、これは状態ベクトルのインスタンスを のインスタンスにマップする (コンストラクタはいくつかの状態ベクトルを無効なものとして拒絶するかもしれない。たとえば分母が0の有理数)。 全域写像dtorがある。 C -> S は C のインスタンスをctorのドメインにおける状態ベクトル S にマップする。

-> は のインスタンスをctorのドメインにおける状態ベクトル にマップする。 C のあらゆるインスタンスcに対して、 C への equals() の契約に従えばctor(dtor(c))はcと等しい。

のあらゆるインスタンスcに対して、 への の契約に従えばctor(dtor(c))はcと等しい。 2つの状態ベクトルs1とs2に対して、それらのコンポーネントのそれぞれが他方の対応するコンポーネントに等しければ (コンポーネントの equals() の契約に従って)、ctor(s1)もctor(s2)も両方未定義となる、もしくは C に対する equals() の契約の下2つは等しい。

の契約に従って)、ctor(s1)もctor(s2)も両方未定義となる、もしくは に対する の契約の下2つは等しい。 等価なインスタンス c と d に対して、同一の操作を呼び出すと等しい結果となる。 c.m() は d.m() と等しい。その上、操作後 c と d は依然として等価であるべきである。 こうした不変条件は私たちの要求を捉えようとする試みだ。キャリアは透過的であるということ、クラス表現と構築、分解の間に単純で予測可能な関係があるということ。APIが表現であるということだ。

データクラスとパターンマッチング

Goetz氏が言うように、ごく普通のデータキャリアには強みがある。"集約形態と分解状態の間を自由に行き来してデータクラスのインスタンスを変換できる"ことだ。これは好都合なことにパターンマッチングとうまく機能するだろう。Goetz氏のパターンマッチングの文書で説明されているように、Goetz氏は switch 構文の活用において分解と改善を考察している。この点を考慮すると、以下のようなコードを書くことができるようになるだろう。

interface Shape { ... } record Point (int x,int y) { ... } record Rect(Point p1,Point p2) implements Shape { ... } record Circle(Point center,int radius) implements Shape { ... } ... switch(shape) { case Rect(Point(var x1,var y1),Point(var x2,var y2)) : ... case Circle(Point(var x,var y),int radius): ... }

Shape のどんな具象クラスのインスタンスも switch 文の中で簡単に分解できる。これはシリアライゼーションやJSONとXMLのマーシャリング/アンマーシャリング、データベースマッピングなどの外部化にも役立つだろう。

設計空間の向上

Goetz氏はごく普通のデータキャリアを持つという要求にあるトレードオフについて考察した。こう述べている。

もっとも単純で -- そしてもっとも厳格な -- データクラスに対するモデルは次のようなfinalクラスである、とすることだ。データクラスが各状態コンポーネントをpublic finalなフィールドで持ち、publicなコンストラクタと、状態記述のシグネチャとシグネチャがマッチする分解のパターンと、コアであるObjectのメソッドの状態ベースの実装とを持ち、さらにほかのメンバ (もしくは暗黙的なメンバへの明示的な実装) が許可されていないというものだ。これは本質的にnaminalなタプルへのもっとも厳格な解釈である。 これは開始地点として単純で安定している。そしてこのことに対してほとんど全員が何か異議を唱えたくなるだろう。どのくらいこうした制限を、私たちが欲するセマンティック上の利点を諦めることなく緩められるだろうか？厳格な開始地点を拡大するような方針をいくつか、そしてその相互作用を見てみよう。

これらの方針は幅広い設計要素と関連する問題をカバーする。

インタフェースと追加メソッド "状態のみ"というルールを破るリスクがある。

暗黙的なメンバのオーバーライド ごく普通のデータキャリアの要件を破るリスクがある。

追加コンストラクタ オブジェクトの状態と状態の記述が等しいことを保証する。

追加フィールド "状態、状態全体、状態のみ"というルールを破るリスクがある。

拡張 データクラスと通常のクラスの間に拡張に関係する問題がある。

変更可能性 データクラスを変更可能とする理論的根拠に疑問がある。

フィールドのカプセル化とアクセサ カプセル化したフィールドは読み取り可能でなければならないことを保証する。

配列と防御的コピー 防御的コピーは等しいインスタンスであることを保証するので、配列の分解と再構築における不変性に違反する。

スレッド安全性 データクラスにおいて変更可能であることをどのようにスレッドセーフにするのか疑問がある。



概要

Javaは2017年すばらしい年を過ごしたが、今年は言語に関して多くの刺激的なことがある。しかし、Goetz氏がInfoQに語ったように、データクラスはその概念が現実のものとなる方法を完全に理解するまでに依然として多くの作業が必要な"不完全な"アイデアとみなされている。

まとめとして、Goetz氏はこう説明している。

Javaで"ごく普通のデータの集約"に対する機能を設計することにおける鍵となる問題点は、諦めてよいことに対する自由度を割り出すことである。クラスの自由度をすべてモデル化しようと試みれば、単に複雑度を周辺に移すだけである。利点をいくつか得るために、制約をいくつか受け入れなければならない。と考えている。受け入れるべき実用的な制約とは、APIから切り離した表現や状態への読み取りアクセスを仲介するカプセル化を利用できなくすることだ。次に、このことはこうした制約を受け入れられるクラスに対して重要なシンタックスやセマンティック上の利点を提供する。

オラクル所属で技術スタッフのプリンシパルメンバであるVicente Romero氏は、データクラス開発に関して"初公開となる活動"を投稿した。開発内容はProject Amberリポジトリのdatumブランチにある。

Goetz氏はデータクラス研究についてInfoQに語った。

InfoQ「文書を公開した後、あなたが受け取ったコミュニティからの反応はどんなものでしたか？」

Brian Goetz氏「予想していた反応でした。このアイデアや"改善"方法に対するさまざまな提案 (大部分は互いに矛盾しています) についてとてもポジティブなコメントがいくつかありました。つまり、みなさんこのアイデアを気に入っていますが、予想したように、多くの人は個人的な好みに合うように設計の中心をある方向へ、もしくは他の方向へ私たちを向かわせようとしています。非常に主観的な機能なので、これは予想していたことです。」

InfoQ「データクラスのメカニズムが将来Javaプログラミング言語に入ることを思い描いていますか？もしそうなら、文書であなたが議論していた懸念点すべてを対処するにはどのような取り組みが必要でしょうか？」

Goetz氏「"じっくり焼く時間"が必要でしょう。言語設計では、最初のアイデアはどんなに注意深く考え抜いたものであっても間違いがあります。2番目のアイデアもそうです。多くの言語機能には最終的に正しい着地点を見つけるまでに6回以上は繰り返しが必要です。なので私たちは実験し、プロトタイプを作り、フィードバックを集め、何度も繰り返しています。正しい場所を見つけたと感じるまで。」

InfoQ「Java以外の言語でのコンパクトなクラス (たとえばScalaのケースクラス) の実装をデータクラスの上に再度構築することを促すというのは目標にありますか？それと目標ではありませんか？」

Goetz氏「すべての言語には独自の表面上のシンタックスがあります。しかし、データクラスは他の言語機能、たとえばパターンマッチングにつながっており、(ラムダで起こったように) 他言語がこうした機能へのランタイムでのサポートを目標にして、相互作用的な利点を享受してくれると思っています。」

InfoQ「あなたが知る限りでかまいませんが、ScalaやKotlin、C#のアーキテクトはよりコンパクトなクラス宣言の実装において似たような課題に直面したのでしょうか？」

Goetz氏「実際そうだったでしょう。もっともKotlinとScalaはC#の場合よりもプロジェクト開始時点でこのことがより身近なものとなっていましたが。なので、進める際の制約はより少なかったでしょう。それぞれ設計空間では少し異なる点があります。」

InfoQ「データクラスに関して読者に知ってほしいことの中で、もっとも重要なメッセージを1つ選ぶとすれば何でしょうか？」

Goetz氏「データクラスはデータについてのもので、シンタックスの簡略化についてのことではありません。オブジェクトモデルにある純粋なデータをモデル化する自然な手段を提供することに関するものです。たとえデータクラスが提供する簡略化のメリットを望んだとしても、クラスすべてが純粋なデータキャリアではないのです。」

InfoQ「データクラス研究では近い将来何がありますか？」

Goetz氏「データクラスが必要とする機能を細かな機能に、全クラスで利用可能な機能に分割することです。たとえば、明らかにデータキャリアではないクラスにおいても、コンストラクタはエラーが起こりやすいことの繰り返しに満ちており、これはコンストラクタのパラメータと表現を高レベルで対応づけることで置き換えることができます。こうすれば、データクラスがよりシンプル (他言語の機能への単なる糖衣) になり、より多くのクラスがデータクラスに自身を押し込めることなく機能のメリットを得られるでしょう。」

資料