Статьи

Использование методов, общих для всех объектов

Эта статья является частью нашего Академического курса под названием Advanced Java .

Этот курс призван помочь вам наиболее эффективно использовать Java. В нем обсуждаются сложные темы, включая создание объектов, параллелизм, сериализацию, рефлексию и многое другое. Он проведет вас через ваше путешествие в мастерство Java! Проверьте это здесь !

1. Введение

Из первой части руководства « Как создавать и уничтожать объекты» мы уже знаем, что Java является объектно-ориентированным языком (однако не чисто объектно-ориентированным). На вершине иерархии классов Java находится класс Object и каждый отдельный класс в Java неявно наследуется от него. Таким образом, все классы наследуют набор методов, объявленных в классе Object , наиболее важными из которых являются следующие:

метод Описание
protected Object clone() Создает и возвращает копию этого объекта.
protected void finalize() Вызывается сборщиком мусора на объекте, когда сборщик мусора определяет, что больше нет ссылок на объект. Мы обсудили финализаторы в первой части урока « Как создавать и уничтожать объекты» .
boolean equals(Object obj) Указывает, равен ли какой-либо другой объект этому.
int hashCode() Возвращает значение хеш-кода для объекта.
String toString() Возвращает строковое представление объекта.
void notify() Просыпается один поток, который ожидает на мониторе этого объекта. Мы собираемся обсудить этот метод в части 9 учебника « Лучшие практики параллелизма» .
void notifyAll() Пробуждает все потоки, которые ожидают на мониторе этого объекта. Мы собираемся обсудить этот метод в части 9 учебника « Лучшие практики параллелизма» .
void wait()

void wait(long timeout)

void wait(long timeout, int nanos)

Заставляет текущий поток ждать, пока другой поток не notifyAll() метод notifyAll() или метод notifyAll() для этого объекта. Мы собираемся обсудить эти методы в части 9 учебника « Лучшие практики параллелизма» .

Таблица 1

В этой части руководства мы рассмотрим методы equals , hashCode , toString и clone , их использование и важные ограничения, о которых следует помнить.

2. Методы equals и hashCode

По умолчанию любые две ссылки на объекты (или ссылки на экземпляры классов) в Java равны, только если они ссылаются на одну и ту же область памяти (равенство ссылок). Но Java позволяет классам определять свои собственные правила равенства, переопределяя метод equals() класса Object . Это звучит как мощная концепция, однако правильная реализация метода equals() должна соответствовать набору правил и удовлетворять следующим ограничениям:

  • Рефлексивный Объект x должен быть равен самому себе, и метод equals (x) должен возвращать true .
  • Симметричный Если equals (y) возвращает true, то y.equals (x) также должен возвращать true .
  • Переходный . Если equals (y) возвращает true, а y.equals (z) возвращает true , то x.equals (z) также должен возвращать true .
  • Последовательный Многократный вызов метода equals () должен приводить к одному и тому же значению, если только не изменены какие-либо свойства, используемые для сравнения на равенство.
  • Равно нулю . Результат equals (null) должен всегда быть ложным .

К сожалению, компилятор Java не может применять эти ограничения в процессе компиляции. Тем не менее, несоблюдение этих правил может привести к очень странным и сложным проблемам. Общий совет таков: если вы когда-нибудь собираетесь написать собственную реализацию метода equals() , подумайте дважды, действительно ли вам это нужно. Теперь, вооружившись всеми этими правилами, давайте напишем простую реализацию метода equals() для класса Person .

01
02
03
04
05
06
07
08
09
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.javacodegeeks.advanced.objects;
 
public class Person {
    private final String firstName;
    private final String lastName;
    private final String email;
     
    public Person( final String firstName, final String lastName, final String email ) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }
     
    public String getEmail() {
        return email;
    }
     
    public String getFirstName() {
        return firstName;
    }
     
    public String getLastName() {
        return lastName;
    }
 
    // Step 0: Please add the @Override annotation, it will ensure that your
    // intention is to change the default implementation.
    @Override
    public boolean equals( Object obj ) {
        // Step 1: Check if the 'obj' is null
        if ( obj == null ) {
            return false;
        }
         
        // Step 2: Check if the 'obj' is pointing to the this instance
        if ( this == obj ) {
            return true;
        }
         
        // Step 3: Check classes equality. Note of caution here: please do not use the
        // 'instanceof' operator unless class is declared as final. It may cause
        // an issues within class hierarchies.
        if ( getClass() != obj.getClass() ) {
            return false;
        }
         
        // Step 4: Check individual fields equality
        final Person other = (Person) obj;
        if ( email == null ) {
            if ( other.email != null ) {
                return false;
            }
        } else if( !email.equals( other.email ) ) {
            return false;
        }
         
        if ( firstName == null ) {
            if ( other.firstName != null ) {
                return false;
            }
        } else if ( !firstName.equals( other.firstName ) ) {
            return false;
        }
             
        if ( lastName == null ) {
            if ( other.lastName != null ) {
                return false;
            }
        } else if ( !lastName.equals( other.lastName ) ) {
            return false;
        }
         
        return true;
    }       
}

Не случайно этот раздел также включает в свой заголовок метод hashCode() . Последнее, но не менее важное правило, которое следует помнить: всякий раз, когда вы переопределяете метод equals() , всегда также переопределяйте метод hashCode() . Если для любых двух объектов метод equals() возвращает true , то метод hashCode() для каждого из этих двух объектов должен возвращать одно и то же целочисленное значение (однако противоположное утверждение не столь строгое: если для любых двух объектов equals() Метод возвращает false , метод hashCode() для каждого из этих двух объектов может возвращать или не возвращать одно и то же целочисленное значение). Давайте посмотрим на метод hashCode() для класса Person .

01
02
03
04
05
06
07
08
09
10
11
12
13
// Please add the @Override annotation, it will ensure that your
// intention is to change the default implementation.
@Override
public int hashCode() {
    final int prime = 31;
         
    int result = 1;
    result = prime * result + ( ( email == null ) ? 0 : email.hashCode() );
    result = prime * result + ( ( firstName == null ) ? 0 : firstName.hashCode() );
    result = prime * result + ( ( lastName == null ) ? 0 : lastName.hashCode() );
         
    return result;
}     

Чтобы защитить себя от неожиданностей, по возможности старайтесь использовать final поля при реализации equals() и hashCode() . Это гарантирует, что на поведение этих методов не будут влиять изменения поля (однако в реальных проектах это не всегда возможно).

Наконец, всегда проверяйте, чтобы одни и те же поля использовались в реализации методов equals() и hashCode() . Это гарантирует согласованное поведение обоих методов в случае любых изменений, затрагивающих рассматриваемые поля.

3. Метод toString

toString() , возможно, является наиболее интересным методом среди других и его переопределяют чаще. Его целью является предоставление строкового представления объекта (экземпляра класса). Правильно написанный метод toString() может значительно упростить отладку и устранение неполадок в реальных системах.

Реализация по умолчанию toString() не очень полезна в большинстве случаев и просто возвращает полное имя класса и хеш-код объекта, разделенные @ , fe:

1
com.javacodegeeks.advanced.objects.Person@6104e2ee

Давайте попробуем улучшить реализацию и переопределить метод toString() для нашего примера класса Person . Вот один из способов сделать toString() более полезным.

1
2
3
4
5
6
7
// Please add the @Override annotation, it will ensure that your
// intention is to change the default implementation.
@Override
public String toString() {
    return String.format( "%s[email=%s, first name=%s, last name=%s]",
        getClass().getSimpleName(), email, firstName, lastName );
}

Теперь метод toString() предоставляет строковую версию экземпляра класса Person со всеми включенными в него полями. Например, при выполнении фрагмента кода ниже:

1
2
final Person person = new Person( "John", "Smith", "john.smith@domain.com" );
System.out.println( person.toString() );

Следующий вывод будет распечатан в консоли:

1
Person[email=john.smith@domain.com, first name=John, last name=Smith]

К сожалению, стандартная библиотека Java имеет ограниченную поддержку для упрощения реализации метода toString() , в частности, наиболее полезными методами являются Objects.toString() , Arrays.toString() / Arrays.deepToString() . Давайте посмотрим на класс Office и его возможную реализацию toString() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package com.javacodegeeks.advanced.objects;
 
import java.util.Arrays;
 
public class Office {
    private Person[] persons;
 
    public Office( Person ... persons ) {
         this.persons = Arrays.copyOf( persons, persons.length );
    }
     
    @Override
    public String toString() {
        return String.format( "%s{persons=%s}",
            getClass().getSimpleName(), Arrays.toString( persons ) );
    }
     
    public Person[] getPersons() {
        return persons;
    }
}

Следующий вывод будет выведен на консоль (как мы видим, экземпляры класса Person также правильно преобразуются в строку):

1
Office{persons=[Person[email=john.smith@domain.com, first name=John, last name=Smith]]}

Сообщество Java разработало несколько довольно всеобъемлющих библиотек, которые очень помогают сделать реализации toString() безболезненными и легкими. Среди них объекты Google Guava's Objects.toStringHelper и Apache Commons Lang ToStringBuilder .

4. Метод клонирования

Если в Java есть метод с плохой репутацией, это определенно clone() . Его цель очень ясна — вернуть точную копию экземпляра класса, к которому он вызывается, однако есть несколько причин, почему это не так просто, как кажется.

Прежде всего, если вы решили реализовать свой собственный метод clone() , существует множество соглашений, которым необходимо следовать, как указано в документации по Java . Во-вторых, метод объявлен protected в классе Object поэтому, чтобы сделать его видимым, он должен быть переопределен как public с типом возврата самого переопределяющего класса. В-третьих, переопределяющий класс должен реализовывать интерфейс Cloneable (который является просто интерфейсом маркера или миксина без методов), в противном случае будет CloneNotSupportedException исключение CloneNotSupportedException . И наконец, реализация должна сначала вызвать super.clone() а затем выполнить дополнительные действия, если это необходимо. Давайте посмотрим, как это можно реализовать для нашего примера класса Person .

1
2
3
4
5
6
7
8
public class Person implements Cloneable {
    // Please add the @Override annotation, it will ensure that your
    // intention is to change the default implementation.
    @Override
    public Person clone() throws CloneNotSupportedException {
        return ( Person )super.clone();
    }
}

Реализация выглядит довольно просто и понятно, так что здесь может пойти не так? Пара вещей, на самом деле. Пока выполняется клонирование экземпляра класса, конструктор класса не вызывается. Следствием такого поведения является то, что может произойти непреднамеренный обмен данными. Давайте рассмотрим следующий пример класса Office , представленный в предыдущем разделе:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package com.javacodegeeks.advanced.objects;
 
import java.util.Arrays;
 
public class Office implements Cloneable {
    private Person[] persons;
 
    public Office( Person ... persons ) {
         this.persons = Arrays.copyOf( persons, persons.length );
    }
 
    @Override
    public Office clone() throws CloneNotSupportedException {
        return ( Office )super.clone();
    }
     
    public Person[] getPersons() {
        return persons;
    }
}

В этой реализации все клоны экземпляра класса Office будут использовать один и тот же массив лиц, что вряд ли приведет к желаемому поведению. Нужно немного поработать, чтобы реализация clone() работала правильно.

1
2
3
4
5
6
@Override
public Office clone() throws CloneNotSupportedException {
    final Office clone = ( Office )super.clone();
    clone.persons = persons.clone();
    return clone;
}

Сейчас это выглядит лучше, но даже эта реализация очень хрупкая, так как создание поля для лиц, которые будут final , приведет к тем же проблемам обмена данными (поскольку final не может быть переназначен)

В общем, если вы хотите делать точные копии ваших классов, вероятно, лучше избегать clone() / Cloneable и использовать гораздо более простые альтернативы (например, конструктор копирования, довольно знакомая концепция для разработчиков с фоном C ++ или фабрика метод, полезный шаблон построения, который мы обсудили в первой части урока « Как создавать и уничтожать объекты» ).

5. Метод равно и == оператор

Существует интересная связь между оператором Java == и методом equals (), которая вызывает много проблем и путаницы. В большинстве случаев (кроме сравнения примитивных типов) оператор == выполняет равенство ссылок: он возвращает истину, если обе ссылки указывают на один и тот же объект, и ложь в противном случае. Давайте посмотрим на простой пример, который иллюстрирует различия:

1
2
3
final String str1 = new String( "bbb" );
System.out.println( "Using == operator: " + ( str1 == "bbb" ) );
System.out.println( "Using equals() method: " + str1.equals( "bbb" ) );

С точки зрения человека, нет никаких различий между str1 == ”bbb” и str1.equals (“bbb”): в обоих случаях результат должен быть таким же, как str1 — просто ссылка на строку “bbb”. Но в Java это не так:

1
2
Using == operator: false
Using equals() method: true

Даже если обе строки выглядят одинаково, в этом конкретном примере они существуют как два разных экземпляра строки. Как правило, если вы имеете дело со ссылками на объекты, всегда используйте equals() или Objects.equals() (см. Подробности в следующем разделе « Полезные вспомогательные классы» ) для сравнения на равенство, если только у вас нет намерения сравнивать если ссылки на объекты указывают на один и тот же экземпляр.

6. Полезные вспомогательные классы

Начиная с выпуска Java 7, есть пара очень полезных вспомогательных классов, включенных в стандартную библиотеку Java. Одним из них является класс Objects . В частности, следующие три метода могут значительно упростить ваши собственные hashCode() методов equals() и hashCode() .

метод Описание
static boolean equals(Object a, Object b) Возвращает true, если аргументы равны друг другу, и false в противном случае.
static int hash(Object... values) Создает хэш-код для последовательности входных значений.
static int hashCode(Object o) Возвращает хеш-код ненулевого аргумента и 0 для нулевого аргумента.

Таблица 2

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
public boolean equals( Object obj ) {
    if ( obj == null ) {
        return false;
    }
         
    if ( this == obj ) {
        return true;
    }
         
    if ( getClass() != obj.getClass() ) {
        return false;
    }
         
    final PersonObjects other = (PersonObjects) obj;
    if( !Objects.equals( email, other.email ) ) {
        return false;
    } else if( !Objects.equals( firstName, other.firstName ) ) {
        return false;           
    } else if( !Objects.equals( lastName, other.lastName ) ) {
        return false;           
    }
         
    return true;
}
         
@Override
public int hashCode() {
    return Objects.hash( email, firstName, lastName );
}     

7. Загрузите исходный код

8. Что дальше

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