How to override the (final) equals method in java enums?

12,747

Solution 1

You can NOT @Override a final method (§8.4.3.3); this much is clear. enum types (§8.9) are treated very specially in Java, which is why the equals is final (also clone, hashCode, etc.) It's simply not possible to @Override the equals method of an enum, nor would you really want to in a more typical usage scenario.

HOWEVER, looking at the big picture, it looks like you are trying to follow the pattern recommended in Effective Java 2nd Edition, Item 34: Emulate extensible enums with interfaces (see the language guide for more information about enum):

You have defined this interface (now documented explicitly for expected equals behavior):

public interface Group implements Group {
    public Point[] getCoordinates();

    /*
     * Compares the specified object with this Group for equality. Returns true
     * if and only if the specified object is also a Group with exactly the same
     * coordinates
     */
    @Override public boolean equals(Object o);
}

It is perfectly acceptable for an interface to define how equals method for implementors should behave, of course. This is exactly the case with, e.g. List.equals. An empty LinkedList is equals to an empty ArrayList and vice versa, because that's what the interface mandates.

In your case, you've chosen to implement some Group as enum. Unfortunately you now can't implement equals as per the specification, since it's final and you can't @Override it. However, since the objective is to comply to the Group type, you can use decorator pattern by having a ForwardingGroup as follows:

public class ForwardingGroup implements Group {
   final Group delegate;
   public ForwardingGroup(Group delegate) { this.delegate = delegate; }

   @Override public Point[] getCoordinates() {
       return delegate.getCoordinates();
   }
   @Override public boolean equals(Object o) {
       return ....; // insert your equals logic here!
   }
}

Now, instead of using your enum constants directly as Group, you wrap them in an instance of a ForwardingGroup. Now this Group object will have the desired equals behavior, as specified by the interface.

That is, instead of:

// before: using enum directly, equals doesn't behave as expected
Group g = BasicGroup.A;

You now have something like:

// after: using decorated enum constants for proper equals behavior
Group g = new ForwardingGroup(BasicGroup.A);

Additional notes

The fact that enum BasicGroups implements Group, even though it does not itself follow the specification of Group.equals, should be very clearly documented. Users must be warned that constants must be e.g. wrapped inside a ForwardingGroup for proper equals behavior.

Note also that you can cache instances of ForwardingGroup, one for each enum constants. This will help reduce the number of objects created. As per Effective Java 2nd Edition, Item 1: Consider static factory methods instead of constructors, you may consider having ForwardingGroup define a static getInstance(Group g) method instead of a constructor, allowing it to return cached instances.

I'm assuming that Group is an immutable type (Effective Java 2nd Edition, Item 15: Minimize mutability), or else you probably shouldn't implement it with enum in the first place. Given that, consider Effective Java 2nd Edition, Item 25: Prefer lists to arrays. You may choose to have getCoordinates() return a List<Point> instead of Point[]. You can use Collections.unmodifiableList (another decorator!), which will make the returned List immutable. By contrast, since arrays are mutable, you'd be forced to perform defensive copying when returning a Point[].

See also

Solution 2

It's not possible to do this in Java. (The sole purpose of the final keyword when it comes to methods, is to prevent overriding!)

equals and a few other methods on Enums are final, so you can't change the behavior of them. (And you shouldn't :) Here is my answer to a related question:


The intuition of clients that deal with enum constants is that two constants are equal if and only if they are the same constant. Thus any other implementation than return this == other would be counterintuitive and error prone.

Same reasoning applies to hashCode(), clone(), compareTo(Object), name(), ordinal(), and getDeclaringClass().

The JLS does not motivate the choice of making it final, but mentions equals in the context of enums here. Snippet:

The equals method in Enum is a final method that merely invokes super.equals on its argument and returns the result, thus performing an identity comparison.

Solution 3

Equality is quite elusive. Different contexts require different equality relations. By having equals() method on Object, Java imposes an "intrinsic" equality, and APIs, like Set, depend on it.

Meanwhile, ordering isn't considered "intrinsic", two objects can be ordered differently in different contexts, and APIs usually allow us to supply a comprator, i.e., a custom ordering relation.

This is interesting. In math terms, equality, like order, is just a relation, and there can be different equality relations. The concept of "intrinsic equality" isn't holy.

so let's have an Equal-ator too, and change APIs to accept custom equality relations:

interface Equalator
    boolean equal(a, b)

public HashSet( Equalator equalator )

Actually, we can build wrappers around current collection APIs, and add this feature of new equality.

This might answer your question. Why do you have a dependency on equals() in the first place? And can you remove that, and depend instead on "equalator"? Then you are set.

Solution 4

You can solve this by calling your method hasSameCoordinatesAs, or similar, rather than equals.

equals for enums is defined in the language specification, so you can't hope to redefine it.

Share:
12,747
neXus
Author by

neXus

Updated on June 03, 2022

Comments

  • neXus
    neXus almost 2 years

    I have a problem with overriding the equals method in an Enum to make it compatible with other classes. The Enum implements an interface and the idea is that all implementations of this interface can be tested for equality, regardless of their type. For Example:

    public interface Group {
        public Point[] getCoordinates();
    }
    
    public enum BasicGroups implements Group {
        a,b,c; // simplified, they actually have constructors
        // + fields and methods
    }
    
    public class OtherGroup implements Group {
        // fields and methods
    }
    

    If both a BasicGroup and an OtherGroup have the same coordinates (in arbitrary order) then the equals method should return true.

    No problem when performing myOtherGroup.equals(BasicGroup.a) but since the equals method in Enums is final, I can't override them.

    Is there some way to work around this? Like when testing on another BasicGroup the default equals method (reference equality) is used and when testing other classes my own implementation is used. And how do I make sure that java doesn't use the wrong one when I do BasicGroup.a.equals(myOtherGroup)?