В этой статье мы рассмотрим методики сравнения объектов в Java и какие инструменты нам для этого понадобятся. Как вы сможете понять из этой статьи задача сравнения объектов не такая уж, и тривиальная и выработать единый и универсальный подход для сравнения всех объектов едва ли представляется возможным. Сравнение объектов в Java выполняется с помощью оператора сравнения == и метода equals(). Если оператор сравнения можно использовать с примитивами, то метод equals() используется только с экземплярами классов. При их использовании проверяется ссылаются ли переменные на один и тот же объект в памяти. Рассмотрим пример:
1 2 3 4 5 |
Object ob1 = new Object(); Object ob2 = ob1; System.out.println(ob1 == ob2); // true System.out.println(ob1.equals(ob2)); //true |
Переменная ob1 содержит в себе ссылку на объект класса Object, а переменная ob2 просто ее копирует при объявлении, в итоге две переменных ссылаются на один и тот же объект в памяти, что при сравнении этих двух переменных всегда возвращается true. Другой пример:
1 2 3 4 5 |
Object ob1 = new Object(); Object ob2 = new Object(); System.out.println(ob1 == ob2); // false System.out.println(ob1.equals(ob2)); //false |
Теперь при объявлении каждой из переменных вызывается оператор new, который для каждой из них создает свой экземпляр класса Object, поэтому при сравнении этих двух переменных каждый раз возвращается false, потому что обе переменные указывают на разные объекты в разных участках памяти. Давайте теперь сравним что-то более конкретное, например, две строки:
1 2 3 4 5 |
String str1 = new String("ABC"); String str2 = new String("ABC"); System.out.println(str1 == str2); // false System.out.println(str1.equals(str2)); //true |
Как видите поведение метода equals() для класса String несколько отличается. Это происходит по одной простой причине, метод equals() определен в классе Object, поэтому все классы в Java его наследуют и вольны переопределять, что и было сделано в классе String. Как видите методом equals() проверяются на соответствие символы двух строк, если они совпадают, то метод возвращает true. Перед тем как приступить к переопределению метода equals() и комплексному сравнению объектов созданных нами классов, мы рассмотрим инструменты, которые нам для этого понадобятся: оператор instanceof и метод getClass().
Оператор instanceof.
Оператор instanceof служит для проверки к какому классу принадлежит объект. a instanceof B возвращает истину, если в переменной a содержится ссылка на экземпляр класса B, подкласс B (напрямую или косвенно, иначе говоря, состоит в иерархии наследования классов) или реализует интерфейс B (так же напрямую или косвенно). Давайте подробнее рассмотрим, как это работает на примере:
1 2 3 |
class A{} class B extends A{} class C extends A{} |
Иерархия классов очень проста, главный класс A, классы B и C от него наследуются. Посмотрим как поведет себя оператор instanceof с нашими классами:
1 2 3 4 |
A aB = new B(); System.out.println( aB instanceof B ); //true System.out.println( aB instanceof A ); //true System.out.println( aB instanceof C ); //false |
Первая проверка возвращает true, потому что aB является экземпляром класса B. Вторая проверка возвращает true, потому что aB является подклассом A. И последний вызов оператора instanceof возвращает false, несмотря на то, что оба класса B и С наследуются от A, это совершенно разные классы. На самом деле, ситуация с последним вызовом оператора instanceof сложнее, чем может показаться. Типом переменной aB является класс A, иначе говоря, она может содержать в себе любой из трех классов, в том числе класс C и компилятор это проверяет, если возникнет ситуация, при которой оператор instanceof не сможет ни при каких условиях вернуть true – возникнет ошибка компиляции:
1 2 |
B aB = new B(); System.out.println( aB instanceof C ); //ошибка компиляции |
Переменная с типом класса B на данном этапе нашей иерархии классов ни при каких условиях не может содержать в себе ссылку на класс С, поэтому возникает ошибка компиляции. Опять же, есть один нюанс, это правило не работает для интерфейсов:
1 2 3 4 |
interface I{} class A{} A a = new A(); System.out.println( a instanceof I ); //false |
Как мы видим связи между классом A и интерфейсом I нет никакой, и тем не менее ошибки компиляции не возникает, а оператор instanceof возвращает false. Всегда существует вероятность в будущем реализации интерфейса тем или иным классом, поэтому в данном случае компилятор идет нам уступки. Если же класс реализует интерфейс, то оператор instanceof вернут true:
1 2 3 4 |
interface I{} class A implements I{} A a = new A(); System.out.println( a instanceof I ); //true |
Как упоминалось ранее все классы наследуются от класса Object либо напрямую, либо через родительский класс, поэтому a instanceof Object всегда будет возвращать true за исключение одной ситуации:
1 2 3 4 |
interface I{} class A implements I{} A a = new A(); System.out.println( a instanceof I ); //true |
Так вот, a instanceof Object всегда будет возвращать true, только если a не равна null.
Метод getClass().
Метод getClass() возвращает класс объекта, содержащий сведения об объекте: public final Class<?> getClass(). Как Вы можете заметить метод является конечным и переопределению не подлежит. С методом getClass() все обстоит несколько проще и очевидней нежели с оператором instanceof.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package example; class A{} class B extends A{}; public class Example{ public static void main(String... args){ A a = new A(); A a1 = new A(); B b = new B(); A ab = new B(); System.out.println(a.getClass() == a1.getClass()); // true System.out.println(a.getClass() == b.getClass()); // false System.out.println(a.getClass() == ab.getClass()); // false } } |
Как видите, метод прост и эффективен, ровно до тех пор, пока не применен механизм наследования. При наследовании этот метод становится менее полезным. Это утверждение может многим показаться спорным, но мы к нему еще вернемся в конце главы, когда будем размышлять о том, каким должен быть идеальный метод для сравнения объектов.
Метод equals().
Почему мы так подробно рассматривали оператор instanceof? Потому что это будет наша первая проверка, хотя многие предпочитают использовать метод getClass(), но обо всем по порядку. Как говорилось выше, метод equals() содержится в классе Object и наследуется всеми классами, в которых мы вправе его переопределить, давайте это сделаем:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Eq{ @Override public boolean equals(Object o){ return true; } } public class Test { public static void main(String[] args) { Eq eq1 = new Eq(); Eq eq2 = new Eq(); System.out.println(eq1.equals(eq2)); //true } } |
В нашем примере метод equals() всегда будет возвращать true, что и было продемонстрировано. Не смотря на то, что в памяти было создано два объекта Eq, метод equals() все равно вернул истину. Единственное на что хотелось бы обратить внимание, это аннотация @Override. Она используется для явного обозначения переопределения метода, если бы мы допустили ошибку и просто перегрузили метод, то с этой аннотацией у нас бы возникла ошибка компиляции:
1 2 3 4 5 6 |
class Eq{ @Override // ошибка компиляции public boolean equals(Eq l){ return true; } } |
В примере выше метод equals() перегружен (в качестве параметра методу передается класс Eq вместо Object), что и приводит к ошибке компиляции. Настало время рассмотреть более сложный пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Car{ private String name; public Car(String name){ this.name = name; } @Override // ошибка компиляции public boolean equals(Object obj){ if (!(obj instanceof Car)) return false; Car car = (Car) obj; return this.name.equals(car.name); } } public class Test { public static void main(String[] args) { Car car1 = new Car("AudiA3"); Car car2 = new Car("AudiA3"); Car car3 = new Car("AudiA8"); System.out.println(car1 == car2); //false System.out.println(car1.equals(car2)); //true System.out.println(car1.equals(car3)); //false } } |
В классе Car происходит более комплексная проверка на равенство объектов: первоначально оператор instanceof проверяет, принадлежат ли классы одной иерархии наследования, если нет, то объекты не равны, если принадлежат, то проверка идет по строковому полю name, при совпадении этого поля (ну или при совпадении названия машин) объекты будут равны. Рассмотрим пример сравнения объектов с помощью метода getClass():
Так как метод equals() является одним из ключевых в Java, существует ряд правил касающихся его работы:
- Рефлексивность. При вызове x.equals(x) по любой ненулевой ссылке x должно возвращаться логическое значение true.
- Симметричность. При вызове x.equals(y) по любым ссылкам x и y должно возвращаться логическое значение true, тогда и только тогда, когда при вызове y.equals(x) возвращается логическое значение true.
- Транзитивность. Если при вызовах x.equals(y) и y.equals(z) по любым ссылкам x, y и z возвращается логическое значение true, то и при вызове x.equls(z) возвращается логическое значение true.
- Согласованность. Если объекты, на которые делаются ссылки x и y, не изменяются, то при повторном вызове x.equals(y) должно возвращаться тоже самое значение.
При вызове x.equals(null) по любой непустой ссылке x, должно возвращаться логическое значение false.
Метод hashCode()
Хэш-код — это целое число, генерируемое на основе конкретного объекта. Метод hashCode() тесно взаимосвязан с методом equals() (если x и y – разные объекты, то с высокой долей вероятности должны различаться результаты вызовов hashCode() для этих объектов), поэтому Oracle рекомендует при переопределении метода equals() так же переопределять метод hashCode():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Car{ private String name; public Car(String name){ this.name = name; } @Override public boolean equals(Object obj){ if (!(obj instanceof Car)) return false; Car car = (Car) obj; return this.name.equals(car.name); } @Override public int hashCode(){ return name.hashCode(); } } |
Теперь класс Car помимо метода equals() еще переопределяет метод hashCode(), который возвращает хэш-код строкового поля name. Пример с классом машин был сознательно упрощен, конечно, если вы занимаетесь разработкой классов, помимо переопределения рассматриваемых методов, не помешает проверка на передачу в конструктор пустых значений (null) или воспользоваться методом Objects.hashCode, который возвращает 0 если его аргумент имеет пустое значение (для Java 7 и выше). В 7ой версси Java был добавлен еще один замечательный метод Objects.hash(), который в качестве параметра получает аргумент переменной длины (varargs) и возвращает хэш-код состоящий из хэш-кодов переданных элементов: public static int hash(Object… values). Пример использования:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package example; import java.util.Objects; class Car{ private int id; private String name; public Car(int id, String name){ this.id = id; this.name = name; } public int hashCode(){ return Objects.hash(id, name); } } public class Example{ public static void main(String... args){ Car car = new Car(156723, "Audi"); System.out.println(car.hashCode()); } } |
Вывод:
6911431
Как видите получить хэш-код для двух переменных не так сложно – всего одна строчка кода! Класс Objects содержит в себе много полезных методов и я вам настоятельно рекомендую изучить его более подробно.
В попытке достичь совершенства.
В стандартной библиотеке Java содержится более 150 реализаций метода equals(). Давайте добавим к ним свою реализацию. Для начала следует определиться, каким способом мы будем сравнивать классы: с помощью оператора instanceof или метода getClass(). Однозначного решения здесь не существует у каждого из этих способов есть свои недостатки.
Приступим к реализации:
1. Всегда используйте аннотацию @Override это не только сделает Ваш код более читабельным, но и позволит избежать ошибки с перегрузкой метода equals().
2. Первое, что нужно будет сделать, это проверить совпадают ли ссылки на представленные объекты, если да, то объекты равны и дальнейшая их проверка не имеет смысла:
1 |
if (this == compareObject) return true; |
3. Далее нужно обязательно проверить является ли ссылка compareObject пустой (null), если да то следует вернуть логическое значение false:
1 |
if (compareObject == null) return false; |
4. Настало время сравнить классы. Если семантика сравнения может измениться в подклассе, лучшим выбором для сравнения будет метод getClass():
1 |
if (getClass() != compareObject.getClass()) return false; |
В ином случае проверку необходимо производить с помощью метода instanceof:
1 |
if (!(compareObject instanceof Имя_класса)) return false; |
5. Далее приведите тип объекта compareObject к типу переменной требуемого класса:
1 |
Имя_класса object = (Имя_класса) compareObject; |
6. Определитесь какие поля класса вы будете сравнивать, для примитивных типов используйте оператор сравнения ==, для ссылочных типов метод equals():
1 |
return имя_поля == object.имя_поля && имя_поля.equals(object.имя_поля); |
Давайте посмотрим на общую картину:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
package example; import java.util.Objects; class Car{ private int id; private String name; public Car(Integer id, String name){ this.id = id; this.name = name; } public int hashCode(){ return Objects.hash(id, name); } @Override public boolean equals(Object compareObject){ if (this == compareObject) return true; if (compareObject == null) return false; //if (!(compareObject instanceof Car)) return false; if (getClass() != compareObject.getClass()) return false; Car object = (Car) compareObject; return id == object.id && name.equals(object.name); } } public class Example{ public static void main(String... args){ Car car = new Car(156723, "Audi"); Car car1 = new Car(156723, "Audi"); System.out.println(car.equals(car1)); // true System.out.println(car.hashCode() == car1.hashCode()); //true } } |
Обратите внимание, что в примере, у одинаковых объектов хэш-код совпадает.
Вместо послесловия
Так же как и гвозди можно забивать как камнем, так и молотком, так и объекты можно сравнивать с использованием готовых инструментов. В этом нам поможет одна замечательная библиотека, которая сделает все за нас — Apache Commons Lang. Вообще, проект Apache Commons очень интересен сам по себе и я о нем вам расскажу более подробно в будущих статьях, а пока вернемся к методу equals().
1 2 3 4 5 |
@Override public boolean equals(Object compareObject){ if (compareObject == null) return false; return EqualsBuilder.reflectionEquals(this, compareObject); } |
Как видите, эта библиотека не избавляет нас от проверки на передачу в метод пустой ссылки. В примере выше проверяются на равенство все поля классов, если нужно сравнивать конкретные поля, то это необходимо указать:
1 2 3 4 5 6 7 8 |
@Override public boolean equals(Object compareObject){ if (compareObject == null) return false; if (getClass() != compareObject.getClass()) return false; Car object = (Car) compareObject; return new EqualsBuilder().appendSuper(super.equals(object)) .append(id, object.id).isEquals(); } |
Уже выглядит не так элегантно как хотелось бы, теперь нам надо проверять принадлежность объектов одному и тому же классу и приводить типы.
Если у Вас есть какие-либо предложения по совершенствованию переопределения метода equals(), Вы их можете направить на адрес javanerd.ru@gmail.com и я их обязательно включу в статью.