Равно и hashCode, которые интервьюер любит спрашивать

Java исходный код

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

Далее мы продолжаем использовать вопрос и ответ на собеседовании, чтобы проверить мастерство использования equals и hashCode.

Интервьюер: Java имеет==Оператор, зачем вам равные?


Роль equals() заключается в том, чтобы определить, равны ли два объекта.Определение в Object:

public boolean equals(Object obj) {
    return (this == obj);
}

Это означает, что до того, как мы реализуем наш собственный метод equals, equals эквивалентен====Оператор должен определить, являются ли два объекта одним и тем же объектом, т.Адреса равны?. Переопределение равенства больше связано с преследованием двух объектов влогически равный, можно сказать дазначение равно, можно также сказать, чтосодержание равно.

В следующих условиях не переопределение равенства может достичь цели:

  • Каждый экземпляр класса по своей сути уникален: выделяет активный объект и не заботится о значении, таком как Thread, о каком потоке мы заботимся, тогда мы можем использовать равные для сравнения.
  • Не важно, предоставляет ли класс проверку логического равенства: Пользователи некоторых классов не будут использовать свою функцию сравнения, например класс Random, в принципе никто не будет сравнивать два случайных значения.
  • Суперкласс уже переопределил равенство, и подклассу нужно только использовать поведение суперкласса.: Например, если в AbstractMap было переопределено equals, эта функция также требуется в поведении унаследованных подклассов, поэтому нет необходимости реализовывать ее снова.
  • Класс является закрытым или закрытым на уровне пакета, и метод equals не используется.: В настоящее время вам нужно переопределить метод equals, чтобы отключить его:@Override public boolean equals(Object obj) { throw new AssertionError();}

Интервьюер: Итак, вы знаете, каковы правила переопределения равенства?


Я видел это на Effective Java, если я правильно помню, это должно быть:

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

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

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

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

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

Интервьюер: Расскажите о ситуациях, в которых нарушается симметрия и транзитивность.


нарушение симметрии

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

public final class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s) {
        if (s == null)
            throw new NullPointerException();
        this.s = s;
    }
    
    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensiticeString)
            return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
        if (o instanceof String)
            return s.equalsIgnoreCase((String) o);
        return false;
    }
}

Эта идея очень хороша.Я хочу создать строку без учета регистра, и она также совместима со строкой в ​​качестве параметра.Предположим, мы создаем строку без учета регистра:

CaseInsensitiveString cis = new CaseInsensitiveString("Case");

тогда должно бытьcis.equals("case"), вот проблема,"case".equals(cis)? String несовместим с CaseInsensiticeString, поэтому функция equals String не принимает CaseInsensiticeString в качестве параметра.

Итак, есть правило, обычно переопределяющее равенствоСовместим только с переменными того же типа.

нарушение транзитивности

Транзитивность означает, что А равно В, В равно С, тогда А также должно быть равно С.

Предположим, мы определяем класс Cat.

public class Cat()
{
    private int height;
    private int weight;
    public Cat(int h, int w)
    {
        this.height = h;
        this.weight = w;
    }
    
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Cat))
            return false;
        Cat c = (Cat) o;
        return c.height == height && c.weight == weight; 
    }
}

Знаменитости говорили, что неважно, черный кот или белый кот поймает мышь, это хороший кот Мы определяем класс ColorCat:

public class ColorCat extends()
{
    private String color;
    public ColorCat(int h, int w, String color)
    {
        super(h, w);
        this.color = color;
    }

Когда мы реализуем метод equals, мы можем добавить сравнение цветов, но добавление цвета не совместимо с обычными кошками.Здесь мы забываем приведенное выше предложение о том, что совместимы только переменные одного типа, и определяем метод equals, совместимый с обычными кошками. , Цвета игнорируются при смешивании.

@Override
public boolean equals(Object o) {
    if (! (o instanceof Cat))
        return false; //不是Cat或者ColorCat,直接false
    if (! (o instanceof ColorCat))
        return o.equals(this);//不是彩猫,那一定是普通猫,忽略颜色对比
    return super.equals(o)&&((ColorCat)o).color.equals(color); //这时候才比较颜色
}

Предположим, мы определяем кота:

ColorCat whiteCat = new ColorCat(1,2,"white");
Cat cat = new Cat(1,2);
ColorCat blackCat = new ColorCat(1,2,"black");

В настоящее время whiteCat равен cat, cat равен blackCat, но whiteCat не равен blackCat, поэтому он не удовлетворяет требованию транзитивности. .

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

Интервьюер, есть ли у вас какие-либо советы по переопределению метода equals в вашей работе, например, по написанию equals в String?


почерк:

 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;
    }

Вышеупомянутое равенство имеет следующие приемы:

  • Используйте оператор ==, чтобы проверить, является ли параметр ссылкой на этот объект.: Если это сам объект, он вернется напрямую, перехватив вызов самого себя, что является оптимизацией производительности.
  • Используйте оператор instanceof, чтобы проверить, что «аргумент имеет правильный тип».: Если нет, верните false, как говорят примеры симметричности и транзитивности, не думайте о совместимости с другими типами, легко ошибиться. На практике проверяемый тип — это в основном тип класса, в котором находится equals, или тип интерфейса, реализованного классом, например, Set, List, Map и эти интерфейсы коллекций.
  • Преобразование параметра в правильный тип: После предыдущего шага обнаружения это в основном удастся.
  • Для «ключевого поля» в классе проверьте, что поле в параметре равно соответствующему полю в объекте.: Используйте поля только базовых типов.==Для сравнения используйте метод Float.compare для поля с плавающей запятой и метод Double.compare для поля double.Что касается других ссылочных полей, мы обычно рекурсивно вызываем их метод equals для сравнения, а также проверку нуля и проверку ссылки на себя. , который обычно записывается так:(field == o.field || (field != null && field.equals(o.field))), а приведенная выше строка использует массив, поэтому просто выньте каждый бит в массиве и сравните его.
  • После написания подумайте, выполняются ли упомянутые выше симметрия, транзитивность, непротиворечивость и т. д..

Есть также некоторые предостережения.

Обязательно переопределяйте hashCode при переопределении равенства

Функция equals должна иметь тип Object в качестве параметра.

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

Интервьюер, какая сейчас польза от hashCode?


hashCode используется для возврата хэш-значения объекта, в основном для ускорения поиска, потому что hashCode также находится в объекте Object, поэтому все объекты Java имеют hashCode в хэш-структурах, таких как HashTable и HashMap, обе позиции в хеш-таблица находится по hashCode.

Если два объекта равны, то их hashCode должен быть равен,

Но hashCode равен, equals не обязательно равен.

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

0 1 2 3 4 5 6 7

Затем при вставке в него данных он вставляется с hashCode в качестве ключа.Как правило, hashCode%8 получает индекс, в котором он находится.Если в индексе есть элемент, используется связанный список, чтобы непрерывно связывать множество элементов с это место.На этой стороне стоит упомянуть принцип HashMap. Таким образом, роль hashCode заключается в том, чтобы найти позицию индекса, а затем использовать equals для сравнения, равны ли элементы Изображение состоит в том, чтобы сначала найти ведро, а затем найти что-то в нем.

Интервьюер: Знаете ли вы какие-нибудь советы по переопределению hashCode?


Цель хорошего метода hashCode:генерировать неравные хэш-коды для неравных объектов, аналогично одинаковые объекты должны иметь одинаковые хэш-коды.

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

Цитата из Эффективная Java

  1. Сохранить ненулевое постоянное значение, например 17, в результате int;

  2. Для каждого ключевого поля f (каждого поля, разработанного методом equals) выполните следующие действия:

    a, вычислить хэш-код типа int для поля;

     i.如果该域是boolean类型,则计算(f?1:0),
     ii.如果该域是byte,char,short或者int类型,计算(int)f,
     iii.如果是long类型,计算(int)(f^(f>>>32)).
     iv.如果是float类型,计算Float.floatToIntBits(f).
     v.如果是double类型,计算Double.doubleToLongBits(f),然后再计算long型的hash值
     vi.如果是对象引用,则递归的调用域的hashCode,如果是更复杂的比较,则需要为这个域计算一个范式,然后针对范式调用hashCode,如果为null,返回0
     vii. 如果是一个数组,则把每一个元素当成一个单独的域来处理。
    

    b.result = 31 * result + c;

  3. вернуть результат

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

Давайте поговорим о том, почему он используется в 2.b31*result + c, Умножение делает хеш-значение зависимым от порядка полей. Если умножения нет, то все объекты String в разном порядке будут иметь одинаковое хэш-значение, а 31 — нечетное простое число. Если оно четное, то умножение переполняется, информация будет потеряна, 31 Приятной особенностью является то, что31*i ==(i<<5)-i, то есть 2 в пятой степени минус 1, виртуальная машина оптимизирует операцию умножения как операцию сдвига.