Методы, общие для всех объектов

Хотя Object — это конкретный класс, он предназначен для расширения всех его нефинальных методов (equals, hashcode, toString и clone).

Пункт 10 «Соблюдайте генеральный контракт при переопределении равных»

Когда уместно переопределить equals? Это когда у класса есть понятие логического равенства, которое отличается от простой идентичности объекта, а суперкласс еще не переопределил равенство. В этом случае класс называется класс значения (класс, представляющий значение, например целое число или строку).

При переопределении метода equals необходимо соблюдать его общий контракт:

1. Рефлексивный: для любого ненулевого значения ссылки x функция x.equals(x) должна возвращать значение true.

2. Симметричный: для любых ненулевых ссылочных значений x и y функция x.equals(y) должна возвращать значение true тогда и только тогда, когда y.equals(x) возвращает значение true.

Пример нарушения:

public final class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s) {
        this.s = Objects.requireNonNull(s);
    }
    @Override public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString)
           return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
        if (o instanceof String)
           return s.equalsIgnoreCase((String) o);
        return false;
    }
}

Вот ошибка

CaseInsensitiveString cis = new CaseInsensitiveString(“Polish”);
String s = “polish”;

cis.equals(s) → true, а s.equals(cis) → false

Проблема в том, что хотя метод equals в CaseInsensitiveString знает об обычных строках, метод equals в String не обращает внимания на Case-InsensitiveString.

Решение «удалить попытку взаимодействия со строками из метода equal»

@Override public boolean equals(Object o) {
    return o instanceof CaseInsensitiveString 
           && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}

3. Transitive: для любых ненулевых ссылочных значений x, y, z, если x.equals(y) возвращает true, а y.equals(z) возвращает true, то x.equals(z) должен вернуть истину.

Пример нарушения:

public class Point {
    private final int x, y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    @Override public boolean equals(Object o) {
        if (!(o instanceof Point)) return false; 
        Point p = (Point)o;
        return p.x == x && p.y == y;
    }
}
public class ColorPoint extends Point {
    private final Color color;
    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }
    @Override public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        if (!(o instanceof ColorPoint)) return o.equals(this);
        return super.equals(o) && ((ColorPoint) o).color == color;
    }
}

Вот ошибка

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

Теперь p1.equals(p2) и p2.equals(p3) возвращают true, а p1.equals(p3) возвращает false. Первые два сравнения являются «дальтониками», а третье учитывает цвет.

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

Некоторые говорят, что метод getClass может помочь

@Override public boolean equals(Object o) {
    if (o == null || o.getClass() != getClass()) return false;
    Point p = (Point) o;
    return p.x == x && p.y == y;
}

Это приводит к приравниванию объектов только в том случае, если они имеют один и тот же класс реализации. Таким образом, это нарушает принцип подстановки Лисков (который гласит, что любое важное свойство типа должно сохраняться и для всех его подтипов, чтобы любой метод, написанный для этого типа, работал одинаково хорошо). на его подтипы).

Несмотря на то, что не существует удовлетворительного способа расширить инстанцируемый класс и добавить компонент значения, существует прекрасное обходное решение «Предпочитать композицию наследованию». Вместо ColorPoint расширить Point, дать ColorPoint частный Точка:

public class ColorPoint {
    private final Point point;
    private final Color color;
    public ColorPoint(int x, int y, Color color) {
        point = new Point(x, y);
        this.color = Objects.requireNonNull(color);
    }
    public Point asPoint() {
        return point;
    }
    @Override public boolean equals(Object o) {
        if (!(o instanceof ColorPoint)) return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.asPoint.equals(point) && cp.color.equals(color);
    }
}

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

4. Непротиворечивость: для любых ненулевых ссылочных значений x и y многократные вызовы x.equals(y) должны последовательно возвращать true или последовательно возвращать false, при условии, что никакая информация, используемая в сравнениях на равенство, не изменяется. Это необходимо учитывать, если ваш объект является неизменяемым.

5. Ненулевой: для любого ненулевого ссылочного значения x функция x.equals(null) должна возвращать false.

Нет необходимости проводить специальную проверку на значение null, достаточно использовать экземпляр для проверки.

6.Само значение равенства: как и в примере Point, если вы проигнорировали переопределение метода equal в ColorPoint, инструмент будет унаследован от класса Point, а информация о цвете будет игнорироваться. Хотя это не нарушает равный контракт, это неприемлемо.

Рецепт качественного равного метода:

  1. Используйте оператор «==», чтобы проверить, является ли аргумент ссылкой на этот объект (увеличение производительности).
  2. Используйте instanceOf, чтобы проверить, имеет ли аргумент правильный тип.
  3. Приведите аргумент к правильному типу.

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

  • Для примитивных полей: используйте оператор ‘==’.
  • Для полей ссылки на объект: вызовите метод equals.

Пункт 11 «Всегда переопределяйте хэш-код, когда вы переопределяете равенство»

Общий контракт hashCode

  • Когда метод hashCode вызывается для объекта повторно, он должен неизменно возвращать одно и то же значение при условии, что никакая информация, используемая в сравнениях на равенство, не изменяется.
  • Если два объекта равны в соответствии с методом equals(Object), то вызов hashCode для двух объектов должен давать одинаковый целочисленный результат.
  • Если два объекта не равны в соответствии с методом equals(Object), не требуется, чтобы вызов hashCode для каждого из объектов давал разные результаты. Однако получение различных результатов для неравных объектов может повысить производительность хеш-таблиц.

Ключевое положение, которое нарушается, если вы не можете переопределить hashCode, — это второе условие: одинаковые объекты должны иметь одинаковые хеш-коды.

Например:

Map<PhoneNumber, String> phoneNum= new HashMap<>();
phoneNum.put(new PhoneNumber(707, 867, 5309), “Jenny”);

На этом этапе вы можете ожидать, что phoneNum.get(new PhoneNumber(707, 867, 5309)) будет возвращает «Дженни», но вместо этого возвращает null. Поскольку метод get будет искать переданный объект в другом сегменте (с другим хэш-кодом), даже если он попадет в тот же сегмент, он вернет null, потому что HashMap имеет оптимизацию, которая кэширует hashCode, связанный с каждым запись и не проверяет равенство объектов, если хэш-коды не совпадают.

Рецепт высококачественного метода hashCode:

  1. Для первого значимого поля инициализируйте именованный результат hashCode. (следуйте инструкциям в 2, чтобы инициализировать его)
  2. Для каждого оставшегося значимого поля
  • Вычислите hashCode для поля: «Примитивное поле» → Type.hashCode (в файле) «Поле ссылки на объект» → Object.hasCode (в файле).
  • Объедините хэши следующим образом: результат = нечетная простая константа (например: 31) * результат + c;

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

Пример:

@Override public int hashCode() { 
    int result = Short.hashCode(areaCode); 
    result = 31 * result + Short.hashCode(prefix);
    result = 31 * result + Short.hashCode(lineNum);
    return result;
}

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

Пункт 12 «Всегда переопределять toString»

Хотя Object предоставляет реализацию метода toString, возвращаемая им строка обычно отличается от того, что хочет видеть пользователь вашего класса. Он состоит из имени класса, за которым следует (@) и шестнадцатеричное представление без знака hashCode, например, PhoneNumber@163b91.

Общий контракт для toString гласит, что возвращаемая строка должна быть «кратким, но информативным представлением, которое легко прочитать человеку, метод toString должен возвращать все интересные информация, содержащаяся в объекте.

Пункт 13 «Разумно отменить клонирование»

На практике ожидается, что класс, реализующий Cloneable, предоставит правильно функционирующий общедоступный метод clone. Для этого класс и все его суперклассы должны иметь метод clone, который не применяется интерфейсом. сам.

Общий контракт для метода клонирования слабый:

Создает и возвращает копию этого объекта, для любого объекта x выражение

  • x.clone() != x → будет true (поскольку это разные объекты)
  • x.clone().getClass() == x.getClass() → будет true
  • x.clone().equals(x) → будет true

По соглашению объект, возвращаемый этим методом, должен быть получен путем вызова super.cloneи того же для всех его суперклассов (кроме Object).

Если метод clone класса возвращает экземпляр, полученный не вызовом super.clone, а вызовом конструктора, компилятор не будет жаловаться, но если подкласс этого class вызывает super.clone, результирующий объект будет иметь неправильный класс, что будет препятствовать правильной работе метода клонирования подкласса.

Мы бы разделили рецепт на два пути

  1. Метод клонирования для класса без ссылок на изменяемое состояние
@Override public PhoneNumber clone() {
    return (PhoneNumber) super.clone();
}

Вызова super.clone будет достаточно для копирования всех его полей, если поля являются примитивными значениями или ссылками на неизменяемые объекты.

2. Метод клонирования для класса со ссылками на изменяемое состояние

Пример:

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(Object e) {...}
    public Object pop() {...}
}

Если метод clone возвращает super.clone(), результирующий экземпляр стека будет иметь правильное значение в своем size, но его поле elements будет ссылаться на тот же массив, что и исходный экземпляр Stack. Модификация оригинала уничтожит инварианты в клоне и наоборот.

Чтобы метод clone в стеке работал правильно, он должен копировать внутренности стека. Самый простой способ сделать это — рекурсивно вызвать clone для массива элементов:

@Override public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

Также обратите внимание, что предыдущее решение не будет работать, если поле elements будет final, потому что метод clone будет запрещен. от присвоения нового значения полю, за исключением случаев, когда изменяемые объекты могут быть безопасно разделены между объектом и его клоном. Чтобы класс можно было клонировать, может потребоваться удалить модификаторы final из некоторых полей.

Напомним, что все классы, реализующие Cloneable, должны переопределять clone с помощью общедоступного метода, тип возвращаемого значения которого — сам класс. . Этот метод должен сначала вызвать super.clone, а затем исправить все поля, которые необходимо исправить. Копируя любые изменяемые объекты и заменяя ссылки клона на эти объекты ссылками на их копии.

Пункт 14 «рассмотреть возможность реализации сопоставимых»

В отличие от других рассмотренных методов, метод compareTo не объявлен в Object. Скорее, это единственный метод в интерфейсе Comparable. По своему характеру он аналогичен методу equals класса Object, за исключением того, что он позволяет проводить сравнения по порядку в дополнение к простым сравнениям на равенство.

Реализуя Comparable, вы позволяете своему классу взаимодействовать со всеми многочисленными универсальными алгоритмами и реализациями коллекций, которые зависят от этого интерфейса, например, сортировка массива объектов, реализующих Сопоставимо просто: Arrays.sort(a);

Общий контракт метода compareTo подобен контракту equals: сравнивает этот объект с указанным объектом для приказ. Возвращает отрицательное целое число, ноль или положительное целое число, поскольку этот объект меньше, равен или больше указанного объекта. Выдает ClassCastException, если указанный тип объекта не позволяет сравнивать его с этим объектом.

  • Симметричный: x.compareTo(y) ==- y.compareTo(x) для всех x и y.
  • Переходный: (x.compareTo(y) › 0 && y.compareTo(z) › 0) подразумевает x.compareTo(z) › 0.
  • Настоятельно рекомендуется, но не обязательно, чтобы (x.compareTo(y) == 0) == (x.equals(y)).

Класс, метод compareTo которого устанавливает порядок, несовместимый с равенством, по-прежнему будет работать, но отсортированные коллекции, содержащие элементы класса, могут не подчиняться общему контракту соответствующих интерфейсов коллекций. (Коллекция, Набор или Карта). Это связано с тем, что общие контракты для этих интерфейсов определяются в терминах метода compareTo, а не метода equals.

Пример

рассмотрим класс BigDecimal, чей метод compareTo несовместим с equals. . Если вы создадите пустой экземпляр HashSet, а затем добавите новые BigDecimal("1.0") и new BigDecimal("1.00"), набор будет содержать два элемента, поскольку два экземпляра BigDecimal, добавленные в набор, неравны, когда по сравнению с использованием метода equals.

Однако если вы выполняете ту же процедуру, используя TreeSet вместо HashSet, набор будет содержать только один элемент, потому что два экземпляра BigDecimal равны при сравнении с использованием метода compareTo.

Чтобы сравнить поля ссылок на объекты, рекурсивно вызовите метод compareTo.

Пример:

// Single-field Comparable with object reference field
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
    public int compareTo(CaseInsensitiveString cis) {
        return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
    }
}
// Multiple-field Comparable with primitive fields
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
    public int compareTo(PhoneNumber pn) {
        int result = Short.compare(areaCode, pn.areaCode);
        if (result == 0) {
            result = Short.compare(prefix, pn.prefix);
            if (result == 0)
                result = Short.compare(lineNum, pn.lineNum);
        }
        return result;
    }
}

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

Пример:

// Comparable with comparator construction methods
private static final Comparator<PhoneNumber> COMPARATOR 
    = comparingInt((PhoneNumber pn) -> pn.areaCode)
        .thenComparingInt(pn -> pn.prefix)
        .thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}

Таким образом, всякий раз, когда вы реализуете класс значений с разумным порядком, вы должны иметь класс, реализующий интерфейс Comparable, чтобы его экземпляры можно было легко сортировать, искать и использовать в коллекциях на основе сравнения. При сравнении значений полей в реализациях методов compareTo избегайте использования операторов ‹ и ›. Вместо этого используйте статические методы сравнения в упакованных примитивных классах или методы построения компаратора в интерфейсе Comparator.

Закажите эффективную книгу по Java 3rd edition для автора Джошуа Блохздесь.