13

読書:効果的なJava-JoshuaBlochによる第2版

項目8-等しい状態をオーバーライドする場合は、一般的な契約に従います。

プログラマーが次のようなequalsメソッドを作成し、それが正しく機能しない理由について何時間も戸惑うことは珍しくありません。

[コードサンプルはこちら]

問題は、このメソッドがObject.equalsをオーバーライドせず、その引数がObject型であるが、代わりにオーバーロードすることです。

コードサンプル:

public boolean equals(MyClass o) {
    //...
}

私の質問:

このコードサンプルのようにオーバーロードする強い型のequalsメソッドが十分でないのはなぜですか?この本は、オーバーライドではなくオーバーロードが悪いと述べていますが、なぜこれが当てはまるのか、どのシナリオでこれがequalsメソッドが失敗するのかについては述べていません。

4

3 に答える 3

20

equals(Object)これは、メソッドをオーバーロードしても、コレクションなど、メソッドが明示的に使用されている場所での動作が変更されないためです。たとえば、次のコードを考えてみましょう。

public class MyClass {

    public boolean equals(MyClass m) {
        return true;
    }
}

これを次のようなものに入れるとHashSet

public static void main(String[] args) {
    Set<MyClass> myClasses = new HashSet<>();
    myClasses.add(new MyClass());
    myClasses.add(new MyClass());
    System.out.println(myClasses.size());
}

オーバーロードからすべてのインスタンスが等しいと予想され、セットが2番目のインスタンスを追加しない2場合でも、これは出力されますが、出力されません。1MyClass

だから基本的に、これはtrue

MyClass myClass = new MyClass();
new MyClass().equals(myClass);

これはfalse

Object o = new MyClass();
new MyClass().equals(o);

後者は、コレクションや他のクラスが同等性を判断するために使用するバージョンです。実際、これが返す唯一trueの場所は、パラメーターが明示的MyClassにそのサブタイプのインスタンスまたは1つである場所です。


編集:あなたの質問ごとに:

オーバーライドとオーバーロード

オーバーライドとオーバーロードの違いから始めましょう。オーバーライドすると、実際にメソッドを再定義します。元の実装を削除して、実際に独自の実装に置き換えます。だからあなたがするとき:

@Override
public boolean equals(Object o) { ... }

実際には、新しい実装を再リンクして、元の実装(または最後に定義したスーパークラス)equalsを置き換えています。Object

一方、あなたがするとき:

public boolean equals(MyClass m) { ... }

同じ名前でパラメーターが異なるメソッドを定義しているため、まったく新しいメソッドを定義しています。HashSetを呼び出すときはequals、次のタイプの変数で呼び出しますObject

Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

HashMap.put(そのコードは、の基礎となる実装として使用されるのソースコードからのものですHashSet.add。)

明確にするために、それが異なるものを使用するのequalsは、equalsメソッドがオーバーライドされたときであり、オーバーロードされたときだけです。@Overrideオーバーロードされたメソッドに追加しようとequalsすると、コンパイラエラーで失敗し、メソッドをオーバーライドしないと文句を言います。equalsオーバーロードしているため、同じクラスで両方のメソッドを宣言することもできます。

public class MyClass {

    @Override
    public boolean equals(Object o) {
        return false;
    }

    public boolean equals(MyClass m) {
        return true;
    }
}

ジェネリック

ジェネリックに関してequalsは、ジェネリックではありません。それは明示的にObjectそのタイプとしてとられるので、その点は議論の余地があります。さて、あなたがこれをやろうとしたとしましょう:

public class MyGenericClass<T> {

    public boolean equals(T t) {
        return false;
    }
}

これは次のメッセージではコンパイルされません:

名前の衝突:MyGenericClass型のequals(T)メソッドは、Object型のequals(Object)と同じ消去を行いますが、オーバーライドしません。

そして、あなたがそれをしようとする@Overrideと:

public class MyGenericClass<T> {

    @Override
    public boolean equals(T t) {
        return false;
    }
}

代わりにこれを取得します:

MyGenericClass型のメソッドequals(T)は、スーパータイプメソッドをオーバーライドまたは実装する必要があります

だからあなたは勝つことはできません。ここで起こっていることは、Javaが消去を使用してジェネリックを実装していることです。Javaがコンパイル時にすべての汎用型のチェックを終了すると、実際のランタイムオブジェクトはすべて。に置き換えられObjectます。どこにでもT、実際のバイトコードにはObject代わりに含まれています。これが、リフレクションがジェネリッククラスでうまく機能しない理由であり、のようなことができない理由ですlist instanceof List<String>

これにより、ジェネリック型でオーバーロードできないようになります。このクラスがある場合:

public class Example<T> {
    public void add(Object o) { ... }
    public void add(T t) { ... }
}

add(T)クラスのコンパイルが実際に完了すると、メソッドは両方とも同じシグネチャを持つため、メソッドからコンパイラエラーが発生しますpublic void add(Object)

于 2012-10-08T19:20:22.867 に答える
12

このコードサンプルのようにオーバーロードする強い型のequalsメソッドが十分でないのはなぜですか?

オーバーライドしないためObject.equals。で宣言されたメソッドについてのみ知っている汎用コードObject(たとえばHashMap、キーの同等性のテスト)は、オーバーロードを呼び出すことにはなりません-参照の同等性を与える元の実装を呼び出すことになります。

オーバーロードはコンパイル時に決定されますが、オーバーライドは実行時に決定されることに注意してください。

オーバーライドする場合はequals、強く型付けされたバージョン提供し、で宣言されたメソッドからそれに委任することをお勧めしますequals

これがどのようにうまくいかないかの完全な例です:

import java.util.*;

final class BadKey {    
    private final String name;

    public BadKey(String name) {
        // TODO: Non-nullity validation
        this.name = name;
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }

    public boolean equals(BadKey other) {
        return other != null && other.name.equals(name);
    }
}

public class Test {

    public static void main(String[] args) throws Exception {
        BadKey key1 = new BadKey("foo");
        BadKey key2 = new BadKey("foo");
        System.out.println(key1.equals(key2)); // true

        Map<BadKey, String> map = new HashMap<BadKey, String>();
        map.put(key1, "bar");
        System.out.println(map.get(key2)); // null
    }
}

修正は、次のようにオーバーライドを追加するだけです。

@Override
public boolean equals(Object other) {
    // Delegate to the more strongly-typed implementation
    // where appropriate.
    return other instanceof BadKey && equals((BadKey) other);
}
于 2012-10-08T19:14:01.703 に答える
2

equalsを使用するコレクションは、。Object.equals(Object)とは異なるメソッド(MyClassでオーバーライドされる可能性があるため、多態的に呼び出される可能性があります)を使用するためMyClass.equals(MyClass)です。

メソッドをオーバーロードすると、新しい別のメソッドが定義されます。このメソッドは、たまたま別のメソッドと同じ名前になっています。

于 2012-10-08T19:15:16.220 に答える