質問
http://leepoint.net/notes-java/data/expressions/22compareobjects.html
equalsを定義することは些細なものではないことがわかります。実際、特にサブクラスの場合、それを正しくするのは適度に困難です。問題の最良の治療法は、HorstmannのCore Java Vol 1にあります。
常にequals()をオーバーライドする必要がある場合、オブジェクト比較を行う必要に迫られないための良いアプローチは何でしょうか?優れた「デザイン」の代替案は何ですか?
編集:
これが私が意図した方向に進んでいるかどうかはわかりません。たぶん、「なぜ2つのオブジェクトを比較したいのか」という線に沿って質問があるはずです。その質問に対するあなたの答えに基づいて、比較の代替解決策はありますか?私が言いたいのは、equals の別の実装という意味ではありません。つまり、平等をまったく使用していません。重要なのは、なぜ 2 つのオブジェクトを比較するのかという質問から始めることだと思います。
解決
equals()を常にオーバーライドする必要がある場合、オブジェクトの比較を行わなければならないように追い詰められないための良いアプローチは何ですか?
あなたは間違っています。可能な限り、equals をオーバーライドする必要はありません。
この情報はすべて次の情報から来ています 効果的な Java、第 2 版 (ジョシュ・ブロック)。これに関する初版の章はまだ無料で入手できます ダウンロード.
効果的な Java から:
問題を回避する最も簡単な方法は、等しい方法をオーバーライドしないことです。この場合、クラスの各インスタンスはそれ自体と等しくなります。
任意にequals/hashCodeをオーバーライドする場合の問題は、継承です。一部の等しい実装では、次のようにテストすることを推奨しています。
if (this.getClass() != other.getClass()) {
return false; //inequal
}
実際、 日食 (3.4) Java エディターは、ソース ツールを使用してメソッドを生成するときにこれを実行します。ブロッホ氏によると、これは規則に違反するため間違いです。 リスコフ置換原理.
効果的な Java から:
均等契約を維持しながら、即座のクラスを拡張してバリューコンポーネントを追加する方法はありません。
平等性の問題を最小限に抑える 2 つの方法については、 クラスとインターフェース 章:
- 継承より合成を優先する
- 継承を設計および文書化するか、禁止する
私の知る限り、唯一の代替策はクラスの外部のフォームで等価性をテストすることです。それがどのように実行されるかは、型の設計とそれを使用しようとしているコンテキストによって異なります。
たとえば、比較方法を文書化するインターフェイスを定義できます。以下のコードでは、Service インスタンスが実行時に同じクラスの新しいバージョンに置き換えられる可能性があります。その場合、異なる ClassLoader があると、equals 比較は常に false を返すため、equals/hashCode のオーバーライドは冗長になります。
public class Services {
private static Map<String, Service> SERVICES = new HashMap<String, Service>();
static interface Service {
/** Services with the same name are considered equivalent */
public String getName();
}
public static synchronized void installService(Service service) {
SERVICES.put(service.getName(), service);
}
public static synchronized Service lookup(String name) {
return SERVICES.get(name);
}
}
「なぜ 2 つのオブジェクトを比較したいのですか?」
わかりやすい例は、2 つの文字列が同じかどうか (または 2 つの文字列が同じかどうか) をテストすることです。 ファイル, 、 または URI)。たとえば、解析するファイルのセットを構築したい場合はどうなるでしょうか。定義上、セットには一意の要素のみが含まれます。ジャワの セット type は、要素の一意性を強制するために、equals/hashCode メソッドに依存します。
他のヒント
私はそれはイコール、常に上書きされなければならないというのは本当だとは思いません。私はそれを理解するようルールをオーバーライドするに等しいそれはあなたが意味的に等価のオブジェクトを定義する方法の明確いる場合にのみ意味があります。あなたは別のハッシュコードを返す同等として定義したオブジェクトを持っていないように、その場合は、あなたにものhashCode()をオーバーライドします。
あなたは意味の等価性を定義することができない場合は、、私は利益が表示されません。
については、ちょうどそれが右の方法を教えてください。
ここに私はジョシュブロッホによって効果的な、Javaから適用される知識であるテンプレートに等しいです。詳細については、本を読んでます:
@Override
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
// only do this if you are a subclass and care about equals of parent
if(!super.equals(obj)) {
return false;
}
if(obj == null || getClass() != obj.getClass()) {
return false;
}
final YourTypeHere other = (YourTypeHere) obj;
if(!instanceMember1.equals(other.instanceMember1)) {
return false;
}
... rest of instanceMembers in same pattern as above....
return true;
}
うーん
一部のシナリオでは、オブジェクトを変更不可 (読み取り専用) にし、単一ポイントから作成する (ファクトリ メソッド) ことができます。
同じ入力データ (作成パラメータ) を持つ 2 つのオブジェクトが必要な場合、ファクトリは同じインスタンス参照を返すため、「==」を使用するだけで十分です。
このアプローチは、特定の状況下でのみ役立ちます。そして、ほとんどの場合、やりすぎに見えるでしょう。
を見てみましょう この答え そのようなことを実装する方法を知るためです。
警告: コードが膨大です
ラッパークラスがどのように動作するかを簡単に見てみましょう Java 1.5以降
Integer a = Integer.valueOf( 2 );
Integer b = Integer.valueOf( 2 );
a == b
は真実ですが、
new Integer( 2 ) == new Integer( 2 )
は誤りです。
内部で参照を保持し、入力値が同じ場合はそれを返します。
ご存知のとおり、Integer は読み取り専用です
質問の元になった String クラスでも同様のことが起こります。
たぶん私は要点を見逃していますが、独自のメソッドを定義するのではなくequalsを使用する唯一の理由です 別の名前で これは、コレクション (およびおそらく JDK または最近呼ばれるものの他のもの) の多くが、equals メソッドが一貫した結果を定義することを期待しているためです。しかしそれを超えて、同等に実行したい比較が 3 種類考えられます。
- 2 つのオブジェクトは実際には同じインスタンスです。== を使用できるため、イコールを使用するのは意味がありません。また、Java での動作を忘れている場合は訂正してください。デフォルトの平等メソッドは、自動的に生成されたハッシュ コードを使用してこれを実行します。
- 2 つのオブジェクトは同じインスタンスへの参照を持っていますが、同じインスタンスではありません。これは便利です、ええと、時々...特に、それらが永続化されたオブジェクトであり、DB 内の同じオブジェクトを参照する場合はそうです。これを行うには、equals メソッドを定義する必要があります。
- 2 つのオブジェクトは、同じインスタンスである場合もそうでない場合もありますが、値が等しいオブジェクトへの参照を持っています (つまり、階層全体で値を比較します)。
2 つのオブジェクトを比較する理由は何ですか?そうですね、それらが等しい場合は、あることをしたいと思うでしょうし、そうでない場合は、別のことをしたいと思うでしょう。
とはいえ、それは当面のケース次第です。
ほとんどの場合、等号をオーバーライドする主な理由は、()は、特定のコレクション内の重複をチェックすることです。たとえば、あなたはあなたがあなたのオブジェクト内equals()とhashCode()をオーバーライドする必要が作成したオブジェクトを含むように設定を使用する場合。あなたがマップ内のキーとしてカスタムオブジェクトを使用する場合も同様です。
私は多くの人が対等に()とhashCode()をオーバーライドすることなく、設定やマップに自分のカスタムオブジェクトを追加する、実際にミスを犯す見てきたように、これは非常に重要です。これは特に陰湿なことができる理由は、コンパイラが文句を言わないであろうと、あなたが同じデータが含まれているが、重複を許可していないコレクション内の別の参照を持っている複数のオブジェクトで終わることができています。
あなたは、単一のString属性「名前」とNameBeanと呼ばれる簡単な豆を持っていた場合は、たとえば、NameBean(例えばNAME1とNAME2)の2つのインスタンスを構築することができ、同じ「名前」属性の値(例えば「アリス」とそれぞれ)。その後、セットにNAME1とNAME2の両方を追加することができ、設定が意図されるものであるサイズ2というよりもサイズが1になります。あなたには、いくつかの他のオブジェクトに名前Beanをマッピングするために、このような地図などの地図を持って、そしてあなたが最初の「最初」の文字列にNAME1をマッピングし、後の文字列に名2をマッピングされた「第二」場合同様に、あなたは、両方のキー/値のペアを持っていますマップ内(例えばNAME1 - > "最初"、名前2 - > "第二")。あなたはマップがルックアップを行うときに、それはnullを返しますNAME1、NAME2、または名前を持つ別の参照「アリス」のいずれかであるあなたが合格正確な参照、にマッピングされた値を返します。
ここではそれを実行するの出力が先行具体例である:
出力:
Adding duplicates to a map (bad):
Result of map.get(bean1):first
Result of map.get(bean2):second
Result of map.get(new NameBean("Alice"): null
Adding duplicates to a map (good):
Result of map.get(bean1):second
Result of map.get(bean2):second
Result of map.get(new ImprovedNameBean("Alice"): second
コード:
// This bean cannot safely be used as a key in a Map
public class NameBean {
private String name;
public NameBean() {
}
public NameBean(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
// This bean can safely be used as a key in a Map
public class ImprovedNameBean extends NameBean {
public ImprovedNameBean(String name) {
super(name);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if(obj == null || getClass() != obj.getClass()) {
return false;
}
return this.getName().equals(((ImprovedNameBean)obj).getName());
}
@Override
public int hashCode() {
return getName().hashCode();
}
}
public class MapDuplicateTest {
public static void main(String[] args) {
MapDuplicateTest test = new MapDuplicateTest();
System.out.println("Adding duplicates to a map (bad):");
test.withDuplicates();
System.out.println("\nAdding duplicates to a map (good):");
test.withoutDuplicates();
}
public void withDuplicates() {
NameBean bean1 = new NameBean("Alice");
NameBean bean2 = new NameBean("Alice");
java.util.Map<NameBean, String> map
= new java.util.HashMap<NameBean, String>();
map.put(bean1, "first");
map.put(bean2, "second");
System.out.println("Result of map.get(bean1):"+map.get(bean1));
System.out.println("Result of map.get(bean2):"+map.get(bean2));
System.out.println("Result of map.get(new NameBean(\"Alice\"): "
+ map.get(new NameBean("Alice")));
}
public void withoutDuplicates() {
ImprovedNameBean bean1 = new ImprovedNameBean("Alice");
ImprovedNameBean bean2 = new ImprovedNameBean("Alice");
java.util.Map<ImprovedNameBean, String> map
= new java.util.HashMap<ImprovedNameBean, String>();
map.put(bean1, "first");
map.put(bean2, "second");
System.out.println("Result of map.get(bean1):"+map.get(bean1));
System.out.println("Result of map.get(bean2):"+map.get(bean2));
System.out.println("Result of map.get(new ImprovedNameBean(\"Alice\"): "
+ map.get(new ImprovedNameBean("Alice")));
}
}
平等性はロジックの基本であり (同一性の法則を参照)、平等性なしで実行できるプログラミングはほとんどありません。自分が作成したクラスのインスタンスの比較に関しては、それはあなた次第です。コレクション内でそれらを見つけたり、マップでキーとして使用したりできるようにする必要がある場合は、等価性チェックが必要になります。
Java で重要なライブラリをいくつか書いたことがある人なら、特に胸にあるツールが equals
そして hashCode
. 。平等性は最終的にクラス階層と密接に結合することになり、コードが脆弱になります。さらに、これらのメソッドは Object 型のパラメータを取るだけであるため、型チェックは提供されません。
等価性チェック (およびハッシュ) でエラーが発生しにくくなり、タイプセーフになる方法があります。の中に 関数型 Java 図書館、見つかるよ Equal<A>
(そして対応する Hash<A>
) ここで、平等は単一のクラスに分離されます。作曲のためのメソッドが備わっています Equal
既存のインスタンスからのクラスのインスタンス、およびコレクションのラッパー、 イテラブル, ハッシュマップ, 、 そして ハッシュセット, 、それを使用します Equal<A>
そして Hash<A>
の代わりに equals
そして hashCode
.
このアプローチの最も優れている点は、equals と hash メソッドが必要になったときにそれらを記述することを決して忘れないことです。型システムは覚えておくのに役立ちます。