Які проблеми / підводні камені слід враховувати при переборі equals
та hashCode
?
Які проблеми / підводні камені слід враховувати при переборі equals
та hashCode
?
Відповіді:
equals()
( javadoc ) має визначати відношення еквівалентності (воно повинно бути рефлексивним , симетричним та транзитивним ). Крім того, він повинен бути послідовним (якщо об'єкти не модифіковані, то він повинен постійно повертати те саме значення). Крім того, o.equals(null)
завжди потрібно повертати помилкове.
hashCode()
( javadoc ) також має бути узгодженим (якщо об'єкт не змінено з точки зору equals()
, він повинен продовжувати повертати те саме значення).
Співвідношення між цими двома методами:
Кожен раз
a.equals(b)
, тодіa.hashCode()
повинен бути таким самимb.hashCode()
.
Якщо ви перекриєте одне, то вам слід перекрити інше.
Використовуйте той самий набір полів, який ви використовуєте для обчислення equals()
для обчислення hashCode()
.
Використовуйте чудові допоміжні класи EqualsBuilder та HashCodeBuilder з бібліотеки Apache Commons Lang . Приклад:
public class Person {
private String name;
private int age;
// ...
@Override
public int hashCode() {
return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
// if deriving: appendSuper(super.hashCode()).
append(name).
append(age).
toHashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Person))
return false;
if (obj == this)
return true;
Person rhs = (Person) obj;
return new EqualsBuilder().
// if deriving: appendSuper(super.equals(obj)).
append(name, rhs.name).
append(age, rhs.age).
isEquals();
}
}
При використанні хеш на основі колекції або карти , такі як HashSet , LinkedHashSet , HashMap , Hashtable або WeakHashMap , переконайтеся , що в хеш - код () ключових об'єктів , які ви поклали в колекції не змінюється , поки об'єкт знаходиться в колекції. Надійний спосіб забезпечити це - зробити ваші ключі непорушними, що має і інші переваги .
instanceof
повертає значення false, якщо його перший операнд є null (знову ефективний Java).
Є деякі проблеми, які варто помітити, якщо ви маєте справу з класами, які зберігаються за допомогою Mapper-Relationship Mapper (ORM), як Hibernate, якщо ви не вважаєте, що це вже безпідставно складне!
Ледачі завантажені об'єкти - це підкласи
Якщо ваші об'єкти зберігаються за допомогою ORM, у багатьох випадках ви матимете справу з динамічними проксі-серверами, щоб уникнути завантаження об'єкта занадто рано із сховища даних. Ці проксі-сервери реалізуються як підкласи вашого власного класу. Це означає, що this.getClass() == o.getClass()
повернеться false
. Наприклад:
Person saved = new Person("John Doe");
Long key = dao.save(saved);
dao.flush();
Person retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass()); // Will return false if Person is loaded lazy
Якщо ви маєте справу з ORM, використання o instanceof Person
- це єдине, що буде вести себе правильно.
Об'єкти з ледачим завантаженням мають нульові поля
ORM зазвичай використовують геттери для примусового завантаження ліниво завантажених об'єктів. Це означає, що person.name
буде, null
якщо person
ледачий навантажений, навіть якщо person.getName()
змушує завантажувати і повертати "Джон Доу". З мого досвіду, ця культура частіше з'являється в hashCode()
і equals()
.
Якщо ви маєте справу з ORM, не забудьте завжди використовувати геттери, а ніколи не посилайтеся на поля в hashCode()
і equals()
.
Збереження об’єкта змінить його стан
Стійкі об'єкти часто використовують id
поле для утримання ключа об'єкта. Це поле буде автоматично оновлено при першому збереженні об'єкта. Не використовуйте поле ідентифікатора в hashCode()
. Але ви можете використовувати його в equals()
.
Я часто використовую схему
if (this.getId() == null) {
return this == other;
}
else {
return this.getId().equals(other.getId());
}
Але: ви не можете включити getId()
в hashCode()
. Якщо ви зробите це, коли об’єкт зберігається, його hashCode
зміниться. Якщо об’єкт знаходиться в a HashSet
, ви "ніколи" не знайдете його знову.
У моєму Person
прикладі я, мабуть, використовував би getName()
для hashCode
і getId()
плюс getName()
(лише для параної) equals()
. Це добре, якщо є певний ризик "зіткнення" hashCode()
, але ніколи не гаразд equals()
.
hashCode()
слід використовувати підменю властивостей, що не змінюються equals()
Saving an object will change it's state
! hashCode
повинен повернутися int
, так як ти будеш користуватися getName()
? Чи можете ви навести приклад для свогоhashCode
Роз'яснення про obj.getClass() != getClass()
.
Це твердження є наслідком того, equals()
що спадкування є недружнім. У JLS (специфікації мови Java) вказує , що якщо A.equals(B) == true
потім B.equals(A)
повинні повернутися true
. Якщо ви опустите цей оператор, успадковуючи класи, які переосмислюють equals()
(і змінюють його поведінку), це специфікація порушить.
Розглянемо наступний приклад того, що відбувається, коли вислів опущено:
class A {
int field1;
A(int field1) {
this.field1 = field1;
}
public boolean equals(Object other) {
return (other != null && other instanceof A && ((A) other).field1 == field1);
}
}
class B extends A {
int field2;
B(int field1, int field2) {
super(field1);
this.field2 = field2;
}
public boolean equals(Object other) {
return (other != null && other instanceof B && ((B)other).field2 == field2 && super.equals(other));
}
}
Виконуючи new A(1).equals(new A(1))
також, new B(1,1).equals(new B(1,1))
результат видає правду, як слід.
Це виглядає дуже добре, але подивіться, що станеться, якщо ми спробуємо використовувати обидва класи:
A a = new A(1);
B b = new B(1,1);
a.equals(b) == true;
b.equals(a) == false;
Очевидно, це неправильно.
Якщо ви хочете забезпечити симетричну умову. a = b, якщо b = a, і принцип заміни Ліскова викликають super.equals(other)
не лише у випадку, B
наприклад, але перевіряють, наприклад, після A
:
if (other instanceof B )
return (other != null && ((B)other).field2 == field2 && super.equals(other));
if (other instanceof A) return super.equals(other);
else return false;
Що виведе:
a.equals(b) == true;
b.equals(a) == true;
Де, якщо a
не посилання B
, то це може бути як посилання класу A
(бо ви розширити його), в цьому випадку ви називаєте super.equals()
теж .
ThingWithOptionSetA
може дорівнювати за Thing
умови, що всі додаткові параметри мають значення за замовчуванням, і аналогічно для a ThingWithOptionSetB
, тоді a має бути можливим ThingWithOptionSetA
порівняння, рівне a, ThingWithOptionSetB
лише якщо всі неосновні властивості обох об'єктів відповідають їх за замовчуванням, але Я не бачу, як ви тестуєте на це.
B b2 = new B(1,99)
, то b.equals(a) == true
і a.equals(b2) == true
але b.equals(b2) == false
.
Для здійснення сприйняття успадкованості ознайомтеся з рішенням Тала Коена: Як я правильно реалізую метод рівних ()?
Підсумок:
У своїй книзі « Ефективний посібник з мовного програмування Java» (Аддісон-Уеслі, 2001) Джошуа Блох стверджує, що «просто немає способу розширити клас миттєвих дій і додати аспект при збереженні контракту на рівні». Тал не погоджується.
Його рішення полягає у здійсненні рівняння () шляхом виклику іншого несиметричного сліпого рівняння () обома способами. blindlyEquals () переосмислюється підкласами, equals () успадковується і ніколи не перекривається.
Приклад:
class Point {
private int x;
private int y;
protected boolean blindlyEquals(Object o) {
if (!(o instanceof Point))
return false;
Point p = (Point)o;
return (p.x == this.x && p.y == this.y);
}
public boolean equals(Object o) {
return (this.blindlyEquals(o) && o.blindlyEquals(this));
}
}
class ColorPoint extends Point {
private Color c;
protected boolean blindlyEquals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint)o;
return (super.blindlyEquals(cp) &&
cp.color == this.color);
}
}
Зауважте, що для задоволення принципу заміни Ліскова рівні () повинні працювати в ієрархіях успадкування .
if (this.getClass() != o.getClass()) return false
, але гнучко в тому, що воно повертає помилкове лише у тому випадку, коли похідний клас (и) заважають модифікувати рівняння. Це так?
Все ще дивується, що ніхто не рекомендував для цього бібліотеку гуави.
//Sample taken from a current working project of mine just to illustrate the idea
@Override
public int hashCode(){
return Objects.hashCode(this.getDate(), this.datePattern);
}
@Override
public boolean equals(Object obj){
if ( ! obj instanceof DateAndPattern ) {
return false;
}
return Objects.equal(((DateAndPattern)obj).getDate(), this.getDate())
&& Objects.equal(((DateAndPattern)obj).getDate(), this.getDatePattern());
}
this
у this.getDate()
значенні нічого (крім безладу)
if (!(otherObject instanceof DateAndPattern)) {
. Погодьтеся з Ернаном та Стівом Куо (хоча це - питання особистої переваги), але все-таки +1.
У суперкласі є два методи як java.lang.Object. Нам потрібно перекрити їх на спеціальний об'єкт.
public boolean equals(Object obj)
public int hashCode()
Рівні об'єкти повинні створювати один і той же хеш-код до тих пір, поки вони рівні, однак неоднакові об'єкти не повинні створювати чітких хеш-кодів.
public class Test
{
private int num;
private String data;
public boolean equals(Object obj)
{
if(this == obj)
return true;
if((obj == null) || (obj.getClass() != this.getClass()))
return false;
// object must be Test at this point
Test test = (Test)obj;
return num == test.num &&
(data == test.data || (data != null && data.equals(test.data)));
}
public int hashCode()
{
int hash = 7;
hash = 31 * hash + num;
hash = 31 * hash + (null == data ? 0 : data.hashCode());
return hash;
}
// other methods
}
Якщо ви хочете отримати більше, перевірте це посилання як http://www.javaranch.com/journal/2002/10/equalhash.html
Це ще один приклад, http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html
Веселіться! @. @
Є кілька способів зробити перевірку рівності класів перед тим, як перевірити рівність членів, і я думаю, що обидва корисні в правильних обставинах.
instanceof
оператором.this.getClass().equals(that.getClass())
.Я використовую №1 в final
рівній реалізації або при реалізації інтерфейсу, який прописує алгоритм для рівних (як java.util
інтерфейси колекції - правильний спосіб перевірити за допомогою (obj instanceof Set)
чи будь-якого інтерфейсу, який ви реалізуєте). Це, як правило, поганий вибір, коли рівняння можна перекрити, оскільки це порушує властивість симетрії.
Варіант №2 дозволяє безпечно розширювати клас, не змінюючи рівних чи порушуючи симетрію.
Якщо ваш клас також є Comparable
, equals
і compareTo
методи також повинні бути послідовними. Ось шаблон для методу рівних у Comparable
класі:
final class MyClass implements Comparable<MyClass>
{
…
@Override
public boolean equals(Object obj)
{
/* If compareTo and equals aren't final, we should check with getClass instead. */
if (!(obj instanceof MyClass))
return false;
return compareTo((MyClass) obj) == 0;
}
}
final
, а compareTo()
метод був скасований для зміни порядку сортування, екземпляри підкласу та надкласу не повинні вважатися рівними. Коли ці об'єкти використовуються разом у дереві, ключі, які були «рівні» відповідно до instanceof
реалізації, можливо, не підлягають обробці.
Для рівних, заглянути в секрети Рівних по Angelika Langer . Мені це дуже подобається. Вона також чудовий FAQ про дженерики на Java . Перегляньте її інші статті тут (прокрутіть униз до "Основна Java"), де вона також продовжується з частиною 2 та "зіставленням змішаного типу". Весело читаючи їх!
метод рівності () використовується для визначення рівності двох об'єктів.
як int значення 10 завжди дорівнює 10. Але метод дорівнює () - це рівність двох об'єктів. Коли ми говоримо об'єкт, він матиме властивості. Для вирішення питання рівності враховуються ці властивості. Не обов'язково, щоб усі властивості враховувались для визначення рівності, а щодо визначення класу та контексту можна визначити. Тоді метод equals () може бути замінений.
ми завжди повинні переосмислювати метод hashCode (), коли ми переосмислюємо метод equals (). Якщо ні, що буде? Якщо ми будемо використовувати хештелі у своєму додатку, він не буде вести себе так, як очікувалося. Оскільки хеш-код використовується для визначення рівності збережених значень, він не поверне потрібне відповідне значення для ключа.
Дана реалізація за замовчуванням метод hashCode () у класі Object використовує внутрішню адресу об'єкта та перетворює його в ціле число та повертає його.
public class Tiger {
private String color;
private String stripePattern;
private int height;
@Override
public boolean equals(Object object) {
boolean result = false;
if (object == null || object.getClass() != getClass()) {
result = false;
} else {
Tiger tiger = (Tiger) object;
if (this.color == tiger.getColor()
&& this.stripePattern == tiger.getStripePattern()) {
result = true;
}
}
return result;
}
// just omitted null checks
@Override
public int hashCode() {
int hash = 3;
hash = 7 * hash + this.color.hashCode();
hash = 7 * hash + this.stripePattern.hashCode();
return hash;
}
public static void main(String args[]) {
Tiger bengalTiger1 = new Tiger("Yellow", "Dense", 3);
Tiger bengalTiger2 = new Tiger("Yellow", "Dense", 2);
Tiger siberianTiger = new Tiger("White", "Sparse", 4);
System.out.println("bengalTiger1 and bengalTiger2: "
+ bengalTiger1.equals(bengalTiger2));
System.out.println("bengalTiger1 and siberianTiger: "
+ bengalTiger1.equals(siberianTiger));
System.out.println("bengalTiger1 hashCode: " + bengalTiger1.hashCode());
System.out.println("bengalTiger2 hashCode: " + bengalTiger2.hashCode());
System.out.println("siberianTiger hashCode: "
+ siberianTiger.hashCode());
}
public String getColor() {
return color;
}
public String getStripePattern() {
return stripePattern;
}
public Tiger(String color, String stripePattern, int height) {
this.color = color;
this.stripePattern = stripePattern;
this.height = height;
}
}
Приклад коду:
bengalTiger1 and bengalTiger2: true
bengalTiger1 and siberianTiger: false
bengalTiger1 hashCode: 1398212510
bengalTiger2 hashCode: 1398212510
siberianTiger hashCode: –1227465966
Один я знайшов те, де два об’єкти містять посилання один на одного (один приклад - це стосунки батька / дитини з зручним методом для батьків, щоб отримати всіх дітей).
Такі речі є досить поширеними, наприклад, коли робиться спляча сплячка.
Якщо ви включите обидва кінці відносини у свій хеш-код або дорівнює тестам, можливо потрапити в рекурсивний цикл, який закінчується StackOverflowException.
Найпростіше рішення - не включати колекцію getChildren у методи.
equals()
. Якби божевільний вчений створив мені дублікат, ми були б рівнозначними. Але у нас не було б того самого батька.