Object equality (Java)

Each object in java has two methods to check object equality: it is Object.equals(Object other) and Object.hashCode(). Often developers override these methods so they can store instances of Object subclasses in hash-based collections e.g. HashMap, HashSet etc. I don’t like this design where each object can be compared for equality with any other object, but in this post I’ll not criticize it, rather I’ll try to demonstrate how to implement it correctly for object oriented code.

Protocol

First of all we need to understand the protocol of these methods, it’s not defined in method signature, but described in javadocs:

The equals method implements an equivalence relation on non-null object references:
It is reflexive: for any non-null reference value x, x.equals(x) should return true.
It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
It is transitive: for any non-null reference values x, y and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified. For any non-null reference value x, x.equals(null) should return false.
The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).
Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

It’s important to remember that all these requirements are handshake deals, java compiler will not be able to check that developers obey these arrangements.

Implementing

It’s very easy to implement equals() or hashCode() for DTOs which are very popular in java world, even the most popular IDE can generate these methods automatically. Also it’s not a problem to write it for final classes which don’t implement domain types.

But if you’re making object-oriented java module, then, most probably, you have interfaces for your domain objects and many implementations or decorators for them. For instance you may have User object:

interface User {
  /**
   * User id.
   */
  String uid();
  
  /**
   * User name.
   */
  String name();
}

and implementations:

  • User user = new SqlUser(database, id) - to find user in a database by id
  • User user = new RqUser(users, request) - to get current user from HTTP request
  • User user = users.user(id) - user by id from Users object

In all these cases we may have different class implementations of same object and we are not able to use User as a key in hash-based collection if we implement equals/hashCode as JDK tutorials suggested, because each class will check that another object has same class type as self: it’s required to be symmetric, because if we don’t do type checking we can get true result for x.equals(y), but not for y.equals(x) if x class implements equality check based on interface, but y class don’t do that (or event it uses Object.equals implementation).

So how to solve it? If we don’t want to ignore built-in collections (like HashMap or HashSet), but wants to decorate our objects and use different implementations of one interface we need to invent another approach for writing equals methods to satisfy JDK requirements, but do not brake OO code.

Decorators

I’ve found a solution which can help here: we can create the decorator for our domain object which will implement equals based on interface methods, not fields of the object. To begin we need to find identity method which will return always same value for one object instance and will be unique for different objects, this is required by equals rules to be consistent. For User object it will be uid() (user id) method, which is unique for different users and always the same for one user instance. We need to use this method in actual equals and hashCode implementations:

final class EqUser implements User {
    private final User origin;

    EqUser(final User origin) {
        this.origin = origin;
    }

    @Override
    public String uid() {
        return this.origin.uid();
    }
    @Override
    public String name() {
        return this.origin.name();
    }

    @Override
    public boolean equals(final Object obj) {
        final boolean same;
        if (obj instanceof EqUser) {
            final User other = (User) obj;
            same = Objects.equals(this.uid(), other.uid());
        } else {
            same = false;
        }
        return same;
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.origin.uid());
    }
}

as Object.equals protocol is based on “verbal arrangements”, our implementation also assumes that User.uid implements correctly equals and hashCode, it’s String in this case, so we can be sure that it’s true. Let’s check java equality requirements:

  • this implementation is “reflexive”: x.equals(x) == true because x.uid().equals(x.uid()) == true
  • it is “symmetric”: if x.equals(y) is true then y.equals(x) is true also, because EqUser accepts only EqUser implementations as other object, so it can be converted to if x.uid().equals(y.uid()) is true then y.uid().equals(x.uid()) is true also
  • it is “transitive”: if x.equals(y) && y.equals(z) then x.equals(z), because when x.equals(y) && y.equals(z) so x.uid().equals(y.uid()) && y.uid().equals(z.uid()) and x.uid().equals(z.uid()) what means that x.equals(z)
  • it is “consistent”: we assume that x.uid() is consistent

Example

And an example now. For instance we need to store user permissions as strings and grant them to some user but we can’t be assure what User implementation we might be handling with:

final class Permissions {
  final Map<User, Set<String>> map = new HashSet<>();

  /**
   * Check user has permission.
   */
  public boolean has(final User user,
    final String permission) {
    final EqUser key = new EqUser(user);
    return map.contains(key) &&
      map.get(key).contains(permission);
  }

  /**
   * Grant permission to the user.
   */
  public void grant(final User user,
    final String permission) {
    final EqUser key = new EqUser(user);
    final Set<String> set;
    if (!map.contains(key)) {
      set = new HashSet<>();
      map.put(key, set);
    } else {
      set = map.get(key);
    }
    set.add(permission);
 } 
}

so we can grant user permission with one type:

permissions.grant(
  new RqUser(users, request), "read"
); // grant 'read' permission to current user

and then check it with any other implementation:

SqlUsers users;
if (permissions.has(users.user(id), "read")) {
  return data.readAllBytes();
}
Written on August 10, 2018