Java.util.Map<K, V>
のインターフェースに完全に汎用のgetメソッドを持たないという決定の背後にある理由は何ですか。
質問を明確にするために、メソッドの署名は
V get(Object key)
の代わりに
V get(K key)
そして、なぜだろうか(remove, containsKey, containsValue
と同じこと)。
他の人が言ったように、get()
などが一般的ではない理由は、取得するエントリのキーがget()
に渡すオブジェクトと同じタイプである必要がないためです。メソッドの仕様は、それらが等しいことのみを必要とします。これは、equals()
メソッドがオブジェクトと同じ型ではなく、パラメーターとしてオブジェクトをどのように受け取るかによって決まります。
多くのクラスではequals()
が定義されているため、そのオブジェクトは自身のクラスのオブジェクトとのみ等しくなることが一般的ですが、Javaにはそうではない場所がたくさんあります。たとえば、List.equals()
の仕様では、2つのListオブジェクトが両方ともListであり、List
の異なる実装であっても同じ内容である場合、等しいとされています。したがって、この質問の例に戻ると、メソッドの仕様によれば、Map<ArrayList, Something>
を持つことができ、LinkedList
を引数としてget()
を呼び出すことができます。同じ内容のリストであるキーを取得します。 get()
がジェネリックであり、引数の型が制限されている場合、これは不可能です。
GoogleのすばらしいJavaコーダーであるKevin Bourrillionは、まさにこの問題について ブログ投稿 少し前に(確かにSet
ではなくMap
のコンテキストで)書いています。最も関連性の高い文:
一様に、Java Collections Framework(およびGoogle Collections Libraryも)のメソッドは、コレクションが破損しないようにする必要がある場合を除き、パラメーターのタイプを制限しません。
原則としてこれに同意するかどうかは定かではありません。たとえば、.NETは適切なキータイプを必要とするように思えますが、ブログ投稿の理由に従う価値はあります。 (.NETについて述べたが、.NETで問題にならない理由の一部は、.NETでbigger問題があることを説明する価値があるより限定された分散...)
契約はこうして表されます:
より正式には、このマップに(key == null?k == null:key.equals(k))のようなキーkから値vへのマッピングが含まれている場合、このメソッドはvを返します;それ以外の場合は、nullを返します。 (このようなマッピングは最大で1つしかありません。)
(私の強調)
そのため、キー検索が成功するかどうかは、入力キーの等式メソッドの実装に依存します。 kのクラスに依存する必要はありません。
それは ポステルの法則 「あなたがすることを保守的にし、他人から受け入れることについては寛大であること」の応用です。
タイプに関係なく、等価チェックを実行できます。 equals
メソッドはObject
クラスで定義され、Object
をパラメーターとして受け入れます。したがって、Object
タイプを受け入れることは、キーの等価性、およびキーの等価性に基づく操作にとって意味があります。
マップがキー値を返す場合、typeパラメーターを使用することにより、可能な限り多くのタイプ情報を保存します。
Genericsチュートリアルのこのセクションで状況を説明していると思います(私の強調):
「ジェネリックAPIが過度に制限されていないことを確認する必要があります。APIの元のコントラクトを引き続きサポートする必要があります。Java.util.Collectionのいくつかの例を再度検討してください。
interface Collection {
public boolean containsAll(Collection c);
...
}
それを生成する素朴な試みは次のとおりです。
interface Collection<E> {
public boolean containsAll(Collection<E> c);
...
}
これは確かにタイプセーフですが、APIの元の契約に従っていません。 containsAll()メソッドは、あらゆる種類の着信コレクションで機能します。着信コレクションに実際にEのインスタンスのみが含まれている場合にのみ成功しますが、次のようになります。
理由は、包含がequals
のメソッドであるhashCode
およびObject
によって決定され、両方がObject
パラメーターをとるからです。これはJavaの標準ライブラリの初期の設計上の欠陥でした。 Javaの型システムの制限と相まって、equalsとhashCodeに依存するものはすべて強制的にObject
を使用します。
タイプセーフなハッシュテーブルとJavaの同等性を持つ唯一の方法は、Object.equals
とObject.hashCode
を避けて、一般的な代替を使用することです。 Functional Java には、この目的のための型クラスが付属しています: Hash<A>
および Equal<A>
。 HashMap<K, V>
のラッパーが提供され、コンストラクターでHash<K>
とEqual<K>
を取ります。したがって、このクラスのget
およびcontains
メソッドは、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
もう1つの重要な理由があります。それは、Mapを壊すため、技術的には行えません。
Javaには、<? extends SomeClass>
のような多態的な汎用構造があります。そのような参照をマークすると、<AnySubclassOfSomeClass>
で署名された型を指すことができます。しかし、多相ジェネリックはその参照をreadonlyにします。コンパイラでは、メソッドの戻り型(単純なゲッターなど)としてのみジェネリック型を使用できますが、ジェネリック型が引数であるメソッド(通常のセッターなど)の使用をブロックします。つまり、Map<? extends KeyType, ValueType>
と記述すると、コンパイラはget(<? extends KeyType>)
メソッドの呼び出しを許可せず、マップは役に立たなくなります。唯一の解決策は、このメソッドをジェネリックではないようにすることです:get(Object)
。
適合。
ジェネリックが利用可能になる前は、get(Object o)だけがありました。
彼らがこのメソッドをget(<K> o)に変更した場合、作業コードを再コンパイルするためだけにJavaユーザーに大規模なコード保守を強制する可能性がありました。
could追加メソッドを導入しました。たとえば、get_checked(<K> o)を使用し、古いget()メソッドを廃止して、移行パスを緩やかにしました。しかし、何らかの理由で、これは行われませんでした。 (現在の状況では、findBugsなどのツールをインストールして、マップのget()引数と宣言されたキータイプ<K>の間のタイプの互換性を確認する必要があります。)
.equals()のセマンティクスに関する引数は偽物だと思います。 (技術的には正しいですが、それでも偽物だと思います。o1とo2に共通のスーパークラスがない場合、o1.equals(o2)をtrueにするデザイナーはいません。)
私はこれを見て、なぜ彼らがこのようにしたのか考えていました。既存の回答のいずれも、新しい汎用インターフェイスにキーの適切なタイプのみを受け入れさせることができない理由を説明するとは思わない。実際の理由は、ジェネリックを導入しても新しいインターフェースを作成しなかったからです。 Mapインターフェースは、一般的なバージョンと非汎用バージョンの両方として機能するのと同じ古い非汎用マップです。このように、非ジェネリックマップを受け入れるメソッドがある場合、Map<String, Customer>
を渡すことができ、それでも機能します。同時に、getのコントラクトはObjectを受け入れるため、新しいインターフェイスもこのコントラクトをサポートする必要があります。
私の意見では、新しいインターフェイスを追加し、既存のコレクションの両方に実装する必要がありましたが、getメソッドの設計が悪いことを意味する場合でも、互換性のあるインターフェイスを優先して決定しました。コレクション自体は既存のメソッドと互換性がありますが、インターフェイスは互換性がないことに注意してください。
下位互換性があると思います。 Map
(またはHashMap
)は引き続きget(Object)
をサポートする必要があります。
現在、大きなリファクタリングを行っていますが、この強く型付けされたget()が欠落していたため、古い型でget()が欠落していなかったことを確認しました。
しかし、コンパイル時間のチェックのための回避策/ ugいトリックを見つけました:強く型付けされたget、containsKey、remove ...でMapインターフェイスを作成し、それをプロジェクトのJava.utilパッケージに入れます。
Get()を呼び出すだけでコンパイルエラーが発生します...間違った型を使用すると、他のものはすべてコンパイラー(少なくともEclipse Kepler内)で問題ありません。
ビルドの確認後にこのインターフェイスを削除することを忘れないでください。これは実行時に必要なものではありません。