web-dev-qa-db-ja.com

Javaの型消去の利点は何ですか?

今日の Tweet を読んだ:

Javaユーザーが型の消去について不満を言うのはおかしいです。これが唯一のことですJavaうまくいきましたが、間違ったことはすべて無視します。

したがって、私の質問は:

Javaの型消去の利点はありますか?後方互換性と実行時パフォーマンスに対するJVM実装の設定以外に、(おそらく)提供する技術的またはプログラミングスタイルの利点は何ですか?

89
vertti

タイプ消去は良いです

事実にこだわりましょう

これまでの回答の多くは、Twitterユーザーに過度に関係しています。メッセンジャーではなくメッセージに集中することは役立ちます。これまでに引用した抜粋だけでも、かなり一貫したメッセージがあります。

Javaユーザーが型の消去について苦情を言うのはおかしいです。これが唯一のことですJavaうまくいきましたが、間違ったことはすべて無視します。

私には大きなメリット(パラメトリックなど)とゼロのコスト(主張されたコストは想像の限界です)があります。

new Tは壊れたプログラムです。 「すべての命題は真実である」という主張と同型です。私はこれにはあまり興味がありません。

目標:合理的なプログラム

これらのつぶやきは、マシンに何かをさせることができるか何かに興味がないという視点を反映していますが、マシンが私たちに何かをさせると推論できるかどうかはもっとわかります実際に欲しい。妥当な推論は証拠です。証明は、正式な表記法またはより正式ではない形式で指定できます。仕様言語に関係なく、それらは明確で厳密でなければなりません。非公式な仕様は正しく構成することは不可能ではありませんが、実際のプログラミングではしばしば欠陥があります。最終的には、非公式の推論に伴う問題を補うために、自動化された探索的テストなどの修正が行われます。これは、テストが本質的に悪いアイデアであると言うことではありませんが、引用されたTwitterユーザーははるかに良い方法があることを示唆しています。

したがって、目標は、マシンが実際にプログラムを実行する方法に対応する方法で、明確かつ厳密に推論できる正しいプログラムを持つことです。ただし、これが唯一の目標ではありません。また、ロジックにある程度の表現力を持たせたいと考えています。たとえば、命題論理で表現できるのはそれだけです。一次論理のようなものから普遍的(∀)および実存的()数量化を行うのは素晴らしいことです。

推論に型システムを使用する

これらの目標は、型システムによって非常にうまく対処できます。これは、 Curry-Howardの対応 のため特に明確です。この対応はしばしば次の類推で表現されます:型はプログラムに対するものであり、定理は証明に対するものです。

この対応はやや深遠です。論理式を取り、それらを型に対応させて変換することができます。次に、コンパイルする同じ型シグネチャを持つプログラムがある場合、論理式が普遍的に真であることを証明しました(トートロジー)。これは、対応が双方向であるためです。型/プログラムと定理/証明の世界の間の変換は機械的であり、多くの場合自動化できます。

Curry-Howardは、プログラムの仕様をどのように処理したいのかをうまく理解しています。

型システムはJavaで役立ちますか?

Curry-Howardを理解していても、型システムの価値を無視するのは簡単だと感じる人もいます。

  1. 取り扱いが非常に難しい
  2. 表現力が限られたロジックに(カリーハワードを通じて)対応
  3. 壊れています(これにより、システムの特性が「弱」または「強」になります)。

最初の点に関しては、おそらくIDEがJavaの型システムを簡単に操作できるようにします(これは非常に主観的です)。

2番目の点については、Javaたまたまalmostは1次論理に対応します。ジェネリックは型システムを使用します残念ながら、ワイルドカードは実存的数量化のほんの一部を提供するだけですが、普遍的な数量化はかなり良いスタートです。List<A>は、すべての可能なリストに対して普遍的に機能します。これは、Aが完全に制約されていないためです。これは、Twitterユーザーが「パラメトリック」に関して話していることにつながります。

パラメトリック性についてよく引用される論文は、Philip Wadlerの Theorems for free! です。この論文の興味深い点は、タイプシグネチャだけから、非常に興味深い不変式を証明できることです。これらの不変式の自動化されたテストを書くとしたら、時間を浪費することになります。たとえば、List<A>flattenの型シグネチャのみから

<A> List<A> flatten(List<List<A>> nestedLists);

私たちはそれを推論することができます

flatten(nestedList.map(l -> l.map(any_function)))
    ≡ flatten(nestList).map(any_function)

それは簡単な例であり、おそらく非公式にそれについて推論することができますが、型システムから無料でそのような証明を正式に取得し、コンパイラによってチェックするとさらに良いです。

消去しないと虐待につながる可能性があります

言語実装の観点から見ると、Javaのジェネリック(ユニバーサル型に対応)は、プログラムの動作に関する証明を得るために使用されるパラメーターに非常に大きく影響します。これは、言及された3番目の問題に到達します。これらの証明と正確さのすべての利点は、欠陥なしに実装されたサウンドタイプシステムを必要とします。 Javaには、間違いなく推論を打ち砕くことができるいくつかの言語機能があります。これらには以下が含まれますが、これらに限定されません。

  • 外部システムの副作用
  • 反射

消去されないジェネリックは、多くの点でリフレクションに関連しています。消去することなく、アルゴリズムの設計に使用できる実装に含まれるランタイム情報があります。これが意味することは、静的に、プログラムについて考えると、全体像がわからないということです。リフレクションは、静的に推論する証明の正確性を著しく脅かします。偶然の反省は、さまざまなトリッキーな欠陥にもつながります。

それで、消去されていないジェネリックが「役に立つ」方法は何ですか?ツイートに記載されている使用方法を考えてみましょう。

<T> T broken { return new T(); }

Tに引数なしのコンストラクタがない場合はどうなりますか?一部の言語では、nullが返されます。または、null値をスキップして、例外を発生させます(null値はとにかくつながるようです)。私たちの言語はチューリング完全であるため、brokenの呼び出しが引数なしのコンストラクターを持つ「安全な」型を含み、どの呼び出しが含まないかを推論することは不可能です。私たちのプログラムが普遍的に機能するという確実性を失いました。

消去とは、推論したことを意味します(消去しましょう)

そのため、プログラムについて推論したい場合は、推論を強く脅かす言語機能を使用しないことを強くお勧めします。それができたら、実行時に型を単にドロップしないのはなぜですか?それらは必要ありません。キャストが失敗しないか、呼び出し時にメソッドが欠落する可能性があるという満足感で、ある程度の効率とシンプルさを得ることができます。

消去は推論を促進します。

89
Sukant Hajra

型は、コンパイラがプログラムの正しさをチェックできるようにプログラムを記述するために使用される構造です。型は値に対する命題です-コンパイラはこの命題が真であることを検証します。

プログラムの実行中、型情報は必要ないはずです。これは、コンパイラによって既に検証されています。コンパイラは、コードの最適化を実行するためにこの情報を自由に破棄する必要があります。実行速度を上げたり、より小さなバイナリを生成したりします。型パラメータを削除すると、これが容易になります。

Javaは、実行時に型情報(リフレクション、instanceofなど)を照会できるようにすることで、静的型付けを中断します。これにより、静的に検証できないプログラムを構築できます。また、静的な最適化の機会を逃します。

型パラメーターが消去されるという事実により、これらの誤ったプログラムの一部のインスタンスを作成できなくなりますが、さらに多くの型情報が消去され、リフレクションおよびinstanceof機能が削除されると、より誤ったプログラムが許可されなくなります。

消去は、データ型の「パラメトリック性」の特性を維持するために重要です。コンポーネントタイプTに対してパラメーター化されたタイプ「リスト」、つまりList <T>があるとします。この型は、このList型はどの型Tでも同じように機能するという命題です。Tが抽象の無制限の型パラメーターであるという事実は、この型について何も知らないことを意味します。

例えばリストxs = asList( "3")があるとします。要素を追加します:xs.add( "q")。最終的に["3"、 "q"]になります。これはパラメトリックであるため、List xs = asList(7);と仮定できます。 xs.add(8)は[7,8]で終わります。このタイプから、Stringに対しても1つのこともIntに対しても1つのことをしないことがわかります。

さらに、私はList.add関数が薄い空気からTの値を生成できないことを知っています。私のasList( "3")に "7"が追加されている場合、唯一の可能な答えは値 "3"と "7"から構築されることを知っています。関数がリストを作成できないため、リストに「2」または「z」が追加される可能性はありません。これらの他の値はどちらも追加するのが賢明ではなく、パラメトリックはこれらの誤ったプログラムの構築を防ぎます。

基本的に、消去はパラメトリックに違反するいくつかの手段を防ぎ、静的な型指定の目標である不正なプログラムの可能性を排除します。

36
techtangents

(私はすでにここに答えを書いていますが、2年後にこの質問を再考しますが、まったく別の答える方法があることに気づきましたので、前の答えをそのままにしてこの質問を追加します。)


Java Genericsが「type erasure」という名前に値するかどうかは議論の余地があります。ジェネリック型は消去されず、生の対応するものに置き換えられるため、より良い選択は「type mutilation 「。

一般的に理解されている意味での型消去の典型的な機能は、アクセスするデータの構造を「ブラインド」にすることで、ランタイムを静的型システムの境界内にとどめることです。これにより、コンパイラにフルパワーが与えられ、静的型のみに基づいて定理を証明できます。また、コードの自由度を制約することでプログラマーを支援し、単純な推論により多くの力を与えます。

Javaの型消去ではそれが達成されません。この例のように、コンパイラは機能しなくなります。

void doStuff(List<Integer> collection) { 
}

void doStuff(List<String> collection) // ERROR: a method cannot have 
                   // overloads which only differ in type parameters

(上記の2つの宣言は、消去後に同じメソッドシグネチャに崩壊します。)

逆に、ランタイムはオブジェクトの型とその理由を検査できますが、真の型に対する洞察は消去によって損なわれるため、静的型違反は達成するのが簡単で、防止するのは困難です。

物事をさらに複雑にするために、元の型と消去された型シグネチャが共存し、コンパイル中に並行して考慮されます。これは、プロセス全体がランタイムから型情報を削除することではなく、下位互換性を維持するために汎用型システムをレガシーの未加工型システムに押し込むことだからです。この宝石は古典的な例です:

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

(冗長extends Objectは、消去された署名の後方互換性を維持するために追加する必要がありました。

さて、それを念頭に置いて、引用を再訪しましょう:

Javaユーザーが型の消去について不満を言うのはおもしろいだけですJava正しくなった

exactly did Java get right?意味に関係なく、Wordそのものですか?対照的に、謙虚なintタイプを見てください:no実行時の型チェックはこれまでに実行されるか、可能ですらあり、実行は常に完全に型保証されます。That's正しく行われた場合の型消去はどのようになりますか:そこにあることすらわかりません。

13
Marko Topolnik

同じ会話の同じユーザーによる後続の投稿:

new Tは壊れたプログラムです。 「すべての命題は真実である」という主張と同型です。私はこれにはあまり興味がありません。

(これは、別のユーザーの声明、つまり「状況によっては「新しいT」の方が良いと思われる」に対する応答であり、new T()は型の消去により不可能であるという考えです(これは議論の余地あり— Tが実行時に利用可能であったとしても、抽象クラスまたはインターフェースであるか、Voidであるか、引数なしのコンストラクター、または引数なしである可能性がありますコンストラクターはプライベートにすることができます(たとえば、シングルトンクラスであるため)、または引数なしのコンストラクターは、ジェネリックメソッドがキャッチまたは指定しないチェック例外を指定できますが、それが前提でした。少なくともT.class.newInstance()を書くことができれば、それらの問題を処理できます。))

型は命題と同型であるというこの見解は、ユーザーが形式型理論の背景を持っていることを示唆しています。 (S)「動的型」や「実行時型」を好まない可能性が高く、ダウンキャストやinstanceofやリフレクションなどのないJavaを好むでしょう。非常に豊富な(静的)型システムを持ち、その動的なセマンティクスが型情報にまったく依存しない標準MLのような言語の場合)

ところで、ユーザーがトローリングしていることを心に留めておく価値があります:(s)彼はおそらく(静的に)型付けされた言語を好むかもしれませんが、(s)彼はnot見る。むしろ、元のツイートの主な目的は、意見が合わない人をto笑することであり、意見が合わない人の一部が声をかけた後、ユーザーは「理由Java hasタイプの消去とは、Wadler等がJavaのユーザーとは異なり、彼らが何をしているのかを知っていることです。残念なことに、これは彼が実際に何を考えているのかを見つけるのを難しくします。ビューに実際の深さがある人々は、一般的にquite this content-freeであるトロルに頼りません。

9
ruakh

ここでまったく考慮されていないことの1つは、OOPのランタイムポリモーフィズムが実行時の型の具体化に基本的に依存していることです。反抗された型によってバックボーンが適切に保持されている言語が、その型システムに大きな拡張を導入し、型消去に基づいている場合、認知的不協和は避けられない結果です。これがJavaコミュニティに起こったことです。これが、型消去が多くの論争を集めた理由であり、最終的には Javaの将来のリリースで取り消す計画ですfunnyという苦情で何かを見つけることは、JavaユーザーがJavaの精神に対する正直な誤解、または意識的に非難するジョークのいずれかを裏切る。

「消去のみが正しいJava正しくなった」という主張は、「関数引数の実行時型に対する動的ディスパッチに基づくすべての言語は根本的に欠陥がある」という主張を暗示しています。独自の、そしてJavaを含むすべてのOOP言語の有効な批評と見なすことさえできるものは、機能を評価および批評するための重要なポイントとしてそれ自体を留保することはできませんJavaのコンテキスト内で、実行時多態性は公理的です。

要約すると、「型消去は言語設計の方法である」と正当に述べるかもしれませんが、Java)内で型消去をサポートする位置は、 、それはあまりにも遅すぎます。オークがSunに受け入れられ、Javaに名前が変更された歴史的な瞬間にもすでにそうでした。



静的型付け自体がプログラミング言語の設計における適切な方向であるかどうかに関して、これは、プログラミングの活動を構成すると考えるもののはるかに広い哲学的文脈に適合します。古典的な数学の伝統に明らかに由来する考え方の1つは、プログラムを数学的概念またはその他(命題、関数など)のインスタンスと見なしますが、プログラミングを方法と見なすまったく異なるクラスのアプローチがあります機械に話しかけて、それから何が欲しいかを説明してください。この見方では、プログラムは動的に組織的に成長するエンティティであり、静的に型付けされたプログラムの慎重に建てられた建物の劇的な反対です。

動的言語をその方向への一歩とみなすのは自然に思えます:プログラムの一貫性はボトムアップから現れ、アプリオリ制約を課すことなくトップダウン方式。このパラダイムは、私たち人間が開発と学習を通じて私たちのものになるプロセスをモデル化するためのステップとみなすことができます。

8
Marko Topolnik

タイプの消去が良いことである理由は、それが不可能にすることは有害だからです。実行時に型引数の検査を防止すると、プログラムについての理解と推論が容易になります。

私がやや直感に反することに気づいたのは、関数シグネチャがmoreジェネリックである場合、それらが理解しやすくなるということです。これは、可能な実装の数が削減されるためです。このシグニチャを使用したメソッドを考えてみましょう。これには何らかの副作用がないことがわかっています。

public List<Integer> XXX(final List<Integer> l);

この関数の可能な実装は何ですか?非常に多くの。この関数が何をするかについてはほとんど知ることができません。入力リストを逆にした可能性があります。 intをペアリングして合計し、半分のサイズのリストを返すこともできます。想像できる他の多くの可能性があります。今考慮してください:

public <T> List<T> XXX(final List<T> l);

この関数の実装はいくつありますか?実装は要素のタイプを知ることができないため、膨大な数の実装を除外できるようになりました。要素を組み合わせたり、リストに追加したり、フィルターで除外したりすることはできません。アイデンティティ(リストへの変更なし)、要素のドロップ、リストの反転などに限定されます。この関数は、その署名のみに基づいて推論するのが簡単です。

を除いて…in Javaあなたはいつでも型システムをごまかすことができます。その汎用メソッドの実装はinstanceofチェックや任意の型へのキャストのようなものを使用できるため、 could要素のタイプを検査し、結果に基づいて任意の数の処理を実行します。これらのランタイムハッキングが許可されている場合、パラメーター化されたメソッドシグネチャははるかに少なくなります。私たちにとって有用です。

Javaに型消去がなかった(つまり、実行時に型引数が具体化された))場合、これは単純にmoreこの種の推論障害のシェナンガンを許可します。上記の例では、リストに少なくとも1つの要素がある場合にのみ、実装は型シグネチャによって設定された期待値に違反できますが、Tが具体化された場合、リストが空であってもそうすることができます。コードの理解を妨げる可能性を(すでに非常に多く)増やします。

型を消去すると、言語の「パワフル」が低下します。しかし、ある種の「力」は実際には有害です。

5
Lachlan

1つの良い点は、ジェネリックが導入されたときにJVMを変更する必要がなかったことです。 Javaは、コンパイラレベルでのみジェネリックを実装します。

5

これは直接的な答えではありません(OPは「利点は何か」と尋ねましたが、「欠点は何か」と答えています)

C#型システムと比較して、Java型消去は2つのレーソンにとって本当の痛みです

インターフェースを2回実装することはできません

C#では、特に2つのタイプが共通の祖先(つまり、祖先isObject)を共有しない場合、_IEnumerable<T1>_と_IEnumerable<T2>_の両方を安全に実装できます。

実際の例:Spring Frameworkでは、_ApplicationListener<? extends ApplicationEvent>_を複数回実装することはできません。 Tに基づいて異なる動作が必要な場合は、instanceofをテストする必要があります

新しいT()を行うことはできません

他の人がコメントしたように、new T()の実行はリフレクションを介してのみ実行でき、コンストラクターに必要なパラメーターを確認します。 C#では、Tをパラメーターなしのコンストラクターに制約する場合、new T()onlyを実行できます。 Tがその制約を尊重しない場合、コンパイルエラーが発生します。

私がC#の作成者であれば、コンパイル時に簡単に検証できる1つ以上のコンストラクター制約を指定する機能を導入していました(そのため、たとえば_string,string_ paramsを持つコンストラクターが必要になります)。でも最後は憶測です

追加のポイントは、他の答えのどれも考慮していないようです:実行時の型付けでジェネリックが本当に必要な場合、あなたはそれを自分で実装できますこのように:

public class GenericClass<T>
{
     private Class<T> targetClass;
     public GenericClass(Class<T> targetClass)
     {
          this.targetClass = targetClass;
     }

このクラスは、Javaが消去を使用しなかった場合、デフォルトで達成可能なことをすべて実行できます。新しいTsを割り当てることができます(T使用する予定のパターンに一致するコンストラクター、またはTsの配列があり、特定のオブジェクトがTであるかどうかを実行時に動的にテストし、それに応じて動作を変更できます。など。

例えば:

     public T newT () { 
         try {
             return targetClass.newInstance(); 
         } catch(/* I forget which exceptions can be thrown here */) { ... }
     }

     private T value;
     /** @throws ClassCastException if object is not a T */
     public void setValueFromObject (Object object) {
         value = targetClass.cast(object);
     }
}
2
Jules

ほとんどの回答は、実際の技術的な詳細よりもプログラミング哲学に関心があります。

そして、この質問は5年以上前のものですが、質問はまだ残っています。なぜ技術的な観点から型消去が望ましいのでしょうか?最終的に、答えはかなり単純です(上位レベル): https://en.wikipedia.org/wiki/Type_erasure

C++テンプレートは実行時に存在しません。コンパイラーは、呼び出しごとに完全に最適化されたバージョンを出力します。つまり、実行は型情報に依存しません。しかし、JITは同じ機能の異なるバージョンをどのように処理しますか?関数を1つだけ持つ方が良いと思いませんか? JITがそのすべての異なるバージョンを最適化する必要はありません。それでは、タイプセーフについてはどうでしょうか。それは窓から出なければならないことを推測します。

しかし、ちょっと待ってください。NETはどのようにそれを行いますか?反射!この方法では、1つの関数を最適化するだけでなく、実行時の型情報も取得できます。そして、それが.NETジェネリックが以前より遅くなった理由です(より良くなりましたが)。私はそれが便利ではないと主張しているのではありません!しかし、それは高価であり、絶対に必要でない場合は使用すべきではありません(コンパイラ/インタープリターはリフレクションに依存しているため、動的に型付けされた言語では高価とは見なされません)。

このように、型消去を使用した汎用プログラミングのオーバーヘッドはほぼゼロです(一部のランタイムチェック/キャストが依然として必要です): https://docs.Oracle.com/javase/tutorial/Java/generics/erasure.html =

0
Marvin H.

同じコードが複数の型に使用されるため、c ++のようなコードの膨張を回避します。ただし、型の消去には仮想ディスパッチが必要ですが、c ++-code-bloatアプローチは非仮想ディスパッチジェネリックを実行できます

0
necromancer