Equals, hashCode и toString

Базовый Java
≈ 3 мин чтения

Три метода, которые описывают объект

Методы equals(), hashCode() и toString() определены в Object, поэтому они есть у любого объекта. Но поведение из Object часто слишком общее для наших классов — и тогда методы переопределяют.

У каждого из них своя задача: equals() отвечает на вопрос «считать ли два объекта равными», hashCode() даёт числовой хеш, согласованный с этим равенством, а toString() формирует текстовое представление объекта.

equals(): равенство по смыслу

Если класс не переопределяет equals(), используется реализация из Object. Она считает два объекта равными только тогда, когда обе ссылки указывают на один и тот же объект — по сути так же, как оператор == для ссылок.

Point first = new Point(2, 3);
Point second = new Point(2, 3);

System.out.println(first == second);      // false
System.out.println(first.equals(second)); // false

Но для класса Point может быть логично считать две точки равными, если совпадают их координаты. Тогда это правило нужно явно выразить в equals().

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj == null || getClass() != obj.getClass()) {
        return false;
    }

    Point other = (Point) obj;
    return x == other.x && y == other.y;
}

Здесь сначала обрабатывается самый простой случай — сравнение объекта с самим собой. Затем отбрасываются null и объект другого класса, а после этого сравниваются поля, которые определяют равенство точек.

Корректный equals() должен соблюдать несколько правил:

  • объект равен самому себе;
  • если a.equals(b) истинно, то и b.equals(a) должно быть истинно;
  • если a равен b, а b равен c, то a должен быть равен c;
  • результат не должен самопроизвольно меняться, пока данные, участвующие в сравнении, остаются прежними;
  • любой ненулевой объект не равен null.

hashCode(): равные объекты должны быть согласованы

hashCode() возвращает целое число — хеш объекта. Главное правило здесь связано с equals(): если два объекта равны по equals(), их hashCode() обязан вернуть одинаковое значение.

Обратное неверно: одинаковый хеш ещё не означает, что объекты равны. Разные объекты вполне могут получить один и тот же hashCode().

@Override
public int hashCode() {
    return java.util.Objects.hash(x, y);
}

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

Переопределяешь equals() — переопредели и hashCode(). Иначе два объекта могут считаться равными, но иметь несогласованные хеши. Это нарушает контракт этих методов и приводит к неправильному поведению кода, который рассчитывает на их согласованность.

toString(): объект в читаемом виде

toString() возвращает строковое представление объекта. Реализация из Object обычно даёт строку вида Point@1a2b3c. Она полезна как техническое представление, но почти ничего не говорит о содержимом объекта.

Поэтому для собственных классов toString() часто переопределяют, включая в результат действительно полезные поля:

@Override
public String toString() {
    return "Point{x=" + x + ", y=" + y + "}";
}

Теперь при выводе объекта можно сразу увидеть его состояние:

Point point = new Point(2, 3);
System.out.println(point); // Point{x=2, y=3}

Явно вызывать point.toString() здесь не обязательно: при передаче объекта в System.out.println() его строковое представление используется автоматически.

Формат toString() стоит делать коротким и понятным человеку. Не следует воспринимать стандартную часть после @ как адрес объекта в памяти или как стабильный идентификатор.