Лучшие практики относительно равных: перегружать или не перегружать?
Рассмотрим следующий фрагмент:
import java.util.*;
public class EqualsOverload {
public static void main(String[] args) {
class Thing {
final int x;
Thing(int x) { this.x = x; }
public int hashCode() { return x; }
public boolean equals(Thing other) { return this.x == other.x; }
}
List<Thing> myThings = Arrays.asList(new Thing(42));
System.out.println(myThings.contains(new Thing(42))); // prints "false"
}
}
Обратите внимание, что contains
возвращается false
!!! Кажется, мы потеряли наши вещи!
Ошибка, конечно, в том, что мы случайно перегружены, а не переопределены, Object.equals(Object)
, Если бы мы написали class Thing
вместо этого, то contains
возвращается true
как и ожидалось.
class Thing {
final int x;
Thing(int x) { this.x = x; }
public int hashCode() { return x; }
@Override public boolean equals(Object o) {
return (o instanceof Thing) && (this.x == ((Thing) o).x);
}
}
Эффективная Java, 2-е издание, пункт 36: последовательно используйте аннотацию Override, в основном использует один и тот же аргумент, чтобы рекомендовать @Override
следует использовать последовательно. Этот совет, конечно, хорош, если бы мы попытались @Override equals(Thing other)
в первом фрагменте наш дружелюбный маленький компилятор немедленно указал бы на нашу маленькую глупую ошибку, поскольку это перегрузка, а не переопределение.
Однако то, что книга конкретно не освещает, - это перегрузка equals
это хорошая идея для начала. По сути, есть 3 ситуации:
- Только перегрузка, без переопределения - ПОЧТИ НЕОБХОДИМО НЕПРАВИЛЬНО!
- Это по сути первый фрагмент выше
- Только переопределение (без перегрузки) - один из способов исправить
- По сути это второй фрагмент выше
- Перегрузка и переопределение комбо - еще один способ исправить
Третья ситуация иллюстрируется следующим фрагментом:
class Thing {
final int x;
Thing(int x) { this.x = x; }
public int hashCode() { return x; }
public boolean equals(Thing other) { return this.x == other.x; }
@Override public boolean equals(Object o) {
return (o instanceof Thing) && (this.equals((Thing) o));
}
}
Здесь, хотя у нас сейчас есть 2 equals
метод, есть еще одна логика равенства, и она находится в перегрузке. @Override
просто делегирует перегрузку.
Итак, вопросы:
- Каковы плюсы и минусы "только переопределения" против "перегрузки и комбинации переопределения"?
- Есть ли оправдание перегрузке?
equals
или это почти наверняка плохая практика?
7 ответов
Я бы не увидел случая перегрузки equals, за исключением того, что он более подвержен ошибкам и сложнее поддерживать, особенно при использовании наследования.
Здесь может быть чрезвычайно сложно поддерживать рефлексивность, симметрию и транзитивность или обнаруживать их несоответствия, потому что вы всегда должны знать о действительном методе equals, который вызывается. Просто подумайте о большой иерархии наследования и только о некоторых типах, реализующих собственный метод перегрузки.
Так что я бы сказал, просто не делай этого.
Если у вас есть одно поле, как в вашем примере, я думаю,
@Override public boolean equals(Object o) {
return (o instanceof Thing) && (this.x == ((Thing) o).x);
}
это путь Все остальное было бы слишком сложным. Но если вы добавите поле (и не хотите передавать рекомендации по 80 столбцам по солнцу), оно будет выглядеть примерно так:
@Override public boolean equals(Object o) {
if (!(o instanceof Thing))
return false;
Thing t = (Thing) o;
return this.x == t.x && this.y == t.y;
}
который, я думаю, немного страшнее, чем
public boolean equals(Thing o) {
return this.x == o.x && this.y == o.y;
}
@Override public boolean equals(Object o) {
// note that you don't need this.equals().
return (o instanceof Thing) && equals((Thing) o);
}
Таким образом, мое практическое правило в основном, если нужно привести его более одного раза в override-only, используйте override- / overload-combo.
Второстепенный аспект - это накладные расходы времени выполнения. Как программирование производительности Java, Часть 2: Стоимость кастинга объясняет:
Операции с понижением (также называемые сужающими преобразованиями в спецификации языка Java) преобразуют ссылку на класс предка в ссылку на подкласс. Эта операция приведения создает накладные расходы на выполнение, поскольку Java требует, чтобы приведение было проверено во время выполнения, чтобы убедиться, что оно допустимо.
Используя комбо overload- / override-combo, компилятору в некоторых случаях (не всем!) Удастся обойтись без downcast.
Чтобы прокомментировать точку зрения @Snehal, то, что раскрытие обоих методов может сбить с толку разработчиков на стороне клиента: другим вариантом было бы позволить перегруженным равным быть закрытыми. Элегантность сохраняется, метод может использоваться внутри, а интерфейс на стороне клиента выглядит как положено.
Проблемы с перегруженными равными:
Все коллекции предоставлены Java, т.е. Set, List, Map используют переопределенный метод для сравнения двух объектов. Таким образом, даже если вы перегружаете метод equals, он не решает цель сравнения двух объектов. Кроме того, если вы просто перегрузите и реализуете метод хэш-кода, это приведет к ошибочному поведению
Если у вас есть как перегруженные, так и переопределенные методы equals и оба этих метода выставлены, вы запутаете разработчиков на стороне клиента. По соглашению люди считают, что вы переопределяете класс Object
В книге есть несколько пунктов, которые охватывают это. (Это не передо мной, поэтому я буду ссылаться на предметы, как я их помню)
Есть пример, использующий equals(..)
где сказано, что перегрузка не должна использоваться, а если используется - ее следует использовать с осторожностью. Пункт о дизайне методов предостерегает от перегрузки методов с одинаковым количеством аргументов. Так что - нет, не перегружайте equals(..)
Обновление: из "Эффективной Java" (стр.44)
Допустимо предоставлять такой "строго типизированный" метод равенства в дополнение к обычному, если оба метода возвращают один и тот же результат, но нет веских причин для этого.
Таким образом, это не запрещено, но это добавляет сложности вашему классу, но не приносит никакой выгоды.
Я использую этот подход с комбинированием переопределения и перегрузки в моих проектах, потому что код выглядит немного чище. У меня не было проблем с этим подходом до сих пор.
Позвольте мне поделиться примером "глючного кода" с перегруженными равными:
class A{
private int val;
public A(int i){
this.val = i;
}
public boolean equals(A a){
return a.val == this.val;
}
@Override
public int hashCode() {
return Objects.hashCode(this.val);
}
}
public class TestOverloadEquals {
public static void main(String[] args){
A a1 = new A(1), a2 = new A(2);
List<A> list = new ArrayList<>();
list.add(a1);
list.add(a2);
A a3 = new A(1);
System.out.println(list.contains(a3));
}
}
Я могу вспомнить очень простой пример, где это не будет работать должным образом, и почему вы никогда не должны делать это:
class A {
private int x;
public A(int x) {
this.x = x;
}
public boolean equals(A other) {
return this.x == other.x;
}
@Override
public boolean equals(Object other) {
return (other instanceof A) && equals((A) other);
}
}
class B extends A{
private int y;
public B(int x, int y) {
super(x);
this.y = y;
}
public boolean equals(B other) {
return this.equals((A)other) && this.y == other.y;
}
@Override
public boolean equals(Object other) {
return (other instanceof B) && equals((B) other);
}
}
public class Test {
public static void main(String[] args) {
A a = new B(1,1);
B b1 = new B(1,1);
B b2 = new B(1,2);
// This obviously returns false
System.out.println(b1.equals(b2));
// What should this return? true!
System.out.println(a.equals(b2));
// And this? Also true!
System.out.println(b2.equals(a));
}
}
В этом тесте вы ясно увидите, что перегруженный метод приносит больше вреда, чем пользы при использовании наследования. В обоих неправильных случаях более общий equals(A a)
называется, потому что компилятор Java знает только, что a
имеет тип A
и этот объект не перегружен equals(B b)
метод.
Задумка: сделать перегруженным equals
Частное лицо решает эту проблему, но действительно ли это вас завоевывает? Он только добавляет дополнительный метод, который может быть вызван только путем приведения.
Поскольку этому вопросу 10 лет, и первоначальный автор, вероятно, больше не интересуется ответом, я добавлю некоторую информацию, которая может быть полезна разработчикам, ищущим ответ в настоящее время.
Для разработчиков Android в Android Studio есть шаблон, который всплывает при попытке перегрузить оператор равенства. Он также генерирует правильный метод hashCode, и результирующий переопределенный
equals
метод будет выглядеть так:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Thing thing = (Thing) o;
return x.equals(thing.x);
}