Статьи

Равенство против идентичности?

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

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

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

Метод hashCode () — реализуется виртуальной машиной как собственная операция, поэтому он не виден в коде, но часто реализуется как простой возврат ссылки на память (в 32-разрядных архитектурах) или представление модуля по модулю 32 ссылка на память (на 64-битной архитектуре).

Одна вещь, которую многие программисты предпочитают делать при разработке классов, это переопределить этот метод другим определением равенства, где вместо сравнения ссылки на память вы смотрите на значения двух экземпляров, чтобы увидеть, можно ли их считать равными. Вот пример этого:

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
import java.util.Objects;
import static java.util.Objects.requireNonNull;
 
public final class Person {
 
    private final String firstname;
    private final String lastname;
 
    public Person(String firstname, String lastname) {
        this.firstname = requireNonNull(firstname);
        this.lastname  = requireNonNull(lastname);
    }
 
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 83 * hash + Objects.hashCode(this.firstname);
        hash = 83 * hash + Objects.hashCode(this.lastname);
        return hash;
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        final Person other = (Person) obj;
        if (!Objects.equals(this.firstname, other.firstname)) {
            return false;
        } else return Objects.equals(this.lastname, other.lastname);
    }
}

Это сравнение называется «равенство» (по сравнению с предыдущим «идентичность»). Пока два человека имеют одинаковые имя и фамилию, они будут считаться равными. Например, это можно использовать для сортировки дубликатов из потока ввода. Помните, что если вы переопределяете метод equals (), вы всегда должны также переопределять метод hashCode ()!

равенство

Теперь, если вы выбираете равенство, а не идентичность, вам нужно подумать о некоторых вещах. Первое, что вы должны задать себе: два экземпляра этого класса с одинаковыми свойствами обязательно одинаковы? В случае человека выше, я бы сказал, нет. Весьма вероятно, что когда-нибудь в вашей системе появятся два человека с одинаковыми именем и фамилией. Даже если вы продолжите добавлять больше личной информации, такой как день рождения или любимый цвет, вы рано или поздно столкнетесь. С другой стороны, если ваша система обрабатывает автомобили, и каждая машина содержит ссылку на «модель», можно смело предположить, что, если обе машины имеют черную модель Tesla S, они, вероятно, являются одной и той же моделью, даже если объекты хранятся в разных местах в памяти. Это пример случая, когда равенство может быть хорошим.

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
import java.util.Objects;
import static java.util.Objects.requireNonNull;
 
public final class Car {
     
    public static final class Model {
         
        private final String name;
        private final String version;
         
        public Model(String name, String version) {
            this.name    = requireNonNull(name);
            this.version = requireNonNull(version);
        }
 
        @Override
        public int hashCode() {
            int hash = 5;
            hash = 23 * hash + Objects.hashCode(this.name);
            hash = 23 * hash + Objects.hashCode(this.version);
            return hash;
        }
 
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null) return false;
            if (getClass() != obj.getClass()) return false;
            final Model other = (Model) obj;
            if (!Objects.equals(this.name, other.name)) {
                return false;
            } else return Objects.equals(this.version, other.version);
        }
    }
     
    private final String color;
    private final Model model;
     
    public Car(String color, Model model) {
        this.color = requireNonNull(color);
        this.model = requireNonNull(model);
    }
     
    public Model getModel() {
        return model;
    }
}

Две машины считаются одинаковыми, только если у них одинаковый адрес памяти. Их модели, с другой стороны, считаются одинаковыми, если они имеют одинаковое имя и версию. Вот пример этого:

1
2
3
4
5
6
7
8
9
final Car a = new Car("black", new Car.Model("Tesla", "Model S"));
final Car b = new Car("black", new Car.Model("Tesla", "Model S"));
 
System.out.println("Is a and b the same car? " + a.equals(b));
System.out.println("Is a and b the same model? " + a.getModel().equals(b.getModel()));
 
// Prints the following:
// Is a and b the same car? false
// Is a and b the same model? true

тождественность

Один риск выбора равенства перед идентичностью состоит в том, что это может быть приглашением к выделению большего количества объектов, чем обязательно в куче. Просто посмотрите на пример автомобиля выше. Для каждого автомобиля, который мы создаем, мы также выделяем место в памяти для модели. Даже если Java обычно оптимизирует выделение строк для предотвращения дублирования, это все равно является определенной тратой для объектов, которые всегда будут одинаковыми. Короткая уловка, чтобы превратить внутренний объект во что-то, что можно сравнить с помощью метода сравнения идентичности и в то же время избежать ненужного выделения объекта, состоит в том, чтобы заменить его перечислением:

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
public final class Car {
     
    public enum Model {
         
        TESLA_MODEL_S ("Tesla", "Model S"),
        VOLVO_V70     ("Volvo", "V70");
         
        private final String name;
        private final String version;
         
        Model(String name, String version) {
            this.name    = name;
            this.version = version;
        }
    }
     
    private final String color;
    private final Model model;
     
    public Car(String color, Model model) {
        this.color = requireNonNull(color);
        this.model = requireNonNull(model);
    }
     
    public Model getModel() {
        return model;
    }
}

Теперь мы можем быть уверены, что каждая модель когда-либо будет существовать только в одном месте в памяти и поэтому может безопасно сравниваться с использованием сравнения идентичности. Однако проблема в том, что это действительно ограничивает нашу расширяемость. Раньше with мог определять новые модели на лету, не изменяя исходный код в файле Car.java, но теперь мы заперлись в перечислении, которое, как правило, должно оставаться неизменным. Если эти свойства желательны, сравнение с равными значениями, вероятно, лучше для вас.

Завершающее замечание: если вы переопределили методы класса equals () и hashCode () и позже хотите сохранить его в Map на основе идентификатора, вы всегда можете использовать структуру IdentityHashMap . Он будет использовать адрес памяти для ссылки на свои ключи, даже если методы equals () и hashCode () были переопределены.

Ссылка: Равенство против идентичности? от нашего партнера JCG Эмиля Форслунда из блога Age of Java .