Javaでequals()を記述する方法

Feb 22 2023
この記事を終えるまでに、Java プログラミング言語で最適なquals() メソッドを作成できるようにするために必要な情報がすべて網羅されることになります。等価性の詳細については、equals() メソッドのベスト プラクティスを使用するために必要な基本情報をカバーするために、数学的定義と Java プログラミング言語の観点から取り上げます。

この記事を終えるまでに、Java プログラミング言語で最適なquals() メソッドを作成できるようにするために必要な情報がすべて網羅されることになります。等価性の詳細については、equals()メソッドのベスト プラクティスを使用するために必要な基本情報をカバーするために、数学的定義と Java プログラミング言語の観点から取り上げます。

平等の数学的定義

Wikipediaによると、数学的等価性の定義は次のとおりです。

数学における等価性とは、2 つの量、より一般的には 2 つの数式の間の関係であり、量が同じ値を持つか、または数式が同じ数学的対象を表すことを主張します。AとBが等しいことは、 A = Bと書かれ、Aと等しいB と発音されます。

Java における平等の定義

Java プログラミング言語では、等しいとは、単に同じ値を持つことを意味します。文字通り同じ値を持つことを意味する場合もあれば、オブジェクトのプロパティに関して同じ構造を持つことを意味する場合もあります。

2 つのプリミティブ型値が等しいことを確認する必要がある場合は、次の例のように等号が直接使用されます。

int a = 5;
int b = 6;
int c = 5;

// Prints "false" since a and b do not have the same value.
System.out.println(a == b);

// Prints "true" since a and c have the same value.
System.out.println(a == c);

一方で、2 つのオブジェクトが等しいかどうかを確認する必要がある場合があります。オブジェクトに関しては、2 つのオブジェクトの同等性をチェックするプロセスの異なる段階で使用される 2 つの異なるアプローチがあります。詳細を見ていきましょう。

参照の等価性

Java プログラミング言語では、オブジェクトの実際の値はメモリ内のどこかに格納され、それらはやはりメモリ内のどこかに格納されているオブジェクト参照によってポイントされます。次のコードスニペットを見てみましょう。

// Create an object with "new" operator.
// Point to object with object reference "a".
Object a = new Object();

// Create another object reference.
// Point to the same object as the reference "a" is pointing.
Object b = a;

// Output is true.
System.out.println(a == b);

// Create another object with "new" operator.
// Point to object with object reference "b".
b = new Object();

// Output is false.
System.out.println(a == b);

このようなシナリオでは、「a」と「b」は実際には同じオブジェクトを指す 2 つの参照であるため、参照の等価性が得られるため、「a」と「b」は等しいと直接言えます。

ただし、その逆のシナリオが常に有効であるわけではありません。2 つの異なる参照が異なるメモリ位置を指す場合がありますが、参照が指すオブジェクトは依然として等しい場合があります。

普段の生活の中で、量を比較することがあります。たとえば、あなたが 2 個のリンゴを持っていて、あなたの友人が 2 個のリンゴを持っている場合、あなたとあなたの友人が持っているリンゴの数は同じになります。

一方、物事はその性質や構造の観点から比較されることもあります。このシナリオの詳細を見てみましょう。

構造的平等

構造的同等性は、オブジェクトがそのプロパティの状態に従って比較されるタイプです。このタイプの等価性をチェックするには、「==」演算子だけではなく、より洗練されたツールが必要です。それは、equals() メソッドです。これは、Java プログラミング言語のクラス階層の最上位にあるオブジェクト クラスに由来します。今後の Java コード スニペットを確認してみましょう。

public class Car {

    // Assume that we have corresponding constructor, accessors, mutators, etc.

    private String brand;
    private String model;
    private Integer modelYear;
    private Integer mileage;
    
}

Car car1 = new Car("Audi","R8", 2023);
Car car2 = new Car("Audi","R8", 2023);
Car car3 = new Car("Audi","R8", 2021);

System.out.println(car1.equals(car2));
System.out.println(car2.equals(car3));

public boolean equals(Object obj) {
    return (this == obj);
}

public class Car {

    private String brand;
    private String model;
    private Integer modelYear;
    private Integer mileage;

    // Assume that we have corresponding constructor, accessors, mutators, etc.

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o; 
        return brand.equals(car.brand) && model.equals(car.model) && modelYear.equals(car.modelYear);
    }
    
}

2 行目では、オブジェクトのアイデンティティの確認を開始します。オブジェクト参照oが null であるか、両方のオブジェクト参照が異なるクラスに属するオブジェクトを指している場合、これら 2 つのオブジェクトは同じ同一性を持たず、等しくないと直接言えます。そうでない場合は、最終的にオブジェクトの実際の構造を確認することができます。

メソッドの 3 行目では、メソッドが Object クラスからオーバーライドされ、Car クラスに属していることがわかったので、参照oを対応するクラス型にキャストします。

最後に、メソッドの最後の行で 2 つのオブジェクトの構造がチェックされ、これが ID が定義される実際の行です。単純に、ブランドのモデルと年式が同じであれば、これら 2 つのオブジェクトは等しいことになります。言い換えれば、これは構造的平等です。

私たちは小学校で数学的平等の性質を学びました。これらのプロパティの一部は、 equals()メソッドでも有効である必要があります。これは、これらのプロパティをequals()メソッドの要素に追加する必要があることを意味します。これらの特性を数学的等価性と Java における等価性の観点から見てみましょう。

再帰的プロパティ

再帰的性質の数学的定義では、すべての実数に対してx ;

x = x

数値はそれ自体に等しい。

Java の平等性の観点から言えば、オブジェクトはそれ自体と等しい必要があります。

Car car1 = new Car("Audi","R8",2013);
System.out.println(car1.equals(car1));

@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o; 
        return mileage < car.mileage && brand.equals(car.brand) && model.equals(car.model) && modelYear.equals(car.modelYear);
    }

対称性

対称性の数学的定義に関しては、すべての実数xとyに対して、

x = yの場合、y = x

等しい順序は関係ありません。

Java の等価性の観点からは、オブジェクト a がオブジェクト b と等しい場合、オブジェクト b はオブジェクト a と等しくなければなりません。

Car car1 = new Car("Audi","R8",2013);
Car car2 = new Car("Audi","R8",2013);
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car1));

public class Car {

    private String brand;
    private String model;
    private Integer modelYear;
    private Integer mileage;

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Car)) return false;
        Car car = (Car) o;
        return brand.equals(car.brand) && model.equals(car.model) && modelYear.equals(car.modelYear);
    }
}

public class SportCar extends Car {
    
    private String engineType;

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof SportCar)) return false;
        SportCar sportCar = (SportCar) o;
        return engineType.equals(sportCar.engineType);
    }
}

Car car1 = new Car("Audi", "R8", 2012, 1000);
SportCar car2 = new SportCar("Audi", "R8", 2012, 1000, "4.0");
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car1));

推移的プロパティ

推移性の数学的定義では、すべての実数x、y、zに対して、

x = yかつy = zの場合、x = z

同じ数字に等しい 2 つの数字は互いに等しい。

Java の等価性の観点から、オブジェクト a がオブジェクト b と等しく、オブジェクト b がオブジェクト c と等しい場合、オブジェクト a はオブジェクト c と等しくなければなりません。

SportCar car1 = new SportCar("Audi", "R8", 2012, 1000, new Turbo("100RPM"));
Car car2 = new Car("Audi", "R8", 2012, 1000);
SportCar car3 = new SportCar("Audi", "R8", 2012, 1000, new Turbo("101RPM"));
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car3));
System.out.println(car1.equals(car3));

public class SportCar extends Car {
    
    private Turbo turbo;

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Car)) return false;
        if (!(o instanceof SportCar)) return o.equals(this);
        return super.equals(o) && ((SportCar) o).turbo == turbo;
    }
}

public class Turbo {
    private String power;

    public Turbo(String power) {
        this.power = power;
    }
}

SportCar car1 = new SportCar("Audi", "R8", 2012, 1000, new Turbo("100RPM"));
Car car2 = new Car("Audi", "R8", 2012, 1000);
SportCar car3 = new SportCar("Audi", "R8", 2012, 1000, new Turbo("101RPM"));
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car3));
System.out.println(car1.equals(car3));

一貫したプロパティ

数学的に完全に同等なものがないため、これは異なるプロパティです。この場合、equals() メソッドを連続して実行して同じ結果を得る必要があります。

以下のコードスニペットを見てみましょう。

Car car1 = new Car("Audi", "R8", 2012, 1000);
Car car2 = new Car("Audi", "R8", 2012, 1000);

for (int i = 0; i < 100000; i++) {
    System.out.println(car1.equals(car2));
}

@Override
public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof Car)) return false;
    Car car = (Car) o;
    Random random = new Random();
    return brand.equals(car.brand) && model.equals(car.model) && (random.nextInt(10) + 2013) == car.modelYear;
}

注:この例は私にとってあまり満足できるものではありません。徹底的に調査した後、より良いものと交換させていただきます。Java のequals() メソッドのドキュメントには、「x.equals(y)一貫して returntrueまたは一貫して returnを複数回呼び出すfalse」と明確に記載されています。 この声明には確実な違反例がなければなりません。

Nullパラメータプロパティ

私はこのプロパティをこのように呼ぶことにしました。

以下のコード スニペットを見てください。

Car car1 = new Car("Audi", "R8", 2012, 1000);
Car car2 = null;
System.out.println(car1.equals(car2));

これで、等価性の正しい計算を行うために、equals() メソッドの実装にどのプロパティが必要かが明確になりました。また、考えられる違反シナリオも確認できたので、コードの品質とビジネス ロジックを考慮して違反シナリオを回避できるようになりました。

現在、多くの IDE は、可能な限り最良のequals()メソッド実装を生成するための生成メカニズムを提供しています。ほとんどの場合、equals()メソッドを実装する代わりに、これらを使用することをお勧めします。Intellij IDEAの例を見てみましょう。

@Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       Car car = (Car) o;
       return brand.equals(car.brand) && model.equals(car.model) && modelYear.equals(car.modelYear) && mileage.equals(car.mileage);
   }

2 つのオブジェクト参照が同じオブジェクトを指している可能性があるため、最初の行では参照の等価性がチェックされます。

2 行目では、 Null パラメーター プロパティの検証が保証され、推移プロパティと対称プロパティの違反を防ぐために、参照によって指定されるオブジェクトのクラス型を制御することにより、同じ if 条件がガード if 句として機能します。 。

3 行目では、Car オブジェクトのプロパティにアクセスするために、オブジェクト参照が対応するクラス タイプ (この例では Car ) にキャストされます。

最後に、4 行目で、構造的同等性の観点から同等性がチェックされます。

他にも、 equals()メソッド生成用のテンプレートがあります。行って確認することもできます。

実装を自分で作成したい場合は、実装ではすべてのquals()メソッドプロパティの有効性を保証する必要があることに留意してください。

この記事の最後まで来ても、カバーする必要のある非常に重要なギャップがまだ残っています。IDE を開いて、equals()メソッドを生成してみます。hashCode()メソッドを使用せずに、 equals()メソッドのみを生成するオプションがないことがわかります。Java プログラミング言語にはいくつかのコレクション型があるため、これら 2 つのメソッドを互いに分離しないことをお勧めします。公式 Java ドキュメントには、通常、 equals()メソッドがオーバーライドされるときは必ずhashCode()メソッドをオーバーライドする必要があるとも記載されています。ハッシュ化とhashCode()メソッドに関する対応する情報は、私の別の記事で提供したいと思います。

また、 JPA のエンティティ クラスなどの例外的なケースでは、equals()メソッドのプロパティに固執する必要がない場合もあります。それは独自の記事にも値します。

結論は

正しいequals()メソッドを実装するための重要なポイントはすべてこの記事で説明されています。また、 equals()メソッドのプロパティの検証をテストするために、対応する単体テストを作成することをお勧めします。Web 上にある他のリソースも忘れずに参照してください。

そもそもオブジェクト指向設計の基礎からの話なので、とても簡単で初歩的な話のように思えます。ただし、詳細を見ればわかるように、これらの基本概念を飛ばさずに完全に理解することが非常に重要です。