Как переопределить метод (final) equals в java-перечислениях?

У меня проблема с переопределением метода equals в Enum, чтобы сделать его совместимым с другими классами. Enum реализует интерфейс, и идея состоит в том, что все реализации этого интерфейса могут быть проверены на соответствие, независимо от их типа. Например:

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
}

Если оба a BasicGroup и a OtherGroup имеют одинаковые координаты (в произвольном порядке), то метод equals должен возвращать true.

Нет проблем при выполнении myOtherGroup.equals(BasicGroup.a), но поскольку метод equals в Enums является окончательным, я не могу их переопределить.

Есть ли способ обойти это? Как и при тестировании в другой BasicGroup используется метод equals по умолчанию (ссылочное равенство), а при тестировании других классов используется моя собственная реализация. И как я могу убедиться, что java не использует неправильный, когда я делаю BasicGroup.a.equals(myOtherGroup)?

Ответы

Ответ 1

Вы можете НЕ @Override a final метод (§8.4.3.3); это ясно. enum типы (§8.9) обрабатываются очень специфично на Java, поэтому equals есть final (также clone, hashCode и т.д.). Это просто невозможно @Override метод equals для enum, и вы бы не захотели в более типичном сценарии использования.

ОДНАКО, глядя на общую картину, похоже, вы пытаетесь следовать шаблону, рекомендованному в Effective Java 2nd Edition, пункт 34: Эмулировать расширяемые перечисления с интерфейсами (см. руководство по языку для получения дополнительной информации о enum):

Вы определили этот interface (теперь явно зарегистрированный для ожидаемого поведения equals):

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);
}

Для interface вполне приемлемо определить способ equals для разработчиков, который должен вести себя, конечно. Это как раз в случае, например, List.equals. Пустой LinkedList равен equals для пустого ArrayList и наоборот, потому что это означает, что мандат interface.

В вашем случае вы выбрали вариант Group как enum. К сожалению, теперь вы не можете реализовать equals согласно спецификации, так как это final, и вы не можете @Override его. Однако, поскольку целью является соответствие типу Group, вы можете использовать шаблон декоратора, указав ForwardingGroup следующим образом:

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!
   }
}

Теперь вместо использования констант enum непосредственно как Group, вы завершаете их в экземпляр ForwardingGroup. Теперь этот объект Group будет иметь желаемое поведение equals, как указано interface.

То есть вместо:

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

Теперь у вас есть что-то вроде:

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

Дополнительные примечания

Тот факт, что enum BasicGroups implements Group, даже если он сам не соответствует спецификации Group.equals, должен быть очень четко документирован. Пользователи должны быть предупреждены о том, что константы должны быть, например, завернутый внутри a ForwardingGroup для правильного поведения equals.

Обратите внимание, что вы можете кэшировать экземпляры ForwardingGroup, по одному для каждой константы enum. Это поможет уменьшить количество созданных объектов. В соответствии с эффективным Java 2nd Edition, пункт 1: рассмотрите статические методы factory вместо конструкторов, вы можете подумать о том, чтобы ForwardingGroup определить метод static getInstance(Group g) вместо конструктора, позволяя ему возвращать кэшированные экземпляры.

Я предполагаю, что Group является неизменным типом (эффективное Java 2nd Edition, пункт 15: минимизация изменчивости), иначе вы, вероятно, не должны реализовывать его с enum в первую очередь. Учитывая это, рассмотрим Эффективное Java 2nd Edition, пункт 25: Предпочтительные списки для массивов. Вы можете выбрать getCoordinates() вернуть List<Point> вместо Point[]. Вы можете использовать Collections.unmodifiableList (еще один декоратор!), Который сделает возвращаемый List неизменяемым. Напротив, поскольку массивы изменяемы, вы должны будете выполнить защитное копирование при возврате Point[].

См. также

Ответ 2

Это невозможно сделать в Java. (Единственная цель последнего ключевого слова, когда дело касается методов, состоит в том, чтобы предотвратить переопределение!)

equals и несколько других методов в Enums являются окончательными, поэтому вы не можете изменить их поведение. (И вы не должны:) Вот мой ответ на вопрос :


Интуиция клиентов, которые имеют дело с константами перечисления, состоит в том, что две константы equal тогда и только тогда, когда они являются одной и той же константой. Таким образом, любая другая реализация, чем return this == other, была бы противоречивой и подверженной ошибкам.

То же рассуждение относится к hashCode(), clone(), compareTo(Object), name(), ordinal() и getDeclaringClass().

JLS не мотивирует выбор сделать его окончательным, но упоминания равны в контексте перечислений здесь. Отрывок:

Метод equals в Enum является окончательным методом, который просто вызывает super.equals в его аргументе и возвращает результат, таким образом выполняя сравнение идентичности.

Ответ 3

Вы можете решить это, вызвав ваш метод hasSameCoordinatesAs или похожий, а не равный.

equals для перечислений определяется в спецификации языка, поэтому вы не можете надеяться переопределить его.

Ответ 4

Равенство довольно неуловимо. В разных контекстах требуются разные отношения равенства. Имея метод equals() в Object, Java накладывает "внутреннее" равенство, и от него зависят API, например Set.

Между тем, упорядочение не считается "внутренним", два объекта могут быть упорядочены по-разному в разных контекстах, и API обычно позволяют нам предоставлять составной элемент, т.е. настраиваемое отношение упорядочения.

Это интересно. В математическом выражении равенство, как и порядок, является просто отношением, и могут быть разные отношения равенства. Понятие "внутреннее равенство" не является святым.

так что давайте также иметь Equal-ator и изменим API для принятия пользовательских отношений равенства:

interface Equalator
    boolean equal(a, b)

public HashSet( Equalator equalator )

На самом деле, мы можем создавать оболочки вокруг текущих API-интерфейсов коллекции и добавлять эту особенность нового равенства.

Это может ответить на ваш вопрос. Почему у вас есть зависимость от equals() в первую очередь? И можете ли вы удалить это и вместо этого зависеть от "эквалайзера"? Затем вы устанавливаете.