Способ расширить инстанцируемый класс и добавить компонент значения, сохраняя контракт равных

Эффективная Java от Джошуа Блоха утверждает:

Нет никакого способа расширить инстанцируемый класс и добавить компонент значения, сохраняя equals контракт, если вы не готовы отказаться от преимуществ объектно-ориентированной абстракции.

Эффективная Java приводит несколько примеров, которые нарушают принцип симметрии, транзитивности или подстановки Лискова. Я полагаю, вы прочитали статью. Я не думаю, что я должен публиковать весь предмет здесь. Однако я нашел решение, которое подчиняется контракту:

Point:

public class Point{
    private final int x;
    private final int y;
    public Point(int x, int y){
        this.x = x;
        this.y = y;
    }
    protected boolean equalsImpl(Point p){
        return p.x == x && p.y == y;
    }
    @Override
    public final boolean equals(Object o) {
        if(!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return equalsImpl(p) && p.equalsImpl(this);
    }
    // Remainder omitted ...
}

ColorPoint:

public class ColorPoint extends Point{
    private java.awt.Color color;
    public ColorPoint(int x, int y, java.awt.Color color){
        super(x, y);
        this.color = color;
    }
    @Override
    protected boolean equalsImpl(Point p){
        return (p instanceof ColorPoint) &&
                super.equalsImpl(p) &&
                ((ColorPoint)p).color == color;
    }
    // Remainder omitted ...
}

Я знаю, что в этом случае следует использовать композицию, но мне интересно , действительно ли мое решение подчиняется equals контракт. Если да, то, что Effective Java заявляет неправильно?

4 ответа

Решение

При такой реализации нет ColorPoint равно Pointтак что это нарушает принцип подстановки Лискова. Это делает честь equals контракт хотя.

Обновление: LSP сломан или нет, зависит от контрактов, которые Point класс имеет. Если у вас есть контракт, в котором говорится, что "две точки с одинаковыми координатами X и Y равны" или "вы можете использовать HashSet "Устранить точки с дублирующими координатами", он нарушается. Если такого контракта не существует, LSP не нарушается.

С другой стороны, если нет ColorPoint равно PointИмеет ли смысл, что ColorPoint это Point?

Нет проблем с тем, чтобы инстанцируемый класс добавил компонент значения, соблюдая оба equals класс и LSP, если контракт базового класса позволит производным классам сделать это, и если equals Метод базового класса кодируется таким образом, чтобы либо

  1. Проверьте точный тип чего-либо, с чем это сравнивается, и сообщите false если точные типы не совпадают.

  2. Определите метод, который он использует как часть процесса проверки на равенство и который производный класс может переопределять по мере необходимости.

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

Использование подхода № 2 немного сложнее, но простым подходом может быть переопределение equals что-то назвать protected Метод equals2, как это:

@override boolean equals(Object other)
{
  if (!(other instanceof MyType)) return false;
  MyType otherAsMyType = (MyType)other;
  return this.equals1(OtherAsMyType) && OtherAsMyType.equals1(this) &&
         this.equals2(OtherAsMyType) && OtherAsMyType.equals2(this);   
}

equals1 метод производного типа вернул бы false когда прошло что-то, что не было получено из него, даже если этот другой объект ничего не знал о производной части; разделение сравнения на разные части гарантирует, что если тип может быстро определить, что объект другого типа не может быть равен ему, код в первом классе не будет тратить время на проверку частей, которые, по-видимому, могут быть равны.

LSP не требуется equals контракт.

Возможно, этот вопрос является результатом ненужного включения LSP в тему equals Реализации. Сгенерированное стандартное затмение equals реализация уже использует super.equals (с использованием наследования) и является гораздо более правильным и менее сложным, чем предлагаемое решение. Тем не менее, он не соответствует критериям LSP, что соответствует документации.

Я хочу знать, что такое LSP, поэтому я наткнулся на одну хорошую статью. Как говорится в статье, принцип подстановки Лискова в Java не выполняется

LSP определяет слабое ограничение для набора свойств, но затем утверждает, что для любого свойства можно показать, что поведение программы одинаково. Единственный способ, которым это когда-либо может быть проверено, - это если исходные объекты в системе настолько строго определены их спецификацией, что они не допускают никакой другой реализации. Например, авторы вышеупомянутой статьи утверждают, что, учитывая набор свойств метода, они могут претендовать на 100% поведенческую совместимость с другим методом, имеющим тот же контракт - и это НЕ ИСТИНА. По сути, я могу утверждать, что контракт метода toString() в Java очень слабый - "Возвращает строковое представление объекта".

Почему важен принцип LiskovSubstitutionPrile?

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

Ссылка по ссылке, может очистить ваши сомнения.

Другие вопросы по тегам