Подтвердить что ты не робот

Рекомендации относительно равных: перегрузка или перегрузка?

Рассмотрим следующий фрагмент:

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 2nd Edition, пункт 36: последовательно использовать аннотацию Override, использует по существу тот же аргумент, чтобы рекомендовать использовать @Override последовательно. Разумеется, этот совет хорош, потому что если бы мы попытались объявить @Override equals(Thing other) в первом фрагменте, наш дружелюбный маленький компилятор немедленно укажет на нашу глупую небольшую ошибку, поскольку это перегрузка, а не переопределение.

Однако в книге особо не говорится о том, стоит ли начинать перегрузку equals. По существу, есть три ситуации:

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

Третья ситуация иллюстрируется следующим фрагментом:

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

Ответ 1

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

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

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

Ответ 2

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

@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/overload-combo.


A вторичный аспект - это служебные данные времени выполнения. Как Программирование производительности Java, часть 2: Стоимость литья объясняет:

Операции Downcast (также называемые сужением конверсий в Спецификации языка Java) преобразуют ссылку класса предка на ссылку подкласса. Эта операция литья создает издержки выполнения, так как Java требует, чтобы приведение было проверено во время выполнения, чтобы убедиться, что оно действительно.

Используя команду overload-/override-combo, компилятор будет в некоторых случаях (не все!) обойтись без downcast.


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

Ответ 3

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

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

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

Ответ 4

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

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

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

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

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

Ответ 5

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

Ответ 6

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

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