简体   繁体   中英

java.util.AbstractMap.equals() : what is it for try/catch?

While writing my own custom MultiHashMap, multiTreeMap etc. classes (yes, I know these are available in Guava Library, but I needed to provide some different functionalities, so I completely rewrote these from scratch) I came across the need to write an equals() method that could compare any MultiMap, returning true if and only if the entrySet's of two MultiMaps are equivalent (same key-value mappings, regardless of the order).

As I hold the multi-values in an ordinary Map I compared my own method with API method java.util.AbstractMap.equals(), and they turned out to be pretty similar, except that I didn't use any try/catch (Java 7):

public boolean equals(Object o) {
    if (o == this)
        return true;

    if (!(o instanceof Map))
        return false;
    Map<K,V> m = (Map<K,V>) o;
    if (m.size() != size())
        return false;

    try {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext()) {
            Entry<K,V> e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            if (value == null) {
                if (!(m.get(key)==null && m.containsKey(key)))
                    return false;
            } else {
                if (!value.equals(m.get(key)))
                    return false;
            }
        }
    } catch (ClassCastException unused) {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }

    return true;
}

The caught exceptions are RuntimeException's and beside that I can't really figure out under which circumstances they may occur.

Any hint ?

They use catching exceptions to make the equals() code shorter. I don't think it's a good practice but it works. They replace many if -checks by catching the exceptions.

Have a look at an example of auto-generated equals() method by Eclipse:

public class Person {
    final private String firstName;
    final private String lastName;
    ...
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        Person other = (Person) obj;
        if (firstName == null) {
            if (other.firstName != null) {
                return false;
            }
        }
        else if (!firstName.equals(other.firstName)) {
            return false;
        }
        if (lastName == null) {
            if (other.lastName != null) {
                return false;
            }
        }
        else if (!lastName.equals(other.lastName)) {
            return false;
        }
        return true;
    }
}

That's a correct way of implementing equals() to fully fulfill its contract . Now notice that in all the cases when some test for a proper type or for a null fails, the equals() method returs false . So the idea in the code you provided is to ommit all the checks and just catch the exception. Something like this:

@Override
public boolean equals(Object obj) {
    try {
        // Ommit any type-checks before type-casting
        // and replace them with catching ClassCastException:
        final Person other = (Person) obj;
        // Ommit any null-checks before using the references
        // and replace them with catching NullPointerException:
        if (firstName.equals(other.firstName)
                && lastName.equals(other.lastName)) {
            return true;
        }
    }
    catch (ClassCastException | NullPointerException unused) {
        // swallow the exception as it is not an error here
    }
    return false;
}

As you may see, the code does the same but is significantly shorter. However, it is usually considered as bad practice. Still I must admit that the code is better readable :)

The reason why it is considered as bad practice is very well described in Joshua Bloch's Effective Java , Item 57: Use exceptions only for exceptional conditions:

Exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow.

I think, the catch is meant to catch erroneous implementations of the equals method in the V type. The call value.equals(m.get(key)) may throw ClassCastException and or NullPointException , when equals is implemented naively in V .

A bad implementation of equals in an actual V -Parameter type, that would be catched nicely:

class Whatever {
  private int attr;
  /* ... */
  @Override public boolean equals(Object o) {
    Whatever w= (Whatever)o; // possible ClassCastException
    return (this.attr == w.attr); // possible NullPointerException
  }
}

The answer appears to be pretty simple: because Map.containsKey method may throw both of these exceptions.

From documentation of Map interface:

/**
 * ....
 * @throws ClassCastException if the key is of an inappropriate type for
 *         this map (optional)
 * @throws NullPointerException if the specified key is null and this map
 *         does not permit null keys (optional)
 */
boolean containsKey(Object key);

Although containsKey implementation in AbstractMap doesn't actually throw these exceptions, some custom implementations may eventually do this. And the most reliable way to handle these exceptions is to wrap containsKey in try-catch block.

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