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 returntrue
. It is symmetric: for any non-null reference valuesx
andy
,x.equals(y)
should returntrue
if and only ify.equals(x)
returnstrue
. It is transitive: for any non-null reference valuesx
,y
andz
, ifx.equals(y)
returns true andy.equals(z)
returnstrue
, thenx.equals(z)
should returntrue
. It is consistent: for any non-null reference valuesx
andy
, multiple invocations ofx.equals(y)
consistently returntrue
or consistently returnfalse
, provided no information used in equals comparisons on the objects is modified. For any non-null reference valuex
,x.equals(null)
should returnfalse
. Theequals
method for classObject
implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference valuesx
andy
, this method returnstrue
if and only ifx
andy
refer to the same object (x == y
has the valuetrue
). Note that it is generally necessary to override thehashCode
method whenever this method is overridden, so as to maintain the general contract for thehashCode
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 idUser user = new RqUser(users, request)
- to get current user from HTTP requestUser user = users.user(id)
- user by id fromUsers
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
becausex.uid().equals(x.uid()) == true
- it is “symmetric”: if
x.equals(y)
istrue
theny.equals(x)
istrue
also, becauseEqUser
accepts onlyEqUser
implementations as other object, so it can be converted to ifx.uid().equals(y.uid())
istrue
theny.uid().equals(x.uid())
istrue
also - it is “transitive”: if
x.equals(y) && y.equals(z)
thenx.equals(z)
, because whenx.equals(y) && y.equals(z)
sox.uid().equals(y.uid()) && y.uid().equals(z.uid())
andx.uid().equals(z.uid())
what means thatx.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();
}