简体   繁体   中英

hashCode implementation for “equals any of some fields are equal”

I want objects of a particular class to be equal if one of their fields are equal. How can I write a consistent hashCode method for such a class?

(Disclaimer because I know this is not best practice: The class is a wrapper for another class and should be used for keys in Maps. It is intended that two different of these objects with one equal field would result in the same Map entry. Actually each of the fields would identify the underlying object on their own but I do not always have the same identifying field for two objects available. I cannot control and therefore change this "ambiguous" identification mechanism. Alternative solutions to tackle this are also welcome.)

Are there strategies for implementing hashCode() accordingly? I only know of implementations involving conjunctions (as with &&) in equals. How to make sure that hashCode() is the same if either of the fields is equal?

Here is the simplified equals method for which I would like to write a consistent hashCode() implementation:

public boolean equals(C other)
{
    return (field1 != null && field1.equals(other.field1))
            || (field2 != null && field2.equals(other.field2))
            || (field3 != null && field3.equals(other.field3));
}

EDIT: as per the input data, no cases like (1, 2, 3) equals (1, 6, 7) can occur. The objects are only produced such that some of the fields can be null, but not contradicting as in the example. Simply put in practise the only combinations equal to (1, 2, 3) should be (1, 2, 3), (1, null, null), (null, 2, null), (1, 2, null) and so forth. I acknowledge that this approach is not particularly robust.

You don't usually implement equals() and hashCode() using just one field of your object class. Everyone will probably advise you against it. The general practice is to ensure that you compare all the fields and ensure that they are all equal in order to call .equals() . hashCode() uses .equals() in order to hash those objects. However, if you can control what you are doing, you can simply use the hashCode() of a particular field of your object and override .equals() and .hashCode() based on that (but again, not advisable).

You can't use any fields to implement the hashCode because the field aren't equals all the time.

Your hashCode method needs to return the same value for equals objects all the time. As only one field needs to be equal in your equals method, and it isn't always the same, your only option is to return a constant in your hashCode method.

The implementation is inefficient but it is valid and consistent with equals.

The implementation could be:

public int hashCode() {
    return 0;
}

It seems the only solution is this

public int hashCode() {
    return 1;
}

The implementation of equals() in the question

  public boolean equals(C other) {
    //TODO: you have to check if other != null...

    return (field1 != null && field1.equals(other.field1)) ||
           (field2 != null && field2.equals(other.field2)) ||
           (field3 != null && field3.equals(other.field3));
  }

is incorrect one . When implementing equals we have to ensure that

  a.equals(a)
  if a.equals(b) then b.equals(a)
  if a.equals(b) and b.equals(c) then a.equals(c)

Counter example for the 1st rule is the instance where all fields of comparison ( field1, field2, field3 ) are null :

  MyObject a = new MyObject();
  a.setField1(null);
  a.setField2(null);
  a.setField3(null);

  a.equals(a); // <- return false

Counter example for the 3d rule:

  MyObject a = new MyObject();
  a.setField1("1"); // field2 and field3 == null 

  MyObject b = new MyObject();
  b.setField1("1"); // field3 == null 
  b.setField2("2"); 

  MyObject c = new MyObject();
  c.setField2("2");  // field1 and field3 == null

  a.equals(b); // true (by field1)
  b.equals(c); // true (by field2)
  a.equals(c); // false!

And that's why there's no solution for hashCode() ...

Starting from the point that as you say it is not a good practice, I think one way you can achieve this is maintanting a reference to the other object in each of the objects and computing hashCode based on the equality of the fields:

public class Test {
    private String field1;
    private Integer field2;

    private Test other;

    public String getField1() {
        return field1;
    }

    public void setField1(String field1) {
        this.field1 = field1;
    }

    public int getField2() {
        return field2;
    }

    public void setField2(int field2) {
        this.field2 = field2;
    }

    public Test getOther() {
        return other;
    }

    public void setOther(Test other) {
        this.other = other;
    }

    @Override
    public int hashCode() {
        if (other == null) {
            return super.hashCode();
        }

        int hashCode = 1;
        if (field1 != null && field1.equals(other.field1)) {
            hashCode = 31 * hashCode + (field1 == null ? 0 : field1.hashCode());
        }
        if (field2 != null && field2.equals(other.field2)) {
            hashCode = 31 * hashCode + field2.hashCode();
        }
        if (hashCode == 1) {
            hashCode = super.hashCode();
        }
        return hashCode;
    }

    public boolean equals(Test other) {
        return (field1 != null && field1.equals(other.field1))
                || (field2 != null && field2.equals(other.field2));
    }

    public static void main(String[] args) {
        Test t1 = new Test();
        t1.setField1("a");
        t1.setField2(1);

        Test t2 = new Test();
        t2.setField1("a");
        t2.setField2(1);

        t1.setOther(t2);
        t2.setOther(t1);

        System.out.println("Equals: " + t1.equals(t2));
        System.out.println("Hash 1: " + t1.hashCode());
        System.out.println("Hash 2: " + t2.hashCode());

        t2.setField2(2);
        System.out.println("Equals: " + t1.equals(t2));
        System.out.println("Hash 1: " + t1.hashCode());
        System.out.println("Hash 2: " + t2.hashCode());

        t2.setField1("b");
        System.out.println("Equals: " + t1.equals(t2));
        System.out.println("Hash 1: " + t1.hashCode());
        System.out.println("Hash 2: " + t2.hashCode());
    }
}

Alternative solutions to tackle this are also welcome.

I would never mess with equals() & hashCode(). Simply implement them correctly for the objects. Write a custom comparator meeting your needs and use a collection that supports a custom comparator (eg TreeSet or TreeMap ) to perform your lookup.

Sample comparator:

public class SingleFieldMatchComparator implements Comparator<Key> {

  public int compare(Key key1, Key key2) {
    if (key1 == null) {
      if (key2 == null) {
        return 0;
      }
      else {
        return 1;
      }
    } else if (key2 == null) {
      return -1;
    }

    int result = ObjectUtils.compare(key1.getField1(), key2.getField1());
    if (result == 0) {
      return 0;
    }

    result = ObjectUtils.compare(key1.getField2(), key2.getField2());
    if (result == 0) {
      return 0;
    }

    return ObjectUtils.compare(key1.getField3(), key2.getField3());
  }

}

Note: above uses ObjectUtils to reduce the code a bit. Can be replaced with a private method if the dependency isn't worth it.

Sample program:

Map<Key,Key> myMap = new TreeMap<Key,Key>(new SingleFieldMatchComparator());
Key key = new Key(1, 2, 3);
myMap.put(key, key);
key = new Key(3, 1, 2);
myMap.put(key, key);

System.out.println(myMap.get(new Key(null, null, null)));
System.out.println(myMap.get(new Key(1, null, null)));
System.out.println(myMap.get(new Key(null, 2, null)));
System.out.println(myMap.get(new Key(null, null, 2)));
System.out.println(myMap.get(new Key(2, null, null)));

Output:

null
1,2,3
1,2,3
3,1,2
null

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