Лучшие практики относительно равных: перегружать или не перегружать? - PullRequest
26 голосов
/ 26 мая 2010

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

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

Effective Java 2nd Edition, пункт 36: последовательно используйте аннотацию Override , использует, по сути, тот же аргумент, чтобы рекомендовать, чтобы @Override использовалось последовательно.Конечно, этот совет хорош, поскольку, если бы мы попытались объявить @Override equals(Thing other) в первом фрагменте, наш дружелюбный маленький компилятор немедленно указал бы на нашу маленькую глупую ошибку, поскольку это перегрузка, а не переопределение.

* 1026Однако то, что в книге конкретно не освещено, является ли перегрузкой 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 ]

17 голосов
/ 26 мая 2010

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

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

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

8 голосов
/ 26 мая 2010

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

@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 .


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

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

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


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

4 голосов
/ 26 мая 2010

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

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

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

2 голосов
/ 26 мая 2010

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

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

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

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

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

1 голос
/ 12 октября 2018

Позвольте мне поделиться примером "глючного кода" с перегруженными равными:

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));
    }
}
1 голос
/ 02 июня 2010

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

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

1 голос
/ 26 мая 2010

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

...