Java:なぜコレクションはComparatorを受け入れますが、(仮想的な)HasherとEquatorを受け入れませんか?


25

この問題は、インターフェイスのさまざまな実装があり、特定のコレクションの目的のために、オブジェクトのインターフェイスレベルのビューのみに関心がある場合に最も顕著です。たとえば、次のようなインターフェイスがあるとします。

public interface Person {
    int getId();
}

クラスを実装hashcode()および実装する通常の方法equals()では、equalsメソッドに次のようなコードが含まれます。

if (getClass() != other.getClass()) {
    return false;
}

あなたがの実装混ぜるときに問題が発生Person中をHashMap。がHashMapのインターフェイスレベルのビューのみに関心がある場合、Person実装クラスのみが異なる重複が発生する可能性があります。

equals()すべての実装で同じリベラルな方法を使用してこのケースを機能させることはできますが、その後equals()、異なるコンテキストで間違ったことを行うリスクが発生します(Personデータベースレコードに裏付けられた2つのsをバージョン番号と比較するなど)。

私の直感では、平等はクラスごとではなくコレクションごとに定義する必要があると教えられています。順序に依存するコレクションを使用する場合、カスタムComparatorを使用して各コンテキストで正しい順序を選択できます。ハッシュベースのコレクションに類似するものはありません。どうしてこれなの?

明確にするために、この質問は、コレクションの実装を処理するため、「。equals()がJavaのクラスにあるのに、なぜ.compareTo()がインターフェイスにあるのかとは異なりますcompareTo()およびequals()/ hashcode()両方ともコレクションを使用する際の普遍性の問題に苦しんでいます:コレクションごとに異なる比較関数を選ぶことはできません。したがって、この質問の目的上、オブジェクトの継承階層はまったく問題ではありません。重要なのは、比較関数がオブジェクトごとに定義されているか、コレクションごとに定義されているかです。


5
以下のためにあなたはいつもラッパーオブジェクトを導入することができますPersonことが期待される実装equalshashCode行動を。その後、あなたはHashMap<PersonWrapper, V>。これは、純粋なOOPアプローチが洗練されていない1つの例です。オブジェクトに対するすべての操作がそのオブジェクトのメソッドとして意味をなすわけではありません。JavaのObjectタイプ全体は、さまざまな責任の融合です。今日のベストプラクティスではgetClassfinalizetoStringメソッドのみがリモートで正当化されるようです。
アモン

1
1)C#ではIEqualityComparer<T>、ハッシュベースのコレクションにを渡すことができます。あなたは1を指定しない場合、それはに基づいてデフォルトの実装を使用Object.EqualsしてObject.GetHashCode()。2)Equals可変参照型でのIMOオーバーライドは、めったに良いアイデアではありません。このように、デフォルトの等式はかなり厳密ですが、カスタムを介して必要な場合は、より緩和された等式ルールを使用できますIEqualityComparer<T>
CodesInChaos

回答:


23

この設計は「普遍的平等」としても知られています。2つのものが等しいかどうかは普遍的な財産であるという信念です。

さらに、平等は2つのオブジェクトのプロパティですが、オブジェクト指向では、常に1つのオブジェクトでメソッドを呼び出し、そのオブジェクトはそのメソッド呼び出しの処理方法を単独で決定します。そのため、Javaのようなデザインでは、同等性は比較される2つのオブジェクトのいずれかのプロパティであるため、対称性(a == bb == a)などの同等性の基本的なプロパティを保証することさえできません。が呼び出されaており、2番目のケースではが呼び出されてbおり、OOの基本原則により、a(最初のケースでは)の決定のみです。bの判断(2番目の場合)は、それ自体が他の判断と等しいかどうかを判断します。対称性を得る唯一の方法は、2つのオブジェクトを連携させることですが、そうでない場合は...幸運です。

1つの解決策は、1つのオブジェクトのプロパティではなく、2つのオブジェクトのプロパティ、または3番目のオブジェクトのプロパティのいずれかを同等にすることです。後者のオプションは、3番目の「コンテキスト」オブジェクトのプロパティを同等にすると、EqualityComparer異なるコンテキストに異なるオブジェクトがあることを想像できるため、普遍的な同等性の問題も解決します。

これ、たとえばEqtypeclass を使用してHaskellに選択された設計です。また、サードパーティのScalaライブラリ(ScalaZなど)によって選択された設計ですが、基盤となるホストプラットフォームとの互換性のために普遍的な同等性を使用するScalaコアまたは標準ライブラリではありません。

おもしろいことに、これはJava Comparable/ Comparatorインターフェースで選択された設計でもあります。Javaの設計者は明らかに問題を認識していましたが、何らかの理由で順序付けのためにのみ解決しましたが、平等(またはハッシュ)では解決しませんでした。

だから、質問に関して

なぜそこにあるComparatorインターフェースが、ノーHasherとはEquator

答えは「わからない」です。明らかに、Javaの設計者は、の存在からComparator明らかなように、問題を認識していましたが、明らかに、平等とハッシュの問題とは考えていませんでした。他の言語とライブラリは異なる選択をします。


7
+1。ただし、複数のディスパッチが存在するオブジェクト指向言語があることに注意してください(Smalltalk、Common Lisp)。したがって、次の文では常に強すぎます。「OOでは、常に1つのオブジェクトのメソッドを呼び出す」。
コアダンプ

探していた見積もりを見つけました。JLS 1.0によるとThe methods equals and hashCode are declared for the benefit of hashtables such as java.util.Hashtable、つまり、両方equalshashCodeは、単にObject Java開発者によってメソッドとして導入されただけですHashtable-仕様のどこにもUEまたはsilimarの概念はありません。そうでない場合はHashtableequalsおそらくのようなインターフェイスになっていたでしょうComparable。そのため、以前はあなたの答えが正しいと信じていましたが、今ではそれが実証されていないと考えています。
vaxquis

@JörgWMittagそれはタイプミス、IFTFYでした。ところで、clone-それは元々はメソッドではなく演算子でした(Oak言語仕様を参照)、引用:-3 The unary operator clone is applied to an object. (...) The clone operator is normally used inside new to clone the prototype of some class, before applying the initializers (constructors)つのキーワードのような演算子はinstanceof new clone(セクション8.1、演算子)でした。私はそれがclone/ Cloneable混乱の本当の(歴史的な)理由だと思います- Cloneable単に後の発明であり、既存のcloneコードはそれで改造されました。
vaxquis

2
「これは、たとえば、Eqタイプクラスを使用してHaskellに選択された設計です」これは、ある種の事実ですが、Javaのアプローチでは異なるが、異なるタイプの2つのオブジェクトは決して等しくないことをHaskellが明示的に前もって述べていることに注意する価値があります。したがって、等値演算はtypeの一部であり(したがって「typeclass」)、3番目のコンテキスト値の一部ではありません。
ジャック

19

本当の答え

なぜそこにあるComparatorインターフェースが、ノーHasherとはEquator

Josh Blochの好意による引用:

元のJava APIは、厳しい市場期間に対応するために、厳しい締め切りの下で非常に迅速に行われました。元のJavaチームは素晴らしい仕事をしましたが、すべてのAPIが完璧というわけではありません。

問題は、.clone()vs などの他の同様の問題と同様に、Javaの歴史のみにありますCloneable

tl; dr

これは主に歴史的な理由によるものです。現在の動作/抽象化はJDK 1.0で導入されましたが、コードの後方互換性を維持することは事実上不可能であったため、後で修正されませんでした。


最初に、いくつかの有名なJavaの事実を要約しましょう。

  1. Javaは、当初から現在まで、下位互換性があり、新しいバージョンでもレガシーAPIを引き続きサポートする必要がありました。
  2. そのため、JDK 1.0で導入されたほぼすべての言語構成体は現在に至るまで存在し、
  3. Hashtable.hashCode()および.equals()JDK 1.0で実装されました(Hashtable
  4. Comparable/ ComparatorはJDK 1.2(Comparable)で導入されました。

今、それは次のとおりです。

  1. それは、改造することは事実上不可能&無意味だった.hashCode().equals()人々はsuperobjectでそれらを置くより良い抽象化があるが実現した後、例えばので、まだ、後方互換性を維持しながら、明確なインターフェースに一つ一つ 1.2によって、Javaプログラマは、すべてのことを知っていたObjectそれらを持っており、彼らが持っていました物理的にそこに留まってコンパイル済みコード(JVM)の互換性も提供します-そして、Objectそれらを実際に実装するすべてのサブクラスに明示的なインターフェイスを追加すると、この混乱がClonable1つに等しくなります(BlochがCloneable sucksの理由、例えばEJ 2ndおよびSOを含む他の多くの場所)、
  2. 将来の世代がWTFを絶え間なく入手できるように、それらをそのまま残しました。

さて、あなたは「Hashtableこのすべてに何があるのか​​」と尋ねることができますか?

答えは:hashCode()/ equals()契約と1996分の1995でコアJava開発者のそれほど良い言語設計スキル。

1996年のJava 1.0言語仕様からの引用-4.3.2 The Class Object、p.41:

メソッドequalshashCodeは、java.util.Hashtable(§21.7)などのハッシュテーブルの利益のために宣言されています。メソッドequalsは、オブジェクト比較の概念を定義します。これは、参照ではなく値の比較に基づいています。

(注この正確な文がされた変更の引用、と言って、それ以降のバージョンでは:The method hashCode is very useful, together with the method equals, in hashtables such as java.util.HashMap.、それは不可能ダイレクトを作るために作るHashtable- hashCode- equals接続歴史的JLSを読まずに!)

Javaチームは、優れた辞書スタイルのコレクションが必要であると判断し、作成しましたHashtable(これまでのところ良いアイデアです)が、プログラマはできるだけ少ないコード/学習曲線でそれを使用できるようにしたかった(おっと! [それはすべての後にJDK 1.0だ]と、そこにはジェネリック医薬品はなかったまだいるので、それはどちらかのことを意味するすべての Object投入がHashtable明示的にいくつかのインターフェイスを実装する必要があります(とインタフェースは何...当時、ちょうど彼らの発端にまだなかったComparableとしてもまだ!) 、これを多くの人に使用することを抑止するか、または暗黙的に何らかのハッシュメソッドを実装Objectする必要があります。

明らかに、上記の理由により、ソリューション2が採用されました。うん、今、私たちは彼らが間違っていたことを知っています。...後知恵で賢くするのは簡単です。含み笑い

さて、hashCode() それを持っているすべてのオブジェクトが個別のequals()メソッドを持っている必要があるので、同様にequals()入れなければならなかったことは非常に明白Objectでした。

以来、デフォルトの有効な上、これらのメソッドの実装ab ObjectS冗長であることによって、本質的に役に立たない(作りa.equals(b) 同等a==ba.hashCode() == b.hashCode() ほぼ同等a==b、またしない限り、hashCodeおよび/またはequals数十万の上書きされるか、またはGC Objectアプリケーションのライフサイクルの中にSを1) 、主にバックアップ手段として、および使用上の便宜のために提供されたと言っても安全です。これは、私たちがいることは周知の事実に着く方法を正確に、常に両方を上書き.equals().hashCode()あなたが実際にオブジェクトを比較するか、それらをハッシュ格納する予定がある場合。他のものなしでそれらの一方のみをオーバーライドすることは、コードを台無しにする良い方法です(邪悪な比較結果または異常に高いバケット衝突値によって)-そして、それを回避することは初心者のための絶え間ない混乱とエラーの原因です(SOを検索それはあなた自身のために)そしてよりベテランのものへの絶え間ない迷惑。

また、C#はイコールとハッシュコードを少し上手に処理しますが、エリックリッパー自身は、C#が始まった年前にSunがJavaで行ったのとほぼ同じ間違いをC#で行ったと述べています

しかし、すべてのオブジェクトがハッシュテーブルに挿入するために自身をハッシュできる必要があるのはなぜですか?すべてのオブジェクトにできることを要求するのは奇妙なことのようです。今日、型システムをゼロから再設計している場合、おそらくIHashableインターフェースを使用して、ハッシングが異なる方法で行われる可能性があると思います。しかし、CLR型システムが設計されたとき、ジェネリック型はなかったため、任意のオブジェクトを格納できるようにするために汎用ハッシュテーブルが必要でした。

1、もちろんObject#hashCode衝突する可能性がありますが、それを行うには少し手間がかかります。詳細については、http//bugs.java.com/bugdatabase/view_bug.do?bug_id = 6809470およびリンクされたバグレポートを参照してください。/programming/1381060/hashcode-uniqueness/1381114#1381114は、この主題をさらに詳しく説明しています。


ただし、Javaだけではありません。同時代人の多く(Ruby、Python、…)と前任者(Smalltalk、…)、および後継者の一部にもUni​​versal EqualityとUniversal Hashabilityがあります(それは言葉ですか?)。
ヨルグWミットタグ

@JörgWMittagはProgrammers.stackexchange.com/questions/283194/を参照してください...- Javaの「UE」については同意しません。UEは歴史的にObjectデザインの本当の関心事ではありませんでした。ハッシュ可能性でした。
vaxquis

@vaxquisこれをハープしたくありませんが、私の以前のコメントは、同時に到達可能な2つのオブジェクトが同じ(デフォルト)ハッシュコードを持つことができることを示しています。
モニカの

1
@vaxquis OK。私はそれを買います。私の懸念は、学習している人がこれを見て、等号の代わりにシステムハッシュコードを使用して賢いと思うことです。彼らがそれを行うと、そうでないまれな場合を除いて十分に機能する可能性があります問題を確実に再現する方法はありません。
ジミージェームズ

1
受け入れられた答えの結論は「わからない」
フェニックス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.