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