Урок 22. hashcode и equals в Java: часть 1


Эта статья является частью уроков по Java 8. Она завершает серию, в которой мы переходим ко всем методам класса java.lang.Object. hashCode и equals - это два из java.lang.Object метода, тесно связанных. На самом деле они связаны таким образом, что мы не можем реализовать одно без другого и ожидаем, что у нас будет хорошо написанный класс. Знание всех подробностей об этих двух методах имеет немалое значение для того, чтобы стать хорошим программистом Java. Так что же делают эти два метода?

Метод equals

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

   @Test
    public void primitivesShouldBeEqual() {
        int i = 4;
        int j = 4;
        assertTrue(i == j);
    }
В коде выше мы видим, что CarTest будет проходить для primitivesShouldBeEqual, так как оператор равенства == по своей сути работает для примитивов. Два примитивных значения i и j присваиваются соответственно следующим образом: i = 4 и j = 4. утверждение с помощью оператора equals успешно выполняется, и мы получаем проходной тест, как и ожидалось.

    @Test
    public void stringShouldBeEqual() {
        String hello1 = "Hello";
        String hello2 = "Hello";
        String hello3 = "H";
        hello3 = hello3 + "ello";
        System.out.println(hello3);
        assertTrue(hello1.equals(hello3));
    }
А тут stringShouldBeEqual имеет две различные строковые ссылочные переменные, hello1 и hello2, с двумя различными строковыми константами “Hello”, назначенными им. Прежде чем говорить о равенствах, давайте посмотрим, что произойдет, если мы попытаемся сравнить эти две строки с оператором равенства, который используется для примитивных значений. hello1 == hello2 будет возвращать значение true, без необходимости использовать метод equals вообще.

Это происходит потому, что строковые значения оптимизируются в фоновом режиме для констант, где на самом деле существует только одна из них, и обе переменные указывают на один и тот же объект в памяти. Часть памяти, в которой хранятся переменные hello1 и hello2, называется стеком. С другой стороны, фактический единственный объект “привет”, на который указывают обе переменные, хранится в другой части памяти, называемой кучей.

Возвращаясь к нашему сценарию оператора равенства, все становится немного сложнее, как только мы добавляем новую строковую переменную, где мы инициализируем hello3 на “H” и повторно назначаем hello3 самому себе, сцепленному с “ello”. hello1 == hello3 будет иметь значение false. Распечатав их оба, мы ожидали бы, что hello1 == hello3 будет значение true. Мсть мы не должны использовать оператор равенства для объектов, включая строки. Вместо этого мы используем метод equals, как показано в примере выше строка 8.

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String) anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
Класс String имеет метод equals, как показано в кодже выше. Во-первых, он проверяет, работает ли оператор равенства для двух сравниваемых строк. На самом деле это проверка того, имеют ли обе переменные одну и ту же ссылку, то есть указывают на одну и ту же строку в памяти, (это для оптимизации производительности).

Затем он проверяет, действительно ли второй объект является строкой. Это достигается с помощью оператора instanceof. instanceof безопасно приводит объект к строке, так что мы можем сравнить их оба. Функция instanceof, о которой многие разработчики не знают, заключается в том, что она включает проверку на null. Нулевой объект никогда не будет строкой и никогда не будет иметь значения true. Наконец, метод просто перебирает и сравнивает символы каждой строки на равенство, возвращая true тогда и только тогда, когда все они полностью равны.

String на самом деле является подклассом Object, и его метод equals переопределяется, что позволяет ему работать со строками типа hello1 и hello3 из примера 1, как и ожидалось.

Пользовательские классы

    @Test
    public void porscheShouldBeEqual() {
        Car myPorsche1 = new Car("Marcus", "Porsche", "silver");
        Car myPorsche2 = new Car("Marcus", "Porsche", "silver");
        assertTrue(myPorsche1.equals(myPorsche2));
    }
Рассмотрим два примера автомобиля пользовательского класса. Это экземпляр нашего класса автомобилей, определенного в коде выше, который оказался серебристым Porsche, принадлежащим Маркусу. Оба экземпляра, по-видимому, имеют совершенно одинаковые атрибуты, поэтому мы ожидаем, что это будет один и тот же автомобиль. Используя оператор "равно" на них, однако возвращает значение false. Это не работает, и тест проваливается, потому что у нас есть два разных экземпляра.

Рассмотрим простой случай, когда myPorsche1 == myPorsche1, сравнивая myPorsche1 с самим собой. Это сравнение возвращает true, поскольку оператор равенства по своей сути возвращает true всякий раз, когда мы сравниваем объект с самим собой. Переходя к myPorsche1 == myPorsche2, казалось бы, нам нужно было бы снова использовать метод equals. На самом деле, выполнение утверждения проваливает наш тест. Поэтому мы пытаемся использовать equals, утверждая myPorsche1.equals(myPorsche2) только для того, чтобы обнаружить себя встреченными неудачным результатом теста. Это так хорошо работало для строк, так почему же это не работает с автомобилем?

Если мы внимательно проследим за выполнением нашего кода, то обнаружим, что мы в конечном итоге попадаем в класс Object, потому что наш класс Car не определяет и не переопределяет свой собственный метод equals. То, что на самом деле происходит внутренне, - это сравнение объекта с самим собой. В нашем случае myPorsche1.equals(myPorsche2) будет сравнивать только сами ссылочные переменные, а не фактические объекты Car. Это сравнение неизменно возвращает false просто потому, что у нас есть две разные ссылочные переменные.

Во всяком случае, это можно было бы сделать снова истинным, если бы мы назначили myPorsche2 myPorsche1. Это приведет к тому, что объект, инициализированный в myPorsche2, больше не будет иметь переменной, указывающей на него. Это также заставило бы обе переменные указывать на объект, к которому был инициализирован myPorsche1. Проведя сравнение еще раз, мы получили бы успешный тестовый запуск, потому что мы снова сравнили бы тот же самый объект с самим собой. К сожалению, это не то, чего мы хотим. Вместо этого мы хотим сравнить оба объекта таким образом, чтобы это имело смысл для нашей программы.

Давайте посмотрим на класс Car.

public class Car {

    private String owner;
    private String manufacturer;
    private String color;

    public Car(String owner, String manufacturer, String color) {
        this.owner = owner;
        this.manufacturer = manufacturer;
        this.color = color;
    }

    @Override
    public boolean equals(Object obj) {
        return false;
    }

    [...]

}
Здесь показан небольшой простой класс с конструктором, некоторыми атрибутами и методом equals. Обратите внимание, что у нас нет Car car в качестве параметра для нашего метода equals. Мы не можем этого сделать из-за исходной сигнатуры объекта equals, поэтому вместо этого мы хотим, чтобы наш параметр был чем-то вроде Object obj. Позже мы должны привести его к Car аналогично тому, что мы видели ранее с классом String.

Кроме того, всегда полезно добавить аннотацию @Override, чтобы убедиться, что мы правильно переопределили метод. Наша реализация equals - это заглушка, которая возвращает только false в качестве значения-заполнителя, и в конечном итоге нам придется конкретизировать детали. Однако оказывается, что реализовать equals не так просто, поэтому нам нужно будет заняться еще одной теорией, прежде чем мы продолжим.

Проектирование

В большинстве случаев программисты просто пропускают разработку этого метода и просто нажимают на кнопку “автогенерация”, что во многих случаях приводит к серьезным ошибкам или, по крайней мере, к неоптимальной производительности. Однако для правильной реализации equals мы должны определить, что делает два экземпляра Car равными таким образом, чтобы это имело смысл для контекста программы, в которой он используется. Имеет ли смысл в его контексте, чтобы два автомобиля были равны, когда у них один и тот же производитель? Когда у них один и тот же производитель и цвет? Тот же двигатель? Или такое же количество колес? Та же максимальная скорость? Тот же идентификационный номер транспортного средства (VIN код)?

Исходя из контекста, мы должны решить, какой набор полей можно использовать для идентификации объекта, а какие поля для сравнения являются избыточными. Кроме того, для повышения производительности нам необходимо определить порядок, в котором мы хотим сравнить атрибуты. Есть ли поля с высокой вероятностью оказаться неравными? Сравниваются ли некоторые поля быстрее, чем другие? Реализация значимого метода равных требует детального анализа этих аспектов.

В этом примере мы предполагаем, что максимальная скорость автомобиля в той или иной форме связана с типом двигателя, что делает его избыточным полем, которое не так полезно для реализации equals, поэтому мы его удаляем. Допустим, в нашем примере наши автомобили всегда имеют четыре колеса. Количество колес не помогло бы нам различать автомобили. А как насчет вин? Это опять же будет зависеть от типа программы, которую мы хотим создать. Для полиции это, вероятно, единственный правильный способ идентификации. Но как насчет программы, которую использует производитель? Пока автомобиль все еще строится, VIN, вероятно, будет менее важен.

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

Реализация equals

Прежде всего, есть пять условий, которым должна соответствовать наша реализация equals. Все это на самом деле довольно просто, поэтому давайте рассмотрим несколько примеров и посмотрим, что они подразумевают:

  • Рефлексивность “объект должен быть равен самому себе.” Это означает, что myOldCar.equals(myOldCar) всегда должен возвращать true. Это довольно тривиально, но все же важно убедиться, что мы тестируем его.
  • Симметрия “два объекта должны согласовываться независимо от того, равны они или нет.” Если myOldCar.equals(someOtherCar) возвращает true, то так же должно быть и someOtherCar.equals(myOldCar). Это также звучит довольно очевидно, когда дело доходит до наследования. Если у вас есть класс автомобилей и подкласс автомобилей BMW, то из этого следует, что все BMW являются автомобилями, но не каждый автомобиль является BMW. Это делает правило довольно сложным для выполнения. Убедитесь, что каждое условие контракта покрыто специальным модульным тестом, чтобы убедиться, что класс остается полностью совместимым с контрактом.
  • Транзитивность “если один объект равен второму, а второй третьему, то первый должен быть равен третьему.” Это правило на самом деле более прямолинейно, чем кажется на первый взгляд. Допустим, у нас есть три автомобиля: carA, carB и carC. If carA == carB, and carB == carC, then carA == carC.
  • "Если два объекта равны, они должны оставаться равными всегда, пока один из них не изменится.” Это означает, что два объекта не должны быть изменены каким-либо образом с помощью метода equals. Поэтому, когда вы повторно сравниваете одни и те же два объекта с помощью метода equals, он всегда должен возвращать один и тот же результат.
  • Null возвращает false “все объекты должны быть неравны нулю.” Это последнее правило - то, что Джош блох, автор эффективной Java, называет “ненулевым”. При задании null в качестве параметра метода equals мы всегда должны возвращать false и никогда не вызывать исключение NullPointerException.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегистатьи IT, уроки по java, java




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




Портирование Java (Android) на C# (WinForms)
Как заблокировать вебвизор
Урок 4. Работа с формами в Yii2