Лучшие практики относительно равняются: перегружаться или не перегружаться?

Рассмотрите следующий отрывок:

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 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 equals(Thing other) в первом отрывке наш дружественный небольшой компилятор сразу указал бы на нашу глупую небольшую ошибку, так как это - перегрузка, не переопределение.

То, что книга конкретно не касается, однако, то, ли перегрузка equals хорошая идея для начала. По существу существует 3 ситуации:

  • Перегрузка только, никакое переопределение - ПОЧТИ НАВЕРНЯКА НЕПРАВИЛЬНО!
    • Это - по существу первый отрывок выше
  • Переопределите только (никакая перегрузка) - один способ зафиксировать
    • Это - по существу второй отрывок выше
  • Перегрузка и комбинация переопределения - другой способ зафиксировать

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, или это - почти наверняка плохая практика?

26
задан polygenelubricants 26 May 2010 в 06:23
поделиться

6 ответов

Я не вижу доводов в пользу перегрузки equals, за исключением того, что это более подвержено ошибкам и сложнее поддерживать, особенно при использовании наследования.

Здесь может быть чрезвычайно трудно поддерживать рефлексивность, симметрию и транзитивность или обнаружить их несоответствия, потому что вы всегда должны знать о фактическом методе equals, который вызывается. Представьте себе большую иерархию наследования и только некоторые из типов, реализующих свой собственный метод перегрузки.

Поэтому я бы сказал просто не делать этого.

17
ответ дан 28 November 2019 в 07:42
поделиться

Я использую этот подход с комбинацией override и overload в своих проектах, потому что код выглядит немного чище. До сих пор у меня не было проблем с этим подходом.

1
ответ дан 28 November 2019 в 07:42
поделиться

В книге есть несколько статей, посвященных этому вопросу. (Это не передо мной, поэтому я буду ссылаться на элементы так, как я их помню)

Есть пример, в котором точно используется equals (..) , где сказано, что перегрузка не должна использоваться , а в случае использования - с осторожностью. Пункт о конструкции метода предостерегает от перегрузки методов с таким же количеством аргументов. Итак - нет, не перегружайте equals (..)

Обновление: Из «Эффективной Java» (стр.44)

Допустимо предоставить такой «строго типизированный» метод equals в дополнение к обычному, если два метода возвращают один и тот же результат, но нет веских причин для этого.

Таким образом, это не запрещено, но это добавляет сложности вашему классу, не прибавляя при этом никакого выигрыша.

2
ответ дан 28 November 2019 в 07:42
поделиться

Проблемы с перегруженными равными:

  • Все коллекции, предоставляемые Java, т.е. Set, List, Map используют переопределенный метод для сравнения двух объектов. Таким образом, даже если вы перегрузите метод equals, он не решит задачу сравнения двух объектов. Кроме того, если вы просто перегрузите и реализуете метод хэш-кода, это приведет к ошибочному поведению

  • . Если у вас есть как перегруженные, так и переопределенные методы equals и использование обоих этих методов, вы запутаете разработчиков на стороне клиента. По соглашению люди считают, что вы переопределяете класс Object

4
ответ дан 28 November 2019 в 07:42
поделиться

Если у вас есть одно-единственное поле, как в вашем примере, я думаю, что

@Override public boolean equals(Object o) {
    return (o instanceof Thing) && (this.x == ((Thing) o).x);
}

- это то, что вам нужно. Все остальное было бы слишком сложным imo. Но если вы добавите поле (и не хотите передавать рекомендацию Sun из 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 требует, чтобы приведение проверялось во время выполнения, чтобы убедиться, что оно действительное.

Используя комбинацию перегрузки / переопределения , компилятор в некоторых случаях (не во всех!) Сможет обойтись без понижающего преобразования.


Чтобы прокомментировать точку @Snehal, раскрытие обоих методов может сбить с толку разработчиков на стороне клиента: Другой вариант - позволить перегруженным равным статьям быть приватными. Элегантность сохраняется, метод можно использовать внутри, а интерфейс на стороне клиента выглядит так, как ожидалось.

8
ответ дан 28 November 2019 в 07:42
поделиться

Я могу вспомнить очень простой пример, где это не будет работать правильно и почему вы никогда не должны этого делать:

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 частным действительно решает эту проблему, но что это дает? Это только добавляет дополнительный метод, который может быть вызван только путем приведения.

0
ответ дан 28 November 2019 в 07:42
поделиться
Другие вопросы по тегам:

Похожие вопросы: