Map.get(Object key)が(完全に)汎用的でない理由は何ですか


405

のインターフェースに完全に汎用的なgetメソッドを持たないという決定の背後にある理由は何ですかjava.util.Map<K, V>

質問を明確にするために、メソッドの署名は

V get(Object key)

の代わりに

V get(K key)

そして、なぜだろうと思います(と同じですremove, containsKey, containsValue)。


3
コレクションについて同様の質問:stackoverflow.com/questions/104799/...
AlikElzin-kilaka


1
すごい。私は20年以上前からJavaを使用していますが、今日この問題に気付きました。
GhostCat 2018

回答:


260

他の人が述べたように、get()取得しているエントリのキーが、渡したオブジェクトと同じタイプである必要がないため、などが一般的ではない理由get()。メソッドの仕様は、それらが等しいことのみを要求します。これは、equals()メソッドがオブジェクトと同じ型ではなく、パラメーターとしてオブジェクトをどのように受け取るかによるものです。

多くのクラスがequals()そのオブジェクトがそれ自身のクラスのオブジェクトとのみ等しくなるように定義していることは一般的に真実かもしれませんが、Javaにはそうではない多くの場所があります。たとえば、の仕様でList.equals()は、2つのListオブジェクトは、両方がListであり、内容が同じであれば、たとえの実装が異なっていても、等しいと規定されていListます。この質問に戻っ例にくるので、この方法の仕様に応じて持つことが可能であるMap<ArrayList, Something>と私は、呼び出すためget()LinkedList、引数として、それは同じ内容のリストであるキーを取得する必要があります。get()ジェネリックで引数の型が制限されている場合、これは不可能です。


28
では、なぜV Get(K k)C#なのでしょうか。

134
問題は、を呼び出したいのであればm.get(linkedList)、なぜmの型を次のように定義しなかったのですMap<List,Something>か?インターフェイスを取得するm.get(HappensToBeEqual)ためにMapタイプを変更せずに呼び出すのが理にかなっているユースケースは考えられません。
Elazar Leibovich、2014

58
うわー、深刻な設計上の欠陥。コンパイラの警告も表示されません。私はエラザーに同意します。これが本当に便利で、頻繁に発生することは疑わしいのですが、getByEquals(Object key)のほうが合理的に聞こえます...
mmm

37
この決定は、実用性というよりは理論的な純粋性に基づいて行われたようです。大部分の使用法について、開発者は、newacctが回答で述べたようなエッジケースをサポートするために無制限にするのではなく、テンプレートタイプによって制限される議論を見るほうがはるかに好ましいでしょう。テンプレート化されていない署名を残すと、解決するよりも多くの問題が発生します。
Sam Goldberg

14
@newacct:「完全に型安全」は、実行時に予期せず失敗する可能性のある構成体に対する強力な主張です。これで動作するハッシュマップにビューを絞り込まないでください。TreeMap間違ったタイプのオブジェクトをメソッドに渡すと失敗する可能性getがありますが、マップがたまたま空である場合など、ときどき渡す可能性があります。さらに悪いことに、指定されComparatorcompareメソッドの場合(ジェネリックシグネチャを持つ!)は、チェックされていない警告なしに間違った型の引数で呼び出される可能性があります。これ壊れた動作です。
Holger、

105

GoogleのすばらしいJavaコーダーであるKevin Bourrillionは、この問題について少し前にブログ投稿で(確かにではSetなくのコンテキストでMap)書いています。最も関連性の高い文:

一様に、Java Collections Framework(およびGoogle Collections Libraryも)のメソッドは、コレクションが壊れないようにする必要がある場合を除いて、パラメーターのタイプを決して制限しません。

原則として同意するかどうかは完全にはわかりません。たとえば、.NETは適切なキーの種類を要求することで問題ないようですが、ブログ投稿の推論に従う価値はあります。(.NETについて言及したので、それが.NETで問題にならない理由の一部は、分散がより限定された.NETでより大きな問題があるということです...)


4
アポカリプス:それは真実ではありません、状況はまだ同じです。
Kevin Bourrillion、2009年

9
@ user102008いいえ、投稿は間違っていません。an Integerとa Doubleが互いに等しくなることは決してありませんが、a Set<? extends Number>に値が含まれているかどうかを尋ねるのは依然として公正な質問ですnew Integer(5)
Kevin Bourrillion、2012年

33
でメンバーシップを確認したいと思ったことは一度もありませんSet<? extends Foo>。私は非常に頻繁にマップのキータイプを変更しましたが、コードの更新が必要なすべての場所をコンパイラが見つけられなかったのでイライラしました。これが正しいトレードオフであると私は本当に確信していません。
Porculus 2012

4
@EarthEngine:常に壊れています。それがポイントです-コードは壊れていますが、コンパイラはそれをキャッチできません。
Jon Skeet

1
そして、それはまだ壊れており、バグを引き起こしました...素晴らしい答えです。
GhostCat 2018

28

契約はこのように表現されます:

より正式には、このマップにキーkから値vへのマッピング(key == null?k == null: key.equals(k))が含まれている場合、このメソッドはvを返します。それ以外の場合はnullを返します。(このようなマッピングは多くても1つしかありません。)

(私の強調)

したがって、キーの検索が成功するかどうかは、入力メソッドの等価メソッドの実装に依存します。これは必ずしも kのクラスに依存しているわけではありません。


4
それも依存していhashCode()ます。hashCode()を適切に実装しequals()ないと、この場合、適切に実装しても意味がありません。
rudolfson

5
equals()とhashCode()が正しく実装されている限り、キー全体を再作成することが実用的でない場合は、原則として、これによりキーに軽量プロキシを使用できると思います。
ビルミシェル

5
@rudolfson:私が知る限り、正しいバケットを見つけるためにハッシュコードに依存しているのはHashMapだけです。たとえば、TreeMapは二分探索木を使用し、hashCode()を気にしません。
Rob、

4
厳密に言えば、連絡先を満たすためget()に型の引数を取る必要はありませんObject。getメソッドがキータイプに制限されていると想像してくださいK-コントラクトはまだ有効です。もちろん、コンパイル時の型がのサブクラスではない場所での使用はコンパイルにK失敗しますが、コントラクトはコードをコンパイルするとどうなるかを暗黙的に議論するため、コントラクトが無効になることはありません。
BeeOnRope 2016年

16

それはポステルの法則の応用であり「あなたがすることを保守的にし、他人から受け入れることを寛大にしてください」です。

タイプに関係なく、等価性チェックを実行できます。equalsこの方法は、上に定義されObjectたクラスと、任意の受け入れObjectパラメータとして。したがって、キーの等価性、およびキーの等価性に基づく操作では、任意のObjectタイプを受け入れることが理にかなっています。

マップがキー値を返す場合、マップはtypeパラメータを使用して、できるだけ多くのタイプ情報を保存します。


4
では、なぜV Get(K k)C#なのでしょうか。

1
それV Get(K k)も理にかなっているので、C#にあります。Javaと.NETのアプローチの違いは、実際には一致しないものをブロックする人だけです。C#ではコンパイラ、Javaではコレクションです。I間での.NETの一貫性のないコレクションクラスに一度程度の怒りが、Get()そしてRemove()唯一確かに一致するタイプを受け入れることは、偶然に間違った値を渡すのを防ぐことができます。
Wormbo

26
これはポステルの法則の誤用です。他人から受け入れることは寛大であるが、あまり自由ではない。このばかげたAPIは、「コレクションに含まれていない」と「静的な入力ミスをした」との違いを見分けることができないことを意味します。get:K-> booleanを使用すれば、何千時間ものプログラマーの時間の損失を防ぐことができます。
メンタル裁判官

1
もちろんそうだったはずcontains : K -> booleanです。
メンタル裁判官


13

ジェネリックチュートリアルのこのセクションでは、状況を説明していると思います(私の強調):

「ジェネリックAPIが過度に制限されていないことを確認する必要があります。APIの元のコントラクトを引き続きサポートする必要があります。java.util.Collectionの例をもう一度検討してください。プレジェネリックAPIは次のようになります。

interface Collection { 
  public boolean containsAll(Collection c);
  ...
}

それを一般化する素朴な試みは:

interface Collection<E> { 
  public boolean containsAll(Collection<E> c);
  ...
}

これは確かにタイプセーフですが、APIの元の規約に準拠していません。 containsAll()メソッドは、あらゆる種類の受信コレクションで機能します。着信コレクションに実際にEのインスタンスのみが含まれている場合にのみ成功しますが、次のようになります。

  • 着信コレクションの静的タイプは異なる可能性があります。これは、おそらく呼び出し側が渡されるコレクションの正確なタイプを知らないためか、それがCollection <S>(SはEのサブタイプ)であるためです。
  • 別のタイプのコレクションでcontainsAll()を呼び出すことは完全に正当です。ルーチンは機能し、falseを返します。」

2
ではcontainsAll( Collection< ? extends E > c )、なぜですか?
メンタル裁判官、2013

1
@JudgeMental、ただし上記の例では示していませんcontainsAllが、Collection<S>where Sがのスーパータイプであることを許可する必要もありEます。もしそうなら、これは許可されませんcontainsAll( Collection< ? extends E > c )。また、としてれる明示的例に記載されている、それは(戻り値は、その後された状態で、異なるタイプのコレクションを渡す正当ですfalse)。
davmac

Eのスーパータイプのコレクションを持つcontainsAllを許可する必要はありません。バグを防ぐために、静的型チェックでその呼び出しを禁止する必要があると私は主張します。ばかげた契約ですが、それは元の質問のポイントです。
メンタル裁判官

6

その理由は、包含はメソッドによって決定されequalshashCodeどちらがメソッドでObjectあり、どちらもObjectパラメーターを取るためです。これは、Javaの標準ライブラリの初期の設計上の欠陥でした。Javaの型システムの制限と相まって、equalsとhashCodeに依存するものはすべて強制的に使用しますObject

Javaでタイプセーフなハッシュテーブルと平等を持っている唯一の方法は避けることですObject.equalsし、Object.hashCodeそして一般的な代替物を使用しています。機能Javaはまさにこの目的のために型クラスが付属していますHash<A>Equal<A>。用のラッパーHashMap<K, V>が提供されてHash<K>おりEqual<K>、そのコンストラクター内で使用されます。したがってget、このクラスとcontainsメソッドはtypeのジェネリック引数を取りKます。

例:

HashMap<String, Integer> h =
  new HashMap<String, Integer>(Equal.stringEqual, Hash.stringHash);

h.add("one", 1);

h.get("one"); // All good

h.get(Integer.valueOf(1)); // Compiler error

4
「オブジェクト」は常にKの祖先であり、「key.hashCode()」は引き続き有効であるため、これ自体は「get」のタイプが「V get(K key)」として宣言されることを妨げません。
finnw 2009

1
それはそれを妨げるものではありませんが、それはそれを説明すると思います。クラスの等価性を強制するためにequalsメソッドを切り替えた場合、それらのメソッドのメソッドプロトタイプに互換性がない場合、マップ内でオブジェクトを見つけるための基本的なメカニズムがequals()とhashmap()を使用することを人々に確実に伝えることができませんでした。
cgp

5

互換性。

ジェネリックが利用可能になる前は、get(Object o)しかありませんでした。

彼らがこのメソッドをget(<K> o)に変更した場合、作業コードを再度コンパイルするためだけに、Javaユーザーに大規模なコードメンテナンスを強制する可能性があります。

彼らは可能性が導入されている追加的な方法を、get_checked言う(<K> O)と穏やかな移行パスがあったので、古いのget()メソッドを非推奨。しかし、何らかの理由で、これは行われませんでした。(現在の状況では、findBugsなどのツールをインストールして、get()引数とマップの宣言されたキータイプ<K>のタイプの互換性を確認する必要があります。)

.equals()のセマンティクスに関連する引数は偽物だと思います。(厳密にはそれらは正しいですが、私はまだそれらは偽物だと思います。彼の正しい考えでは、o1とo2に共通のスーパークラスがない場合、o1.equals(o2)をtrueにするつもりはありません。)


4

もう1つ大きな理由があります。Mapが壊れているため、技術的には実行できません。

Javaには、のような多態的な汎用構造があり<? extends SomeClass>ます。このような参照をマークすると、で署名された型を指すことができ<AnySubclassOfSomeClass>ます。しかし、ポリモーフィックなジェネリックはその参照を読み取り専用にします。コンパイラでは、ジェネリック型をメソッドの戻り型(単純なゲッターなど)としてのみ使用できますが、ジェネリック型が引数であるメソッド(通常のセッターなど)の使用はブロックされます。つまり、を記述したMap<? extends KeyType, ValueType>場合、コンパイラーはmethodを呼び出すget(<? extends KeyType>)ことができず、マップは役に立たなくなります。唯一の解決策は、このメソッドをジェネリックにしないことですget(Object)


なぜsetメソッドは強く型付けされているのですか?
センテンザ2014年

「put」を意味する場合:put()メソッドはマップを変更し、<?などのジェネリックでは使用できません。SomeClass>を拡張します。これを呼び出すと、コンパイル例外が発生します。このようなマップは「読み取り専用」になります
Owheee 2014年

1

下位互換性があると思います。Map(またはHashMap)まだサポートする必要がありますget(Object)


13
しかし、同じ議論をすることができますput(これはジェネリック型を制限します)。raw型を使用すると、下位互換性が得られます。ジェネリックは「オプトイン」です。
Thilo、2011

個人的には、この設計決定の最も可能性の高い理由は、下位互換性です。
geekdenz 2016年

1

私はこれを見て、なぜ彼らがこのようにそれをしたのかを考えていました。既存の回答のどれもが、新しいジェネリックインターフェイスに適切なタイプのキーのみを受け入れさせられなかった理由を説明しているとは思いません。実際の理由は、ジェネリックを導入しても、新しいインターフェイスを作成しなかったためです。Mapインターフェースは古いジェネリックでないMapと同じで、ジェネリックバージョンと非ジェネリックバージョンの両方として機能します。このようにして、非ジェネリックMapを受け入れるメソッドがある場合Map<String, Customer>、それを渡すことができ、それでも機能します。同時に、getのコントラクトはObjectを受け入れるため、新しいインターフェイスもこのコントラクトをサポートする必要があります。

私の意見では、新しいインターフェイスを追加して既存のコレクションに両方を実装する必要がありましたが、たとえgetメソッドの設計が悪いことを意味するとしても、互換性のあるインターフェイスを支持することにしました。コレクション自体は既存のメソッドと互換性があることに注意してください。インターフェイスだけは互換性がありません。


0

今、大きなリファクタリングを行っていますが、この強く型付けされたget()が欠落していたため、古い型のget()が欠落していないことを確認しました。

しかし、コンパイル時間チェックの回避策/醜いトリックを見つけました:強く型付けされたget、containsKey、removeを使用してMapインターフェースを作成し、プロジェクトのjava.utilパッケージに配置します。

get()を呼び出すだけでコンパイルエラーが発生します...間違った型を使用すると、他のすべてのものはコンパイラーに問題がないようです(少なくともeclipse kepler内)。

これはランタイムで必要なものではないため、ビルドのチェック後にこのインターフェースを削除することを忘れないでください。

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