0
Обложка: Методы equals() и hashcode() в языке Java

Методы equals() и hashcode() в языке Java

Методы equals и hashCode в Java в чём-то очень схожи, и даже вместе генерируются средствами IDE, таких как IntelliJ IDEA. Что в них общего, каковы отличия, и что будет, если использовать только один из методов?

Знаете, чем отличаются ==, equals и hashCode?

  1. Метод equals()
  2. Контракт equals()
  3. Использование equals
  4. Метод hashcode()
  5. Контракт hashCode()
  6. Использование hashCode
  7. Почему equals и hashCode в Java переопределяются вместе

Метод equals() в Java

Как вы наверняка знаете, сравнение посредством == в Java сравнивает ссылки, но объекты таким образом не сравнить. Следующий пример подобного сравнения двух строк вернёт false:

public static void main(String[] args) {
    //false
    System.out.println(new String("Tproger") == new String("Tproger"));
}

Пусть значения и одинаковы, но переменные String указывают на разные объекты.

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

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

Контракт equals() в Java

Используя equals, мы должны придерживаться основных правил, определённых в спецификации Java:

  1. Рефлексивностьx.equals(x) возвращает true.
  2. Симметричностьx.equals(y) <=> y.equals(x).
  3. Транзитивностьx.equals(y) <=> y.equals(z) <=> x.equals(z).
  4. Согласованность — повторный вызов x.equals(y) должен возвращать значение предыдущего вызова, если сравниваемые поля не изменялись.
  5. Сравнение nullx.equals(null) возвращает false.

Использование equals

Предположим, у нас есть класс Programmer, в котором предусмотрены поля с должность и зарплатой:

public class Programmer {
    private final String position;
    private final int salary;

    protected Programmer(String position, int salary) {
        this.position = position;
        this.salary = salary;
    }
}

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

Зачастую метод equals в Java определяется вместе с hashCode, но здесь мы рассмотрим первый метод отдельно:

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

    Programmer that = (Programmer) o;

    if (salary != that.salary) return false;
    return Objects.equals(position, that.position);
}

Определим объекты programmer1 и programmer2 типа Programmer с одинаковыми значениями. При их сравнении с помощью == вернётся false, так как это разные объекты. Если же мы используем для сравнения метод equals(), вернётся true:

public class Main extends Programmer {
    protected Main(String position, int salary) {
        super(position, salary);
    }

    public static void main(String[] args) {
        Programmer programmer1 = new Programmer("Junior", 300);
        Programmer programmer2 = new Programmer("Junior", 300);

        //false
        System.out.println(programmer1 == programmer2);
        //true
        System.out.println(programmer1.equals(programmer2));
    }
}

А вот такой результат мы получим, если хотя бы одна переменная (обозначенное поле объекта) из метода equals() не совпадёт:

public class Main extends Programmer {
    protected Main(String position, int salary) {
        super(position, salary);
    }

    public static void main(String[] args) {
        Programmer programmer1 = new Programmer("Junior", 300);
        Programmer programmer2 = new Programmer("Middle", 300);

        //false
        System.out.println(programmer1 == programmer2);
        //false
        System.out.println(programmer1.equals(programmer2));
    }
}

Метод hashcode() в Java

Наконец, мы дошли до сравнения методов equals и hashCode в языке Java.

Фундаментальное отличие в том, что hashCode() — это метод для получения уникального целочисленного номера объекта, своего рода его идентификатор. Благодаря хешу (номеру) можно, например, быстро определить местонахождение объекта в коллекции.

Это число используется в основном в хеш-таблицах, таких как HashMap. При этом хеш-функция получения числа на основе объекта должна быть реализована таким образом, чтобы обеспечить равномерное распределение элементов по хэш-таблице. А также минимизировать возможность появления коллизий, когда по разным ключам функция вернёт одинаковое значение.

В случае Java, метод hashCode() возвращает для любого объекта 32-битное число типа int. Сравнить два числа между собой гораздо быстрее, чем сравнить два объекта методом equals(), особенно если в нём используется много полей.

Контракт hashCode() в Java

  1. Повторный вызов hashCode для одного и того же объекта должен возвращать одинаковые хеш-значения, если поля объекта, участвующие в вычислении значения, не менялись.
  2. Если equals() для двух объектов возвращает true, hashCode() также должен возвращать для них одно и то же число.
  3. А вот для неравных между собой объектов hashCode должен возвращать разные хеш-значения.

Использование hashCode

Вернёмся к нашему классу Programmer. По-хорошему, вместе с equals() должен быть использован и метод hashCode():

public class Programmer {
    private final String position;
    private final int salary;

    protected Programmer(String position, int salary) {
        this.position = position;
        this.salary = salary;
    }

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

        Programmer that = (Programmer) o;

        if (salary != that.salary) return false;
        return Objects.equals(position, that.position);
    }

    @Override
    public int hashCode() {
        int result = position != null ? position.hashCode() : 0;
        result = 31 * result + salary;
        return result;
    }
}

Почему equals и hashCode в Java переопределяются вместе

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

Рассмотрим два сценария.

1. equals есть, hashCode нет

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

Map<Point, String> m = new HashMap<>();
m.put(new Point(1, 1), "Point A");
//pointName == null
String pointName = m.get(new Point(1, 1));

2. hashCode есть, equals нет

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

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

Читайте также об условных операторах в Java.