Preferred Equals() Method Implementation

Go To StackoverFlow.com

3

This is a question about how to implement the equals method when I need to find instance of the object in a List given a value that one of the instances my have in their member.

I have an object where I've implemented equals:

class User {

    private String id;

    public User(id) {
        this.id = id;
    }

    public boolean equals(Object obj) {
        if (!(obj instanceof User)) {
            return false;
        }
        return ((User)obj).id.equals(this.id);
    }
}

Now if I want to find something in the List I would do something like this:

public function userExists(String id) {
        List<Users> users = getAllUsers(); 
        return users.contains(new User(id));
}

But perhaps this might be a better implementation?

class User {

    private String id;

    public boolean equals(Object obj) {
        if (!(obj instanceof User)) {
            return false;
        }
        if (obj instanceof String) {
            return ((String)obj).equals(this.id);
        }
        return ((User)obj).id.equals(this.id);
    }
}

With this instead:

public function userExists(String id) {
    List<Users> users = getAllUsers(); 
    return users.contains(id);
}
2012-04-04 19:10
by udeleng
Actually, I ran into maintaining a mess of code that had this idom of being "equals" to a String. Like all mis-steps it wasn't too bad at first, but as the Collections began to be used more heavily, so much workaround code was packed in that I would argue the second technique is a bug not a benefit - Edwin Buck 2012-04-04 19:23


6

Do not override equals for things that are not mathematically equal.

You might think it is a good idea to do

User bob = new User("Bob");
if (bob.equals("Bob")) {
  ...
}

but it rarely is. Do you want all of the equals observing code getting confused when Strings are "equal" to Users?

If you want a lookup method, write it

class User {

    private String id;

    public boolean equals(Object obj) {
        if (obj instanceof User) {
            User other = (User)obj;
            if (id.equals(other.id)) {
              return true;
            }
        }
        return false;
    }

    public String getId() {
        return id;
    }

}

Then the code elsewhere to maintain the "fast lookup" table.

Map<String, User> idTable = new HashMap<String, User>();
User bob = new User("Bob");
idTable.put(bob.getId(), bob);

public User findUser(String id) {
  return idTable.get(id);
}

Note that this doesn't mess around with the equals implementation, so now you can safely have Sets of Users, Lists of Users, etc. all without worrying if somehow a String will foul the works.

Now if you can't find a good place to maintain a Map of Users indexed by their id, you can always use the slower Iterator solution

List<User> users = new List<User>();
users.add(new User("Bob"));
users.add(new User("Steve"));
users.ass(new User("Ann"));

public User findUser(String id) {
  Iterator<User> index = users.iterator();
  while (index.hasNext()) {
    User user = index.next();
    if (id.equals(user.getId())) {
      return user;
    }
  }
  return null;
}
2012-04-04 19:17
by Edwin Buck
I generally agree with your answer, but the OP was overriding not overloadingKevin Welker 2012-04-07 23:23
@KevinWelker Agreed, and I'll update the answer too - Edwin Buck 2012-04-09 06:11


7

Doing it the second way is dangerous, because it breaks symmetric property of the equality.

Java expects implementations of equals() to be reflexive, symmetric, and transitive. Second implementation breaks symmetry: if you compare User to a String representing it's ID you'd get true, but if you compare the string to the user, you will get a false.

2012-04-04 19:15
by dasblinkenlight
I remember reading about symmetry in Sierra/Bates, but I guess I just glanced over it then. Now I know better. But, is creating a dummy User object to find a User in the list, given an ID as string, proper - udeleng 2012-04-04 19:27
@udeleng no, creating a dummy user to find a User in a list is not proper. Either search the list with an iterator, checking the field on each element, or store the users in a Map indexed by the id (as demonstrated in my example below). A proper implementation of user.equals(Object) would require that you already have the User you seek prior to searching it in the list - Edwin Buck 2012-04-04 19:35


2

First, your second implementation is functionally equivalent to the first, because an instance of String is not an instance of User, and the first return statement will short-circuit it before the check for a String happens.

What I mean is,

public boolean equals(Object obj) {
    if (!(obj instanceof User)) { // This will execute if obj is a String
        return false;
    }
    if (obj instanceof String) {
        // Never executes, because if obj is a String, we already
        // returned false above
        return ((String)obj).equals(this.id);
    }
    return ((User)obj).id.equals(this.id);
}

So for the remainder of the answer, I will assume that what was meant is

public boolean equals(Object obj) {
    if ( obj == null ) return false; // Add a null check for good measure.
    if (!(obj instanceof User)) {
        if (obj instanceof String) {
            // Now we're checking for a String only if it isn't a User.
            return ((String)obj).equals(this.id);
        }
        return false;
    }
    return ((User)obj).id.equals(this.id);
}

Now we come to the actual problem.

Implementing an equals that returns true for a User-to-String comparison is bad practice because equals is expected to be symmetric ( a.equals(b) if and only if b.equals(a) ). And since a String can never equal a User, a User should never equal a String.

2012-04-04 19:16
by trutheality
Thanks, I see my mistake in regards to the short circuit. I just typed the code here while posting. I get the principle of symmetry now. Take a look at my question in response to @dasblinkenlight - udeleng 2012-04-04 19:30
@trutheality Honestly, I did miss your point entirely. I apologize, my assumption was that he wrote his second equals implementation semi-correctly, and not that a bug on top of a conceptual bug actually would save the day. I guess I'd better slow down my reading a bit, or drink a cup of coffee - Edwin Buck 2012-04-04 19:50


1

Don't forget to override Object.hashCode() if you override Object.equals().

Equal objects must have equal hash codes and you may run into probs with Collections if you do not obey the contract.

2012-04-04 19:41
by horbags


0

Second choice is very bad. That means you are allowing a user to pass something other than a User into User.equals() (i.e., really Object.equals() when you are not really trying to compare a String with the User. So you are in essence violating the contract for what equals is supposed to do.

Also, neither of your answers handle null-checking.

2012-04-04 19:19
by Kevin Welker
Ads