В чём разница между == и equals() в Java? Сравнение различных типов данных в Java

Хотя все учебники довольно ясно освещают этот вопрос, у новичков всё равно время от времени возникают проблемы с "ожидаемым поведением" их кода, вызванные рядом нюансов, особенно когда дело касается объектов класса String или Integer.

Объекты (общий случай)

Оператор == для ссылочных типов, в том числе объектов, сравнивает значения ссылок. Если две ссылочные переменные ссылаются на один и тот же объект в памяти машины, то данный оператор вернёт true. Например:
Object o1 = new Object();Object o2 = o1;
System.out.println(o1 == o2);
Данный код выведет true, так как o1 и o2 ссылаются на один и тот же объект.
Метод equals() наследуется всеми объектами от класса Object и поэтому может быть вызван на объекте любого типа. Предполагается, что разработчик класса переопределит этот метод тем или иным образом, чтобы задать логику определения, эквивалентны ли объекты друг другу.
Например, два разных объекта, моделирующих прямоугольники, могут считаться одинаковыми, если равны их размеры. Это разные объекты и == должно возвращать false, но это прямоугольники одинакового размера и equals() должно возвращать true. В коде это будет выглядеть так:
public static void main(String[] args) {
class Rectangle { int width, heigth;
Rectangle(int width, int heigth) { this.width = width; this.heigth = heigth; }
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Rectangle rectangle = (Rectangle) o; return width == rectangle.width && heigth == rectangle.heigth; }
@Override public int hashCode() { return Objects.hash(width, heigth); } }
Rectangle fst = new Rectangle(20, 10); Rectangle snd = new Rectangle(20, 10);
System.out.println(fst == snd); System.out.println(fst.equals(snd));
}
Во-первых, пусть не смущает тот факт (если вас он смущает), что класс Rectangle объявлен внутри метода main. Внутри методов можно объявлять классы. У Rectangle есть два поля: width и heigth, которые инициализируются в конструкторе. Строки:
System.out.println(fst == snd);System.out.println(fst.equals(snd));
выведут false и true соответственно, потому что fst и snd -- это разные объекты и == должен вернуть false, но с точки зрения метода equals -- это два одинаковых объекта. Рассмотрим переопределённый equals подробнее:
@Overridepublic boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Rectangle rectangle = (Rectangle) o; return width == rectangle.width && heigth == rectangle.heigth;}
Это типичный equals, автоматически сгенерированный средой разработки Intellij Idea.
  • В первой строке проверяется, не являются ли текущий объект и сравниваемый одним и тем же объектом. Один и тот же объект должен быть эквивалентен сам себе по определению.
  • Во второй строке проверяется, являются ли текущий и сравниваемый объекты объектами одного типа. Если нет, то они заведомо не эквивалентны.
  • В третьей строке сравниваемый объект приводится к текущему типу Rectangle.
  • И уже в четвёртой строке сравниваются значения width и heigth текущего и переданного объекта. И если они одинаковы, то возвращается true.

Кроме того, при переопределении equals() принято также переопределять метод hashCode(), чтобы, например, правильно работали коллекции объектов данного класса. Раньше по этому поводу писали различные правила, как это сделать хорошо. Теперь в общем случае все пользуются библиотечным методом Objects.hash(), в который передают все переменные поля класса.
Для equals() и hashCode() существует правило, согласно которому, если equals() говорит, что объекты одинаковы, то значения, которые возвращают их hashCode() должны совпадать. Но обратное -- необязательно. Два объекта могут возвращать один и тот же хеш-код, но не быть одинаковыми с точки зрения equals().
Итого. Переопределив метод equals(), мы можем считать одинаковыми два разных объекта, если совпадают их характеристики.
Если не переопределить метод equals() в разрабатываемом классе, то при его использовании будет задействован ближайший переопределённый equals() ближайшего предка. Если таковых предков не найдётся, то будет задействован метод equals(), унаследованный от Object. Этот метод выглядит так:
public boolean equals(Object obj) { return (this == obj);}
Как видно, он просто возвращает результат применения == к текущему и сравниваемому объекту и вернёт true, только если сравниваются переменные, указывающие на один и тот же объект.

Классы Integer и String

Для классов Integer и String Java использует так называемые пулы значений. Для одинаковых с обычной точки зрения значений берутся объекты из так называемого пула. Выражается это следующим образом:
Integer i1 = 1;Integer i2 = Integer.valueOf(1);
System.out.println(i1 == i2);
Данный код выведет на консоль true. Дело в том, что Java хранит в так называемом пуле 256 объектов Integer, представляющие числа от -128 до 127. И при создании объекта из литерала через боксинг как в первой строке и при создании объекта через метод Integer.valueOf() новые объекты не создаются, а берутся из пула. Но числа 128, например, в пуле уже нет. И если в данном примере заменить 1 на 128, то на консоли уже будет false.
Для объектов типа String java также поддерживает пул. В отличие от Integer'ов этот пул не стандартный, а формируется во время компиляции программы из строковых литералов и пополняется по мере исполнения программы при вызове на строке метода intern(). Таким образом следующий код выведет на консоль true:
String s1 = "java";String s2 = "java";
System.out.println(s1 == s2);
Соответственно, если очень нужно, чтобы два Integer'а или два String'а оказались разными объектами при том, что у них одинаковое значение, то нужно создавать новый объекты с помощью оператора new, и не полагаться на литералы:
String s1 = new String("java");String s2 = new String("java");
System.out.println(s1 == s2);
Код выведет false. Точно такой же эффект дал бы new Integer(1).

Константы времени компиляции

Главный подвох, который может ожидать неопытного разработчика заключается в том, что компилятор во время компиляции делает некоторые вычисления, результаты которых могут оказаться частью пула. Например:
String s1 = "java";String s2 = "ja" + "va";System.out.println(s1 == s2);
Integer i1 = 3;Integer i2 = 1+2;System.out.println(i1 == i2);
Данный код дважды выведет true. Действительно, компилятор, выполняя свою работу, делает некоторые известные оптимизационные преобразования. В том числе вычисляет константы времени компиляции. Результаты операций "ja" + "va" и 1+2 известны уже в момент компиляции, поэтому, чтобы не тратить на них вычислительные ресурсы во время выполнения программы, эти результаты будут вычислены сразу в момент компиляции, а дальше для них будут действовать правила попадания (или не попадания) в пулы.
Тройка для Integer и так содержится в пуле, поэтому и переменная i1, и переменная i2 в итоге ссылаются на один и тот же объект в пуле. Всё потому, что сумма была вычислена до запуска программы, в момент компиляции. Аналогичная ситуация с конкатенацией строк "ja" + "va".

Массивы

Массивы также относятся к объектным типам, но у них нельзя переопределить метод equals(). Поэтому следующий код:
Integer[] arr1 = {1,2,3};Integer[] arr2 = {1,2,3};
System.out.println(arr1 == arr2);System.out.println(arr1.equals(arr2));
дважды напечатает false. С поведением == вопросов быть не может, мы явно создаём два разных объекта. Но equals() в данном случае берётся от Object'а и под капотом просто использует тот же ==. Поэтому следующее поведение тоже не удивительно:
Integer[] arr1 = {1,2,3};Integer[] arr2 = arr1;
System.out.println(arr1 == arr2);System.out.println(arr1.equals(arr2));
Дважды выведет true, так как две переменные ссылаются на один объект.
Чтобы содержимое массивов сравнить через equals(), применённый к каждому элементу попарно, нужно воспользоваться библиотечным методом Arrays.equals():
Integer[] arr1 = {1,2,3};Integer[] arr2 = {1,2,3};
System.out.println(Arrays.equals(arr1, arr2));
Вернёт true. Под капотом этот метод убедится, что массивы одинаковой длины, и, что каждый элемент первого массива эквивалентен соответствующему элементу второго массива.
Обратите внимание, что метод вернёт false, если хотя бы один из массивов окажется null'ом. Но вернёт true, если оба массива будут null'ами.

Примитивные типы

Оператор == для примитивных типов сравнивает равенство значений переменных, например:
int i1 = 1;int i2 = 2;
System.out.println(i1 == i2);
Данный код выведет false, так как 1, очевидно, не равно 2. При сравнении примитивных типов сюрпризов в подавляющем большинстве случаев не бывает. Неожиданности может принести типы float и double.
Вычисления, проводимые над типами с плавающей точкой (float и double) являются неточными и применять к результатам таких вычислений операторы сравнения, в том числе == может быть опасно. Классический пример:
System.out.println(0.1 + 0.2 == 0.3);
Эта строка выведет false. Такова природа примитивных типов с плавающей точкой, вычисления с ними быстрые, но не точные:
System.out.println(0.1 + 0.2);
Выведет 0.30000000000000004
При этом, если дробные величины не вычислялись в коде программы, а получены из внешних источников (например, из БД), то обычно (! не всегда) их можно вполне безопасно сравнивать. Однако, и здесь нужно понимать, запрос в базу просто достал эти величины из таблички или база выполняла какие-то вычисления. Если база выполняла вычисления, то вполне реально получить такие же искажённые результаты, достаточные для непосредственного использования после округления, но неприменимые для операции сравнения.
В Java существует тип BigDecimal, операции с которым точные. Результаты этих операций могут сравниваться с помощью метода equals() и ведут себя интуитивно предсказуемо.

Метод equals() и NullPointerException

Integer i1 = null;Integer i2 = 0;System.out.println(i1.equals(i2));
Попытка вызвать метод equals() на переменной i1 приведёт к выбрасыванию NullPointerException. Часто мы не можем заранее быть уверены, что та или иная переменная ссылается на конкретный объект, а не равно null'у. А сравнивать объекты надо. Поэтому для таких ситуаций можно либо предварять вызов equals() соответствующими проверками переменной на равенство null'у (что громоздко), либо воспользоваться библиотечным методом Objects.equals():
Integer i1 = null;Integer i2 = 0;System.out.println(Objects.equals(i1, i2));
Данный код выведет false и, что само главное, не выбросит исключение.
Обратите внимание, выражение Objects.equals(null, null) возвращает true, что логично.