Как работает оператор проверки на равенство (оператор ==)?
В каких ситуациях следует его применять?
Имеются ли ему альтернативы?
Ответы
Ответ 1
Оператор ==
В Java оператор == возвращает значение типа boolean - результат сравнения экземпляро
объектов, либо примитивов. Поведение операции сравнения зависит от типов её операндов (объектных, либо примитивных типов).
Сравнение объектов
Если оба операнда являются экземплярами объектных типов, то данный оператор прост
проверяет равенство ссылок (указывают ли ссылки, значения которых хранятся в сравниваемых переменных, на один и тот-же объект в Heap'е), а не сравнивает на равенство внутреннее содержимое объектов.
Таким образом, при сравнении объектов в Java оператор == вернет true лишь в том случае, когда ссылки указывают на один и тот-же объект.
Сравнение примитивов
Сравнение примитивных типов происходит в следующих ситуациях:
оба операнда являются экземплярами примитивных типов (byte, short, int, long, char, float, либо double);
если хотя бы один из операндов являются экземпляром примитивного типа (byte, short
int, long, char, float, либо double), а второй является экземпляром объектного тип
обертки (Wrapper Class) над примитивным (Byte, Short, Integer, Long, Character, Float
либо Double), то отработает механизм autounboxing (у экземпляра объектного типа будет вызван соответствующий этому типу метод: byteValue(), shortValue(), intValue(), longValue(), charValue(), floatValue(), либо doubleValue()), после чего мы получаем два экземпляра примитивных типов (возможно, разных - см. следующий абзац, подробнее см. здесь).
Алгоритм сравнения:
Сначала операнды распространяются (т.е. происходит арифметическое распространени
(promotion) операндов) до наибольшего, если это необходимо (если операнды являются экземплярами разных примитивных типов), а затем происходит непосредственное побитовое сравнение (если все биты равны, то возвращаемое значение будет true, иначе - false).
Контрольный пример:
Например, значение 48.0f (типа float, соответственно) равно значению '0' (код которог
равен 48 согласно таблице символов Unicode) типа char, значение которого неявно распространяется до типа float.
char char1 = '0';
int int1 = 48;
float float1 = 48.0F;
Integer integer1 = 48; // произойдет вызов: Integer.valueOf(48);
System.out.println(char1 == int1); // true
System.out.println(char1 == float1); // true
System.out.println(int1 == float1); // true
System.out.println(int1 == integer1); // true (произойдет вызов: int2.intValue();)
// ---------------------------------------------
char char2 = 'A';
long long2 = 65L;
float float2 = 65.0F;
Long longInteger2 = 65L; // произойдет вызов: Long.valueOf(65L);
System.out.println(char2 == long2); // true
System.out.println(char2 == float2); // true
System.out.println(long2 == float2); // true
System.out.println(long2 == longInteger2); // true (произойдет вызов: longInteger2.longValue();)
Сравнение вещественных примитивов
Для вещественных примитивов (float и double) существуют некоторые особенности, которы
необходимо учитывать. Представление вещественных чисел в Java соответствует стандарт
IEEE 754, т.е. число представлено в виде мантисы (значимой части) и порядка (степени
в которую возводится основание/база системы) откуда следует то, что как целая, так и дробная часть числа представление с помощью конечного числового ряда 2^(n) (где как максимальное, так и минимальное значение числа n зависит как от размера мантисы и порядка, которые, в свою очередь, зависят от конкретного типа: float, либо double), а потому о точном представлении произвольно взятого числа говорить не приходится.
Контрольный пример:
Примеры представления чисел с плавающей точкой в памяти машины согласно стандарту IEEE 754 можно найти здесь.
Само собой разумеется, что две вещественные переменные, проинициализированные одни
и тем же вещественным литералом, будут равны (ведь они проинициализировались одной и той же последовательностью бит):
float float1 = 0.7F;
float float2 = 0.7F;
System.out.println(float1 == float2); // true
Но стоит начать выполнять арифметические операции с переменными данного типа, как
скорее всего, начнет накапливаться погрешность вычислений (из-за указанных выше особенностей представления экземпляров данного типа):
float float1 = 0.7F;
float float2 = 0.3F + 0.4F;
System.out.println(float1 == float2); // false
System.out.println(float1); // 0.7
System.out.println(float2); // 0.70000005
Как следует сравнивать вещественные примитивы
Вещественные примитивы (float и double) стоит сравнивать с определенной точностью
Например, округлять их до 6-го знака после запятой (1E-6 для double, либо 1E-6F для float), либо, что предпочтительнее, проверять абсолютное значение разницы между ними.
Контрольный пример:
float float1 = 0.7F;
float float2 = 0.3F + 0.4F;
final float EPS = 1E-6F;
System.out.println(Math.abs(float1 - float2) < EPS); // true
Стоит понимать, что существуют и другие более точные, но при этом куда более изощренные варианты сравнения чисел с плавающей точкой.
Использование Pool'ов
Как уже было сказано выше, оператор == проверяет указывают ли ссылки на один и то
же объект, но иногда под этим простым выражением скрывается нечто большее, чем может показаться на первый взгляд.
Для более эффективного использования памяти, в Java используются так называемые пул
(Boolean, Short, Integer, Long и String). Когда мы создаем объект, не используя операто
new (посредством литералов, либо с использованием механизма autoboxing), объект помещаетс
в пул, и в последствии, если мы захотим создать такой же объект (опять же, без используя оператора new), то новый объект создан не будет, а мы просто получим ссылку на наш объект из пула, т.е. по факту один и тот же объект будет переиспользован в нескольких местах (такое поведение допустимо для объектов вышеописанных типов, т.к. они являются immutable).
Примечание:
Стоит заметить, что использование любого Pool'а за исключением String будет осуществлятьс
при любой автоупаковке (если, конечно, значение соответсвует определенному диапазону, зависящему от типа, см. autoboxing). В случае же Pool'а String интернирование (intern) работает только для литералов.
Boolean pool
Boolean pool хранит оба возможных значения, т.е. объектные Wrapper'ы для обоих примитивов (true и false).
Контрольный пример:
Boolean valueFromPool1 = true; // произойдет вызов: Boolean.valueOf(true);
Boolean valueFromPool2 = true; // произойдет вызов: Boolean.valueOf(true);
System.out.println(valueFromPool1 == valueFromPool2); // true
System.out.println(true == valueFromPool1); // true
System.out.println((Boolean) true == valueFromPool1); // true
Short / Integer / Long pool
Особенность целочисленных (Short, Integer и Long) pool'ов состоит в том, что, п
умолчанию, они хранят только числа, которые помещаются в тип данных byte, т.е. числ
в интервале от -128 до 127 (который начиная с Java 7 (раньше это было захардкожено внутр
классов java.lang.Short, java.lang.Integer и java.lang.Long, соответственно) можно расширить с помощью опции JVM, пример для Integer: -Djava.lang.Integer.IntegerCache.high=size или -XX:AutoBoxCacheMax=size). Для остальных чисел данных типов pool не работает.
Почему отсутствуют Float и Double pool:
Для чисел с плавающей точкой (Float, Double) отсутствуют специфичные pool'ы вследстви
особенностей хранения таких чисел согласно стандарту IEEE 754. Использование для них pool'ов оказалось бы крайне неэффективно (хотя бы, в виду огромного количества чисел с плавающей точкой, находящихся в интервале от -128.0 до 127.0).
Контрольный пример:
Integer valueFromPool1 = 127; // произойдет вызов: Integer.valueOf(127);
Integer valueFromPool2 = 127; // произойдет вызов: Integer.valueOf(127);
Integer valueNotFromPool1 = 128; // произойдет вызов: Integer.valueOf(128);
Integer valueNotFromPool2 = 128; // произойдет вызов: Integer.valueOf(128);
System.out.println(valueFromPool1 == valueFromPool2); // true
System.out.println(valueNotFromPool1 == valueNotFromPool2); // false
System.out.println(127 == valueFromPool1); // true
System.out.println(128 == valueNotFromPool1); // true
System.out.println((Integer) 127 == valueFromPool1); // true
System.out.println((Integer) 128 == valueNotFromPool1); // false
String pool
Выделим основные нюансы строкового пула в Java:
строковые литералы (в одном/разных классе(ах) и в одном/разных пакете(ах)) представляют собой ссылки на один и тот же объект;
строки, получающиеся сложением констант, вычисляются во время компиляции и дале
смотри пункт первый;
строки, создаваемые во время выполнения НЕ ссылаются на один и тот же объект;
метод intern(), в любом случае, возвращает объект из пула, вне зависимости от того
когда создается строка, на этапе компиляции или выполнения (если на этапе выполнения, то объект сначала принудительно разместится в pool, а затем на него будет получена ссылка).
В контексте данных пунктов речь шла об "одинаковых" строковых литералах.
Контрольный пример:
String hello = "Hello", hello2 = "Hello";
String hel = "Hel", lo = "lo";
System.out.println("Hello" == "Hello"); // true
System.out.println("Hello" == "hello"); // false
System.out.println(hello == hello2); // true
System.out.println(hello == ("Hel" + "lo")); // true
System.out.println(hello == (hel + lo)); // false
System.out.println(hello == (hel + lo).intern()); // true
Подробнее про interning можно прочитать здесь, а про сравнивание строк здесь.
Альтерантивы оператору ==
Для сравнения двух экземпляров объектных типов в Java также существуют такие метод
как: equals() и hashCode(). Методы hashCode() и equals() определены в классе java.lang.Object, который является родительским классом для объектов Java, поэтому все Java-объекты наследуют от этих методов реализацию по умолчанию.
Использование equals() и hashCode()
Метод equals(), как и следует из его названия, используется для простой проверк
равенства двух объектов. Реализация этого метода, по умолчанию, проверяет равенств
ссылок двух объектов, т.е. по умолчанию, поведение идентично работе оператора == за исключением того, что оператор == не позволяет сравнивать операнды неприводимых друг к другу типов.
Метод hashCode() обычно используется для получения уникального целого числа, чт
на самом деле не является правдой, т.к. результат работы данного метода - это цело
число примитивного типа int (называемое хеш-кодом), полученное посредством работы хэш-функции входным параметром которой является объект, вызывающий данный метод, но множество возможных хеш-кодов ограничено примитивным типом int (всего 2^32 вариантов значений), а множество объектов ограничено только нашей фантазией.
Из вышеописанного между методами hashCode() и equals() следует следующий контракт:
если хеш-коды разные, то и объекты гарантированно разные;
если хеш-коды равны, то входные объекты могут быть неравны (это обусловленно тем
что множество объектов мощнее множества хеш-кодод, т.к. множество возможных хеш-кодов ограничено размером примитивного типа int).
Подробнее про данные методы можно прочитать здесь.
Переопределение поведения по умолчанию
Так как в Java отсутствует возможность переопределять операторы (т.е. поведеие оператор
== не может быть изменено) как, например, в C++ или C#, то для сравнения двух объектов определенных классов согласно необходимому для вас алгоритму можно переопределить (@Override) методы equals() и hashCode() внутри нашего класса и использовать их.
Таким образом, создавая пользовательский класс следует переопределять методы hashCode(
и equals(), чтобы они корректно работали и учитывали необходимые для вас данные (поля
объекта. Кроме того, если не переопределить реализацию из Object, то, например, при использовании таких объектов в java.util.HashMap у вас наверняка возникнут проблемы, поскольку HashMap активно использует hashCode() и equals() в своей работе (подробнее см. здесь).
Особенности при работе с вещественными типами Float и Double
В Java NaN'ы несравнимы между собой, но есть два исключения в работе классов Float и Double, рассмотрим на примере класса Float:
Если float1 и float2 оба представляют Float.NaN, тогда метод equals возвращает true, в то время как Float.NaN == Float.NaN принимает значение false.
Если float1 содержит +0.0f в то время как float2 содержит -0.0f, метод equals возвращает false, в то время как 0.0f == -0.0f возвращает true.
Контрольный пример 1:
Float float1 = new Float(Float.NaN);
Float float2 = new Float(Float.NaN);
System.out.println(float1 == float2); // false
System.out.println(float1.equals(float2)); // true
System.out.println(Float.NaN == Float.NaN); // false
// --------------------------------------------------
Double double1 = new Double(Double.NaN);
Double double2 = new Double(Double.NaN);
System.out.println(double1 == double2); // false
System.out.println(double1.equals(double2)); // true
System.out.println(Double.NaN == Double.NaN); // false
Контрольный пример 2:
Float float1 = new Float(0.0F);
Float float2 = new Float(-0.0F);
System.out.println(float1 == float2); // false
System.out.println(float1.equals(float2)); // false
System.out.println(0.0F == -0.0F); // true
// --------------------------------------------------
Double double1 = new Double(0.0);
Double double2 = new Double(-0.0);
System.out.println(double1 == double2); // false
System.out.println(double1.equals(double2)); // false
System.out.println(0.0 == -0.0); // true
Подведем итоги
Если вам необходимо проверить не ссылаются ли две переменных на один и тот же объект
либо сравнить на равенство два примитива, то вам определенно стоит воспользоваться операторо
== (при этом стоит иметь в виду то, что вещественные числа следует сравнить лишь с определенно
точностью), но если же вам необходимо сравнить именно внутреннее содержимое объектов (либо основываясь на каком-нибудь нестандартном алгоритме), на которые ссылаются данные переменные, то вам стоит воспользоваться методом equals() соответствующего класса (причем если вы в своем классе переопределяете метод equals(), то при этом вам также стоит не забыть переопределить метод hashCode() согласно вышеописанному контракту между ними).
Комментариев нет:
Отправить комментарий