今日私は言ったつぶやきを読みました:
Javaユーザーが型の消去について文句を言うのはおかしいです。これは、Javaが正しくなった唯一のものであり、間違ったものはすべて無視しています。
したがって、私の質問は:
Javaの型消去のメリットはありますか?下位互換性とランタイムパフォーマンスのためのJVM実装設定以外に(おそらく)提供する技術的またはプログラミングスタイルの利点は何ですか?
今日私は言ったつぶやきを読みました:
Javaユーザーが型の消去について文句を言うのはおかしいです。これは、Javaが正しくなった唯一のものであり、間違ったものはすべて無視しています。
したがって、私の質問は:
Javaの型消去のメリットはありますか?下位互換性とランタイムパフォーマンスのためのJVM実装設定以外に(おそらく)提供する技術的またはプログラミングスタイルの利点は何ですか?
回答:
これまでの回答の多くは、Twitterユーザーに過度に関係しています。メッセンジャーではなくメッセージに集中することは役に立ちます。これまでに述べた抜粋だけでもかなり一貫したメッセージがあります:
Javaユーザーが型の消去について文句を言うのはおかしいです。これは、Javaが正しくなった唯一のものであり、間違ったものはすべて無視しています。
私は大きな利益(例:パラメトリック性)とゼロのコスト(申し立てられたコストは想像力の限界です)を得ます。
new Tは壊れたプログラムです。「すべての命題は真実である」という主張に同型である。私はこれに大したことはありません。
これらのつぶやきは、マシンに何かをさせることができるかどうかではなく、マシンが実際に望むことを行うと推論できるかどうかという観点を反映しています。適切な推論は証拠です。証明は、正式な表記またはあまり正式でない表記で指定できます。仕様言語に関係なく、それらは明確で厳密でなければなりません。非公式な仕様を正しく構成することは不可能ではありませんが、実際のプログラミングにはしばしば欠陥があります。私たちは、非公式な推論で抱えている問題を補うために、自動化された探索的テストなどの修正を行います。これは、テストが本質的に悪い考えであると言っているわけではありませんが、引用されているTwitterユーザーは、もっと良い方法があることを示唆しています。
したがって、私たちの目標は、マシンが実際にプログラムを実行する方法に対応する方法で、明確かつ厳密に推論できる正しいプログラムを持つことです。ただし、これが唯一の目標ではありません。また、ロジックにはある程度の表現力を持たせたいと考えています。たとえば、命題論理で表現できるのは非常に限られています。一次論理のようなものから普遍的(and)と実存的(∃)の定量化があると便利です。
これらの目標は、型システムによって非常に適切に対処できます。これは、カリーハワード対応のため特に明確です。この対応は多くの場合、次の類推で表されます。型はプログラムに対するものであり、定理は証明に対するものです。
この対応はやや深遠です。論理式を取り、型への対応を通じてそれらを変換できます。次に、コンパイルする同じ型シグネチャを持つプログラムがある場合、論理式が普遍的に真であることを証明しました(トートロジー)。これは、対応が双方向であるためです。タイプ/プログラムと定理/証明の世界の間の変換は機械的であり、多くの場合自動化できます。
Curry-Howardは、プログラムの仕様で何をしたいかをうまく果たしています。
Curry-Howardを理解していても、型システムの価値を無視するのは簡単な場合があります。
最初の点に関しては、おそらくIDEはJavaの型システムを操作するのに十分簡単にします(これは非常に主観的です)。
2番目の点に関しては、Javaはたまたま1次のロジックにほぼ対応しています。ジェネリックスは、普遍的な数量化と同等の型システムを使用します。残念ながら、ワイルドカードは実存的な定量化のほんの一部しか提供しません。しかし、普遍的な定量化はかなり良いスタートです。Aは完全に制約されていないので、関数List<A>
はすべての可能なリストに対して普遍的に機能すると言えます。これは、「パラメトリック性」に関してTwitterユーザーが話していることにつながります。
パラメトリック性についてよく引用される論文は、フィリップワドラーの定理です。。このホワイトペーパーの興味深い点は、型シグネチャのみから、非常に興味深い不変式を証明できることです。これらの不変条件の自動テストを作成する場合、時間を大幅に浪費することになります。たとえば、の場合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
引数なしのコンストラクターを持つ「安全な」型に関係し、どれが関係しないのかを推論することは不可能です。プログラムが普遍的に機能するという確信が失われました。
したがって、プログラムについて推論したい場合は、推論を強く脅かす言語機能を使用しないことを強くお勧めします。それをしたら、実行時に型を単にドロップしないのはなぜですか?それらは必要ありません。キャストが失敗しないか、呼び出し時にメソッドが欠落しているかもしれないという満足感で、効率とシンプルさを得ることができます。
消去すると推論が促進されます。
タイプは、コンパイラーがプログラムの正当性をチェックできるようにプログラムを作成するために使用される構造です。タイプは値の命題です-コンパイラはこの命題が真であることを検証します。
プログラムの実行中、型情報は必要ありません。これはコンパイラーによって既に確認されています。コンパイラーは、コードで最適化を実行するためにこの情報を自由に破棄する必要があります。コードをより高速に実行し、より小さなバイナリーを生成します。型パラメーターの消去により、これが容易になります。
Javaは、実行時に型情報(リフレクション、instanceofなど)を照会できるようにすることで、静的型付けを中断します。これにより、静的に検証できないプログラムを構築できます-プログラムは型システムをバイパスします。また、静的な最適化の機会を逃します。
型パラメーターが消去されるという事実は、これらの誤ったプログラムの一部のインスタンスが構築されるのを防ぎますが、型情報がさらに消去され、リフレクションおよびinstanceof機能が削除された場合、より多くの誤ったプログラムは許可されません。
消去は、データ型の「パラメトリック性」の特性を維持するために重要です。コンポーネントタイプTを介してパラメーター化された「リスト」タイプがあるとします。つまり、List <T>です。その型は、このリスト型がすべての型Tで同じように機能するという命題です。Tが抽象の無制限の型パラメーターであるという事実は、この型について何も知らないため、Tの特別な場合に特別なことは何もできないことを意味します。
たとえば、リストxs = asList( "3")があるとします。要素を追加します:xs.add( "q")。私は["3"、 "q"]で終わります。これはパラメトリックなので、List xs = asList(7);と想定できます。xs.add(8)は[7,8]で終わる
さらに、List.add関数はTの値を薄い空気から生み出すことができないことも知っています。asList( "3")に "7"が追加されている場合、可能な回答は値 "3"と "7"からのみ作成されることを知っています。関数がリストを作成できないため、リストに「2」または「z」が追加される可能性はありません。これらの他の値のどちらも追加するのが賢明ではなく、パラメトリック性により、これらの誤ったプログラムの構築が防止されます。
基本的に、消去は、パラメトリック性に違反するいくつかの手段を防ぎ、静的型付けの目的である誤ったプログラムの可能性を排除します。
(私はすでにここで回答を書いていますが、2年後にこの質問を再訪して、別の完全に異なる回答方法があることに気付いたので、以前の回答をそのまま残して、これを追加します。)
Java Genericsで行われたプロセスが「型消去」という名前に値するかどうかは、非常に議論の余地があります。ジェネリック型は消去されずに、対応する生の型に置き換えられるため、「型の切断」がより適切な選択のようです。
一般的に理解されている意味での型消去の典型的な機能は、アクセスするデータの構造に対して「ブラインド」にすることで、ランタイムを静的型システムの境界内に留めることです。これはコンパイラに全力を与え、静的型のみに基づいて定理を証明することを可能にします。また、コードの自由度を制限してプログラマーを支援し、単純な推論により多くの力を与えます。
Javaの型消去はそれを達成しません—次の例のように、コンパイラーを不自由にします:
void doStuff(List<Integer> collection) {
}
void doStuff(List<String> collection) // ERROR: a method cannot have
// overloads which only differ in type parameters
(上記の2つの宣言は、消去後に同じメソッドシグネチャにまとめられます。)
反対に、ランタイムはオブジェクトのタイプとその理由を検査できますが、真のタイプへの洞察は消去によって損なわれるため、静的タイプ違反は簡単に達成でき、防止するのは困難です。
さらに複雑にするために、元の型シグネチャと消去された型シグネチャは共存し、コンパイル時に並行して考慮されます。これは、プロセス全体がランタイムから型情報を削除することではなく、下位互換性を維持するためにジェネリック型システムをレガシーraw型システムにシューホーンすることに関するためです。この宝石は典型的な例です:
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
(冗長extends Object
性は、消去された署名の下位互換性を維持するために追加する必要がありました。)
それを念頭に置いて、引用をもう一度見てみましょう。
Javaユーザーが型の消去について文句を言うのはおかしい
Javaは正確に何を正しくしましたか?意味に関係なく、単語そのものですか?対照的に、控えめなint
型を見てください。実行時の型チェックは実行されず、実行も不可能であり、実行は常に完全に型保証されています。これが、型消去が正しく行われたときの様子です。そこにあるかどうかさえわかりません。
ここでまったく考慮されていないことの1つは、OOPの実行時のポリモーフィズムは、実行時の型の具体化に基本的に依存していることです。修正された型によってバックボーンが保持されている言語が、その型システムに主要な拡張機能を導入し、型消去に基づいている場合、認知的不協和は必然的な結果です。これがまさにJavaコミュニティに起こったことです。型消去が多くの論争を呼んでいるのはそのためであり、最終的にはJavaの将来のリリースで型消去を元に戻す計画があるのです。面白いものを見つけるJavaユーザーの不満では、Javaの精神に対する正直な誤解、または意識的に非難されている冗談を裏切っています。
「Javaが正しかったのは消去だけだ」という主張は、「ランタイム型の関数引数に対する動的ディスパッチに基づくすべての言語には根本的な欠陥がある」という主張を意味します。確かにそれ自体が正当な主張であり、Javaを含むすべてのOOP言語の正当な批評と見なすこともできますが、ランタイムのポリモーフィズムが行われるJavaのコンテキスト内で機能を評価および批判するための重要なポイントとしてそれ自体を提出することはできません。公理的です。
要約すると、「型消去は言語設計への道である」と正当に述べているかもしれませんが、Java内で型消去をサポートする立場は、それがあまりにも遅すぎて、歴史的な瞬間でさえすでにそうであったという理由で誤って配置されていますオークがSunに採用され、Javaに改名されたとき。
静的型付け自体がプログラミング言語の設計における適切な方向性であるかどうかに関しては、これは、プログラミングの活動を構成すると私たちが考えるもののはるかに広い哲学的コンテキストに適合します。数学の古典的な伝統から明確に派生した1つの思考の学校は、プログラムを1つの数学的概念または他の例(命題、関数など)のインスタンスと見なしますが、プログラミングを機械に話しかけ、機械に何が欲しいかを説明します。この見方では、プログラムは動的で有機的に成長するエンティティであり、静的に型付けされたプログラムの注意深く構築された建物の劇的な反対です。
動的言語をその方向への一歩であると見なすのは自然なことです。プログラムの一貫性はボトムアップで現れ、トップダウンでそれを課すアプリオリな制約はありません。このパラダイムは、私たち人間が開発と学習を通じて私たちが存在するようになるプロセスをモデル化するためのステップと見なすことができます。
同じ会話の同じユーザーによる後続の投稿:
new Tは壊れたプログラムです。「すべての命題は真実である」という主張に同型である。私はこれに大したことはありません。
(これは、アイデアが「それはいくつかの状況の新しいT '良いだろうに思える」、すなわちこと、他のユーザーによる声明に応じたnew T()
消去を入力することにより不可能である(これは議論の余地がある- 。場合でもT
で使用可能でした実行時、それは抽象クラスまたはインターフェースである可能性がありますVoid
。または、引数なしコンストラクタがないか、引数なしコンストラクタがプライベートである可能性があります(たとえば、シングルトンクラスであると想定されているため)、またはno-argコンストラクターは、総称メソッドがキャッチまたは指定しないチェック済み例外を指定できますが、それは前提です。消去せずに、少なくともT.class.newInstance()
これらの問題を処理するを記述できることは事実です。))
型は命題に同型であるというこの見解は、ユーザーが形式的な型理論の背景を持っていることを示唆しています。(S)彼は「動的型」や「実行時型」を好まない可能性が高く、ダウンキャストinstanceof
やリフレクションなどのないJavaを好みます。(Standard MLのような、非常に豊富な(静的)型システムを持ち、動的セマンティクスが型情報にまったく依存しない言語を考えてください。)
ところで、ユーザーがトローリングしていることは覚えておく価値があります。(静的に)タイプされた言語を心から好む可能性がありますが、そのビューの他の人を誠実に説得しようとはしていません。むしろ、元のツイートの主な目的は、反対する人をあざけることであり、反対する人の一部が口にした後、ユーザーは次のようなフォローアップのつぶやきを投稿しました。 Javaのユーザーとは異なり、彼らはやっています。」残念ながら、これは彼が実際に何を考えているかを見つけるのを難しくします。しかし幸いにも、そうすることはそれほど重要ではないことも意味します。自分の意見への実際の深さを持つ人々は、一般的に荒らしに頼らないでください、かなりこのコンテンツフリー。
良い点の1つは、ジェネリックが導入されたときにJVMを変更する必要がなかったことです。Javaは、ジェネリックをコンパイラレベルでのみ実装します。
タイプ消去が良いことである理由は、それが不可能にすることが有害であることです。実行時に型引数が検査されないようにすると、プログラムの理解と推論が容易になります。
直観に反していることがわかったのは、関数のシグネチャがより一般的であると、理解しやすくなるということです。これは、可能な実装の数が減るためです。このシグネチャを持つメソッドについて考えてみましょう。このメソッドには副作用がないことがわかっています。
public List<Integer> XXX(final List<Integer> l);
この関数の可能な実装は何ですか?非常に多くの。この関数が何をするかはほとんどわかりません。入力リストを逆にする可能性があります。intをペアにして合計し、サイズの半分のリストを返すこともできます。想像できる他の多くの可能性があります。今検討してください:
public <T> List<T> XXX(final List<T> l);
この関数の実装はいくつありますか?実装は要素のタイプを認識できないため、多数の実装を除外できます。要素を組み合わせたり、リストに追加したり、フィルターで除外したりすることはできません。アイデンティティ(リストへの変更なし)、要素の削除、またはリストの反転などの制限があります。この関数は、その署名だけに基づいて推論するのが簡単です。
Java以外では、常に型システムをだますことができます。このジェネリックメソッドの実装では、instanceof
チェックや任意の型へのキャストなどを使用できるため、型シグネチャに基づく推論は簡単に役に立たなくなる可能性があります。この関数は要素のタイプを検査し、その結果に基づいてさまざまなことを実行できます。これらのランタイムハックが許可されている場合、パラメーター化されたメソッドシグネチャは私たちにとってはるかに役に立たなくなります。
Javaが型の消去を行わなかった場合(つまり、型引数が実行時に具体化された場合)、これにより、この種の推論を損なう悪意がさらに増える可能性があります。上記の例では、リストに少なくとも1つの要素がある場合、実装は型シグネチャによって設定された期待に違反することができます。しかしT
具体化されていれば、リストが空であっても具体化できます。具象化された型は、コードの理解を妨げる可能性を(すでに非常に多く)増やすだけです。
型消去は、言語の「強力さ」を低下させます。しかし、「力」のいくつかの形態は実際には有害です。
instanceof
が型に基づいてコードが何をするかを推論する能力を妨げることです。Javaが型引数を具体化すると、この問題はさらに悪化します。実行時に型を消去すると、型システムがより便利になります。
これは直接的な回答ではありません(OPは「メリットは何ですか」と尋ねました。「デメリットは何ですか」と返信しています)
C#の型システムと比較すると、Javaの型消去は2つの理由で非常に困難です。
C#では、特に2つのタイプが共通の祖先を共有しない場合(つまり、それらの祖先がである場合)、両方IEnumerable<T1>
をIEnumerable<T2>
安全に実装できます。 Object
実際の例:Spring Frameworkでは、ApplicationListener<? extends ApplicationEvent>
複数回実装することはできません。T
テストに基づいて異なる動作が必要な場合instanceof
(それを行うにはクラスへの参照が必要です)
他の人がコメントしnew T()
たようにClass<T>
、のインスタンスを呼び出して、コンストラクターに必要なパラメーターを確認することによってのみ、同等のものを行うことはリフレクションを介してのみ行うことができます。C#では、パラメーターなしのコンストラクターに制約する場合にnew T()
のみ実行できますT
。T
その制約を尊重しない場合、コンパイルエラーが発生します。
Javaでは、多くの場合、次のようなメソッドを作成する必要があります。
public <T> T create(....params, Class<T> classOfT)
{
... whatever you do
... you will end up
T = classOfT.newInstance();
... or more advanced reflection
Constructor<T> parameterizedConstructorThatYouKnowAbout = classOfT.getConstructor(...,...);
}
上記のコードの欠点は次のとおりです。
ReflectiveOperationException
は、実行時にスローされます私がC#の作成者であれば、コンパイル時に検証しやすい1つ以上のコンストラクター制約を指定する機能を導入しました(たとえば、パラメーターを持つコンストラクターが必要になる場合がありますstring,string
)。でも最後は憶測です
その他のポイントは、他のどの回答も考慮していないようです:ランタイム型指定でジェネリックが本当に必要な場合は、次のように自分で実装できます。
public class GenericClass<T>
{
private Class<T> targetClass;
public GenericClass(Class<T> targetClass)
{
this.targetClass = targetClass;
}
このクラスは、Javaが消去を使用しなかった場合にデフォルトで達成できるすべてのことを実行できます。新しいT
s(T
使用する予定のパターンに一致するコンストラクターがあると想定)、またはT
sの配列を割り当てることができます。特定のオブジェクトがであるかどうかを実行時に動的にテストし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);
}
}
同じコードが複数の型に使用されるため、C ++のようなコードの膨張を回避します。ただし、型の消去には仮想ディスパッチが必要ですが、c ++-code-bloatアプローチでは仮想的にディスパッチされないジェネリックを実行できます
ほとんどの回答は、実際の技術的な詳細よりもプログラミングの哲学に関心があります。
この質問は5年以上前のものですが、疑問は残ります。なぜ、技術的な観点から型消去が望ましいのでしょうか。結局のところ、答えは(より高いレベルで)かなり単純です:https : //en.wikipedia.org/wiki/Type_erasure
C ++テンプレートは実行時に存在しません。コンパイラーは呼び出しごとに完全に最適化されたバージョンを発行します。つまり、実行は型情報に依存しません。しかし、JITは同じ関数の異なるバージョンをどのように扱いますか?一つの機能を持たせた方がいいのでは?JITがそのすべての異なるバージョンを最適化する必要があるとは思わないでしょう。では、タイプセーフについてはどうでしょうか。それは窓の外に出なければならないことを推測します。
しかし、少し待ってください:.NETはどのように実行しますか?反射!この方法では、1つの関数を最適化するだけで、実行時の型情報を取得できます。そして、それが.NETジェネリックが以前より遅くなった理由です(ただし、はるかに改善されています)。それが便利ではないと私は主張していません!しかし、それは高価であり、絶対に必要でない場合は使用しないでください(コンパイラ/インタープリタはとにかくリフレクションに依存しているため、動的型付き言語では高価とは見なされません)。
このように、型消去を使用した一般的なプログラミングはオーバーヘッドがゼロに近くなります(一部のランタイムチェック/キャストが引き続き必要です):https : //docs.oracle.com/javase/tutorial/java/generics/erasure.html