Javaのジェネリックの実装が悪いと主張する人がいるのはなぜですか?


115

ジェネリックではJavaが正しく機能しないと聞いたことがあります。(最近の参照、ここ

私の経験不足を許してください、しかし何が彼らをより良くしたでしょうか?


23
これを閉じる理由はありません。ジェネリックについてのすべてのがらくたがあるので、JavaとC#の違いをいくらか明確にしておくと、悪意のある泥沼化を回避しようとする人々に役立つ可能性があります。
Rob

回答:


146

悪い:

  • 型情報はコンパイル時に失われるため、実行時には、それがどの「型」であるのかがわかりません
  • 値タイプには使用できません(これは非常に重要です。.NETでは、たとえばList<byte>実際にに支えられてbyte[]おり、ボクシングは必要ありません)。
  • ジェネリックメソッドを呼び出すための構文(IMO)
  • 制約の構文は混乱する可能性があります
  • ワイルドカードは一般的に混乱します
  • 上記によるさまざまな制限-キャストなど

良い:

  • ワイルドカードを使用すると、呼び出し側で共分散/反変を指定できます。これは、多くの状況で非常に便利です
  • 何もないよりはましだ!

51
@Paul:私は実際には一時的な破損を好んでいたが、その後はまともなAPIを好んだと思います。または、.NETルートを使用して、新しいコレクションタイプを紹介します。(2.0の.NETジェネリックスは1.1アプリを壊しませんでした。)
Jon Skeet

6
既存のコードベースが大きいので、1つのメソッドのシグネチャを変更して、それを呼び出すすべての人を変更せずにジェネリックスを使用できるようになりました。
ポールトンブリン

38
しかし、そのマイナス面は莫大で永続的なものです。短期間の大きな勝利と長期的な損失のIMOがあります。
ジョンスキート、

11
ジョン-「何もないよりはまし」は主観的です:)
MetroidFan2002 2009年

6
@ロジェリオ:最初の「悪い」アイテムが正しくないと主張されました。私の最初の「悪い」項目は、コンパイル時に情報が失われることを示しています。それが正しくないためには、コンパイル時に情報失われないことを言っている必要があります...他の開発者を混乱させることに関しては、私が言っていることに混乱したのはあなただけです。
Jon Skeet、

26

最大の問題は、Javaジェネリックはコンパイル時のみのものであり、実行時にそれを覆すことができるということです。C#はより多くのランタイムチェックを行うため、賞賛されています。この投稿には本当に良い議論がいくつかあり、他の議論にリンクしています。


それは本当に問題ではありません、それはランタイムのために作られたことはありません。まるで山に登れないのでボートは問題だと言っているようです。言語として、Javaは多くの人々が必要とすることを行います:意味のある方法で型を表現します。実行時タイプの施行の場合でも、人々はClassオブジェクトを渡すことができます。
Vincent Cantin 2016年

1
彼は正しい。ボートを設計する人々は山を登るように設計する必要があるので、あなたのアノロジーは間違っています。彼らの設計目標は、何が必要かについて十分に考え出されていませんでした。今、私たちはすべてボートだけで行き詰まり、山に登ることはできません。
WiegleyJ

1
@VincentCantin-それは間違いなく問題です。したがって、私たちはそれについて不平を言っています。Javaジェネリックは中途半端です。
Josh M.

17

主な問題は、Javaには実際には実行時にジェネリックスがないことです。これはコンパイル時の機能です。

Javaでジェネリッククラスを作成すると、 "Type Erasure"と呼ばれるメソッドを使用して、クラスからすべてのジェネリック型を実際に削除し、基本的にそれらをObjectで置き換えます。ジェネリックの上位バージョンは、コンパイラーがメソッド本体に現れるたびに、指定されたジェネリック型にキャストを挿入するだけです。

これにはマイナス面がたくさんあります。最大の1つであるIMHOは、リフレクションを使用してジェネリック型を検査できないことです。型は実際にはバイトコードでジェネリックではないため、ジェネリックとして検査することはできません。

ここでの違いの概要:http : //www.jprl.com/Blog/archive/development/2007/Aug-31.html


2
なぜあなたが反対票を投じられたのかはわかりません。あなたが正しいと私が理解していることから、そのリンクは非常に有益です。賛成票。
ジェイソンジャクソン

@ジェイソン、ありがとう。元のバージョンにはタイプミスがありました。
JaredPar 2009

3
エラー、リフレクションを使用してジェネリック型、ジェネリックメソッドシグネチャを確認できるため。リフレクションを使用して、パラメーター化されたインスタンスに関連付けられている一般的な情報を検査することはできません。たとえば、メソッドfoo(List <String>)を持つクラスがある場合、反射的に "String"を見つけることができます
oxbow_lakes

型の消去は実際の型パラメーターを見つけることが不可能になると言う記事はたくさんあります。言いましょう:Object x = new X [Y](); xを指定して、タイプYを見つける方法を示すことができますか?
user48956 2013

@oxbow_lakes-ジェネリック型を見つけるためにリフレクションを使用する必要があるのは、それができないのと同じくらい悪いことです。例えばそれは悪い解決策です。
Josh M.

14
  1. 実行時の実装(つまり、型消去ではありません);
  2. プリミティブ型を使用する機能(これは(1)に関連しています)。
  3. ワイルドカードは有用ですが、構文といつ使用するかを知ることは、多くの人々を困惑させるものです。そして
  4. パフォーマンスは向上しません((1)のため、Javaジェネリックはcastiオブジェクトの構文上の砂糖です)

(1)非常に奇妙な動作につながります。私が考えることができる最良の例は、です。想定:

public class MyClass<T> {
  T getStuff() { ... }
  List<String> getOtherStuff() { ... }
}

次に、2つの変数を宣言します。

MyClass<T> m1 = ...
MyClass m2 = ...

今すぐ電話getOtherStuff()

List<String> list1 = m1.getOtherStuff(); 
List<String> list2 = m2.getOtherStuff(); 

2番目のパラメーターは、パラメーター化された型とは関係ありませんが、生の型(パラメーター化された型が提供されないことを意味する)であるため、コンパイラーによって汎用型引数が取り除かれています。

JDKからの私のお気に入りの宣言についても触れます。

public class Enum<T extends Enum<T>>

ワイルドカード(混合バッグ)以外は、.Netジェネリックの方が良いと思います。


しかし、コンパイラーは、特にrawtypes lintオプションを使用して通知します。
トム・ホーティン-タックライン09

申し訳ありませんが、なぜ最後の行はコンパイルエラーですか?私はそれをEclipseでいじっていて、そこで失敗することはありません-残りをコンパイルするのに十分なものを追加した場合。
oconnor0 2010

public class Redundancy<R extends Redundancy<R>> ;)
java.is.for.desktop 2010

@ oconnor0それは明確な理由がないのに、コンパイルの失敗が、コンパイラの警告、ではありません:The expression of type List needs unchecked conversion to conform to List<String>
artbristol

Enum<T extends Enum<T>>最初は奇妙/冗長に見えるかもしれませんが、実際にはかなり面白く、少なくともJava /それのジェネリックの制約内では。values()列挙Enum型には、notとして型付けされた要素の配列を与える静的メソッドがあり、その型はジェネリックパラメーターによって決定されますEnum<T>。つまり、もちろん、その型付けは列挙型のコンテキストでのみ意味があり、すべての列挙型はのサブクラスなEnumので、が必要ですEnum<T extends Enum>。ただし、Javaは生の型とジェネリックを混在させることを好みません。Enum<T extends Enum<T>>、一貫性がれます。
JAB

10

私は本当に物議を醸す意見を捨てるつもりです。ジェネリックスは言語を複雑にし、コードを複雑にします。たとえば、文字列を文字列のリストにマップするマップがあるとします。昔は、これを次のように簡単に宣言できました。

Map someMap;

今、私はそれを次のように宣言する必要があります

Map<String, List<String>> someMap;

そして、それをいくつかのメソッドに渡すたびに、その長い長い宣言をもう一度繰り返す必要があります。私の意見では、余分なタイピングはすべて開発者の注意をそらし、彼を「ゾーン」から外します。また、コードが多くのクリフトで満たされた場合、後で戻って重要なロジックを見つけるためにすべてのクリフトをすばやくふるいにかけることが難しい場合があります。

Javaは、一般的に使用されている最も冗長な言語の1つであるという評判はすでに悪く、ジェネリックはその問題に追加されます。

そして、あなたはそのすべての余分な冗長性のために本当に何を買いますか?誰かが文字列を保持するはずのコレクションに整数を入れたとき、または誰かが整数のコレクションから文字列をプルしようとしたときに、実際に何回問題が発生しましたか?商用Javaアプリケーションの構築に携わった10年の経験の中で、これが大きなエラーの原因になることは決してありません。だから、私はあなたが余分な冗長性のために何を手に入れているのか本当にわかりません。それは本当に官僚的な手荷物として本当に私を襲います。

今、私は本当に物議を醸すつもりです。Java 1.4のコレクションの最大の問題は、どこでも型キャストする必要があることです。私はそれらの型キャストをジェネリックと同じ問題の多くを持っている余分で冗長な残骸だと考えています。だから、例えば、私はただすることはできません

List someList = someMap.get("some key");

私がしなければなりません

List someList = (List) someMap.get("some key");

もちろん、その理由は、get()がListのスーパータイプであるObjectを返すためです。したがって、型キャストなしでは割り当てを行うことはできません。繰り返しますが、そのルールが実際にどれだけあなたを買うかについて考えてください。私の経験から、それほどではありません。

Javaは、1)ジェネリックを追加していなかったが、2)スーパータイプからサブタイプへの暗黙的なキャストを許可していれば、はるかに優れていたと思います。実行時に誤ったキャストをキャッチします。それから私は定義するのが簡単だったかもしれない

Map someMap;

そして後で

List someList = someMap.get("some key");

すべての残骸がなくなり、コードにバグの大きな新しいソースを導入することは本当にないと思います。


15
申し訳ありませんが、あなたが言ったことのほとんどすべてに同意しません。しかし、あなたがそれをよく論じているので、私はそれを反対票を投じません。
ポールトムブリン

2
もちろん、PythonやRubyなどの組み込み言語で構築された大規模で成功したシステムの例はたくさんあります。これらのシステムは、私の回答で提案したとおりのことを行います。
クリントミラー

このタイプのアイデアに対する私の全体的な反対は、それ自体が悪いアイデアではなく、PythonやRubyなどの現代の言語が開発者の生活をより簡単に、より簡単にするので、開発者は知的に満足し、最終的には低くなると思います自分のコードの理解度。
Dan Tao

4
「そして、その余分な冗長性のために本当に何を購入しますか?...」これは、貧弱な保守プログラマーに、これらのコレクションに何が含まれるべきかを伝えます。これが商用Javaアプリケーションでの問題であることがわかりました。
richj 2010

4
「誰かが[y]を保持するはずのコレクションに[x]を入れるという問題に実際に何回遭遇しましたか?」-ああ、私は数を失った!また、バグがなくても、読みやすさのキラーです。そして、多くのファイルをスキャンして、オブジェクトがどうなるか(またはデバッグ)を見つけると、本当にゾーンから外れます。あなたはHaskellを好むかもしれません-強い型付けでさえ、粗雑さが少ないです(型が推論されるため)。
マーティンカポディッチ

8

ランタイムではなくコンパイル時であることのもう1つの副作用は、ジェネリック型のコンストラクターを呼び出せないことです。したがって、これらを使用して汎用ファクトリを実装することはできません...


   public class MyClass {
     public T getStuff() {
       return new T();
     }
    }

--jeffk ++


6

Javaジェネリックは、コンパイル時に正しいかどうかがチェックされ、すべての型情報が削除されます(このプロセスは型消去と呼ばれます。したがって、ジェネリックList<Integer>は、任意のクラスのオブジェクトを含むことができるraw型 non-generic Listに削減されます。

これにより、実行時に任意のオブジェクトをリストに挿入できるようになるだけでなく、ジェネリックパラメーターとして使用されている型を判別できなくなります。後者は次に

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if(li.getClass() == lf.getClass()) // evaluates to true
  System.out.println("Equal");

5

これがwikiだったらいいので、他の人に追加したいと思いますが...

問題:

  • 型消去(実行時の可用性なし)
  • プリミティブ型はサポートされていません
  • アノテーションとの非互換性(両方とも1.5で追加されましたが、なぜアノテーションが機能の急ぎを除いてジェネリックを許可しないのかはまだわかりません)
  • 配列との非互換性。(時々私は本当にClassのような何かをしたい<ですか?MyObject >[]を拡張しますが、私は許可されていません)
  • Wierdワイルドカードの構文と動作
  • ジェネリックサポートがJavaクラス間で一貫していないという事実。彼らはそれをほとんどのコレクションメソッドに追加しましたが、たまに、そこにないインスタンスに出くわします。

5

タイプ全体の消去の混乱を無視すると、指定されたジェネリックは機能しません。

これはコンパイルします:

List<Integer> x = Collections.emptyList();

しかし、これは構文エラーです:

foo(Collections.emptyList());

fooは次のように定義されています。

void foo(List<Integer> x) { /* method body not important */ }

したがって、式の型チェックがローカル変数に割り当てられているのか、メソッド呼び出しの実際のパラメーターに割り当てられているのかによって、式のタイプがチェックされるかどうかが決まります。クレイジーなの?


3
javacによる一貫性のない推論はがらくたです。
mP。

3
後者の形式が拒否される理由は、メソッドのオーバーロードにより "foo"の複数のバージョンが存在する可能性があるためだと思います。
Jason Creighton、

2
Java 8が改善されたターゲット型推論を導入したため、この批評はもはや適用されません
oldrinb '29

4

アーキテクトは機能性、使いやすさ、およびレガシーコードとの下位互換性のバランスをとろうとしていたため、Javaへのジェネリックの導入は困難な作業でした。かなり予想通り、妥協が必要でした。

Javaのジェネリックの実装が言語の複雑さを許容できないレベルに高めたと感じる人もいます(Ken Arnoldの「ジェネリックは有害と見なされる」を参照)。Angelika LangerのGenerics FAQsは、物事がいかに複雑になるかについてかなり良い考えを与えています。


3

Javaは、実行時にGenericsを適用せず、コンパイル時にのみ適用します。

これは、間違った型をジェネリックコレクションに追加するなど、興味深いことができることを意味します。


1
あなたがそれを管理するならば、コンパイラはあなたが台無しにしたとあなたに言うでしょう。
トム・ホーティン-タックライン09

@トム-必ずしもそうではありません。コンパイラをだます方法があります。
ポールトンブリン

2
コンパイラの指示に耳を傾けませんか?(私の最初のコメントを参照)
トム・ホーティン-タックライン

1
@ Tom、ArrayList <String>があり、それを単純なリストを受け取る古いスタイルのメソッド(たとえば、レガシーまたはサードパーティのコード)に渡す場合、そのメソッドは好きなものをそのリストに追加できます。実行時に強制されると、例外が発生します。
ポールトンブリン

1
static void addToList(List list){list.add(1); } List <String> list = new ArrayList <String>(); addToList(list); これはコンパイルされ、実行時にも機能します。問題が発生するのは、文字列が必要なリストから削除してintを取得するときだけです。
Laplie Anderson、

3

Javaジェネリックはコンパイル時のみであり、非ジェネリックコードにコンパイルされます。C#では、実際にコンパイルされたMSILは汎用です。Javaはまだ実行時にキャストするため、これはパフォーマンスに大きな影響を与えます。詳細はこちらをご覧ください


2

Java Posse#279-Joe DarcyとAlex Buckleyへのインタビューを聞くと、彼らはこの問題について話します。これは、Reified Generics for JavaというタイトルのNeal Gafterブログ投稿にもリンクしています。

多くの人々は、ジェネリックがJavaで実装される方法によって引き起こされる制限に満足していません。具体的には、ジェネリック型パラメーターが具体化されていないことに不満があります。実行時には使用できません。ジェネリックは消去を使用して実装され、ジェネリック型パラメーターは実行時に単純に削除されます。

そのブログの投稿は、要件の移行の互換性に関する要点を強調した、古いエントリである消しゴムによるパズル:回答セクションを参照しています。

目標は、ソースコードとオブジェクトコードの両方の下位互換性、および移行の互換性を提供することでした。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.