简体   繁体   中英

What is the proper way to implement a robust equals() and hashCode() method in an inheritance hierarchy?

I have the following abstract Person class:

import java.util.Objects;

public abstract class Person {

    protected String name;
    protected int id;

    public Person(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public abstract String description();

    @Override
    public boolean equals(Object obj) {
        if(this == obj) return true;
        if(!(obj instanceof Person)) return false;

        return Objects.equals(this.name, ((Person) obj).name) &&
                this.id == ((Person) obj).id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.name, this.id);
    }
}

And Now I have a subclass of Person called Employee :

import java.time.LocalDate;
import java.util.Objects;

public class Employee extends Person {

    private double salary;
    private LocalDate hireDay;

    public Employee(String name, int id, double salary, int year, int month, int day) {
        super(name, id);
        this.salary = salary;
        this.hireDay = LocalDate.of(year, month, day);
    }

    @Override
    public String description() {
        return "Employee with a salary of " + this.salary;
    }

    @Override
    public int hashCode() {
        return super.hashCode() + Objects.hash(this.salary,this.hireDay);
    }

    @Override
    public boolean equals(Object obj) {

        return super.equals(obj) && 
        Double.compare(this.salary, ((Employee) obj).salary) == 0
              && Objects.equals(this.hireDay,((Employee)obj).hireDay);

}

To implement an equals method properly, it must conform to the following contract.

Reflextive: x.equals(x) is always True
Symmetric: x.equals(y) is equivalent to y.equals(x)
Transitive: x.equals(y) and y.equals(z) implies x.equals(z) is true

When I call the equals() method of the superclass inside the subclass, I first ensure that all objects being compared are subclasses of the superclass. This issue resolves the problem of comparing mixed-types and takes care of the contract mentioned above. I no longer have to use the following implementation of equals:

    @Override
    public boolean equals(Object obj) {

        if(this == obj) return true;
        else if(obj == null || this.getClass() != obj.getClass()) return false;

        Employee other = (Employee) obj;

        return Objects.equals(this.name, other.name) &&
               Double.compare(this.salary, other.salary) == 0 &&
               Objects.equals(this.hireDay, other.hireDay);

    }

Namely, I no longer have to check explicitly whether the current object ( this ) is of the same class as obj because of the method in the superclass that uses the instance of operator.

Is it more robust to put that implementation in the equals operator of the superclass or is it better to use the more explicit test in the subclass using the getClass() method in order to conform to the contract?

In terms of the hashCode() method, I hash the private instance fields specific to the subclass and simply add that to the result of the hash method in the superclass. I couldn't find any documentation that shows whether or not this is the proper way to implement the hashCode() function in general or in an inheritance hierarchy. I have seen code where people have explicitly specified their own hash functions.

I apologize if my questions are too general but I tried my best to ask them without being too ambiguous.

EDIT:

I asked Intellij to implement an equals and hashcode method and it decided to go with the last implementation that I posted above. Then, under what circumstances would I use instance of in the superclass? Would it be when I'm implementing a final equals method in the superclass such as only comparing Person objects based on user id?

Is it possible for two people to ever have the same id ? It shouldn't. So that logic extends to the Employee class, which means implementing equals and hashCode in the Person class is enough.

At this point, since you're only dealing with an int , you can use Integer.hashCode(id) for the hashCode and just compare the values for the equals .

If you want to implement equals and hashcode method use eclipse just right click in file go to source and select generate equals() & hashcode() with the fields that you need , just like below :

在此处输入图片说明

在此处输入图片说明

Here are my notes from reading Effective Java 2nd Edition:

Equals must adhere to general contract:

  • Reflexive: for non-null x : x.equals(x) == true
  • Symmetric: for non-null x,y : x.equals(y) <==> y.equals(x)
  • Transitive: for non-null x,y,z : x.equals(y) and y.equals(z) ==> x.equals(z) == true
  • Consistent: for any non-null x,y: if x.equals(y) == true , then for all invocations must return true if no change in x and y
  • Null: for non-null x : x.equals(null) == false

High Quality equals method:

  1. Using == to check if the argument is a reference to this object ( x == x )
  2. Use instanceof to check if argument is the correct type (also checks for null )
  3. Cast the argument to correct type
  4. For each "significant" field in the class, check if that field of the argument matches the corresponding field of this object
  5. After done, check if Symmetric, transitive and consistent

Final caveats:

  • always override hashCode when you override equals
  • Don't try to be too clever
  • don't substitute another type for Object in the equals declaration -> Not worth it minor performance gains for added complexity

Hashcode direct quote from Effective Java 2nd Edition

  1. Store some constant nonzero value, say, 17, in an int variable called result.
  2. For each significant field f in your object (each field taken into account by the equals method, that is), do the following:

    • Compute an int hash code c for the field:
      1. If the field is a boolean, compute (f ? 1 : 0) .
      2. If the field is a byte, char, short, or int, compute (int) f.
      3. If the field is a long, compute (int) (f ^ (f >>> 32)).
      4. If the field is a float, compute Float.floatToIntBits(f).
      5. If the field is a double, compute Double.doubleToLongBits(f) , and then hash the resulting long .
      6. If the field is an object reference and this class's equals method compares the field by recursively invoking equals , recursively invoke hashCode on the field. If a more complex comparison is required, compute a “canonical representation” for this field and invoke hashCode on the canonical representation. If the value of the field is null , return 0 (or some other constant, but 0 is traditional).
      7. If the field is an array, treat it as if each element were a separate field. That is, compute a hash code for each significant element by applying these rules recursively, and combine these values per step 2.b. If every element in an array field is significant, you can use one of the Arrays.hashCode methods added in release 1.5.
    • Combine the hash code c computed in step 2.a into result as follows: result = 31 * result + c;
  3. Return result.

  4. When you are finished writing the hashCode method, ask yourself whether equal instances have equal hash codes. Write unit tests to verify your intuition!

so following these rules:

@Override
public boolean equals(Object obj) {

    if (this == obj) {
        return true;
    }

    if (!(obj instanceof Employee)) {
        return false;
    }
    Employee other = (Employee) obj;

    return super.equals(other) &&
           Double.compare(this.salary, other.salary) == 0 &&
           this.hireDay.equals(other.hireDay);

}

In your case though it seems like id should already uniquely identify any person, so you should just use that for comparison in equals and not override it in any subclass.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM