Страницы

Поиск по вопросам

воскресенье, 30 сентября 2018 г.

Как работает оператор ==

Как работает оператор проверки на равенство ("=="), имеются ли ему альтернативы и в каких ситуациях следует его применять?


Ответ

Оператор "=="
В Java оператор "==" возвращает значение типа boolean - результат сравнения ссылок на объекты (кроме примитивов), т.е. данный оператор не сравнивает на равенство внутреннее содержимое объектов, а просто проверяет указывают ли ссылки на один и тот-же объект. Таким образом, при сравнении объектов в Java оператор "==" вернет true лишь в том случае, когда ссылки указывают на один и тот-же объект.

Сравнение примитивов
Для примитивных типов (byte, short, int, long, char, float и double) концепция равенства/неравенства достаточно тривиальна: сначала операнды распространяются (т.е. происходит арифметическое распространение (promotion) операндов) до наибольшего если это необходимо, а затем происходит непосредственное побитовое сравнение (если все биты равны, то возвращаемое значение будет true, иначе - false).
Например, значение 48.0f (типа float, соответственно) равно значению '0' (код которого равен 48 согласно таблице символов Unicode) типа char, значение которого неявно распространяется до типа float
Контрольный пример:
char char1 = '0'; int int1 = 48; float float1 = 48.0F; System.out.println(char1 == int1); // true System.out.println(char1 == float1); // true System.out.println(int1 == float1); // true
// ---------------------------------------------
char char2 = 'A'; int int2 = 65; float float2 = 65.0F; System.out.println(char2 == int2); // true System.out.println(char2 == float2); // true System.out.println(int2 == float2); // true

Сравнение вещественных примитивов
Для вещественных примитивов (float и double) существуют некоторые особенности, которые необходимо учитывать. Представление дробной части осуществляется с помощью конечного числового ряда 2^(-n) (где значение числа n зависит от размера мантисы, который в свою очередь зависит от конкретного типа: float - 24 бита – это 2^(-24) ≈ 6*10^(-8), т.е. 6E-8F, либо double - 53 бита – это 2^(-53) ≈ 10^(-16), т.е. 1E-16, что фактически является шагом с которым реально идут значения в представлении данных типов), а потому о точном представлении произвольно взятого числа говорить не приходится.
Само собой разумеется, что две вещественные переменные, проинициализированные одним и тем же вещественным литералом, будут равны (ведь они проинициализировались одной и той же последовательностью бит):
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 используются так называемые пулы: Integer pool, String pool и некотоыре другие. Когда мы создаем объект не используя операцию new, объект помещается в пул, и в последствии, если мы захотим создать такой же объект (опять не используя new), то новый объект создан не будет, а мы просто получим ссылку на наш объект из пула.
Стоит заметить, что использование Integer Pool'а будет осуществляться при любой автоупаковке (autoboxing), если значение соответсвует указанному диапазону, в отличие от строк (String Pool), для которых интернирование (intern) работает для литералов.

Integer pool
Особенность Integer pool'а в том, что он хранит только числа, которые помещаются в тип данных byte: от -128 до 127 (который начиная с Java 7 (раньше это было захардкожено внутри java.lang.Integer) можно расширить с помощью опции JVM: -Djava.lang.Integer.IntegerCache.high=size или -XX:AutoBoxCacheMax=size). Для остальных чисел из Integer'а пул не работает.
Контрольный пример:
Integer valueFromPool1 = 127; Integer valueFromPool2 = 127; Integer valueNotFromPool1 = 128; Integer valueNotFromPool2 = 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 в любом случае возвращает объект из пула, вне зависимости от того, когда создается строка, на этапе компиляции или выполнения.
В контексте данных пунктов речь шла об "одинаковых" строковых литералах.
Контрольный пример:
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() определены в классе Object, который является родительским классом для объектов Java. Поэтому все Java объекты наследуют от этих методов реализацию по умолчанию.
Использование equals() и hashCode()
Метод equals(), как и следует из его названия, используется для простой проверки равенства двух объектов. Реализация этого метода по умолчанию просто проверяет по ссылкам два объекта на предмет их эквивалентности, т.е. просто сравнивает ссылки.
Метод hashCode() обычно используется для получения уникального целого числа, что на самом деле не является правдой, т.к. результат работы данного метода - это целое число примитивного типа int (называемое хеш-кодом), полученное посредством работы хэш-функции входным параметром которой является объект, вызывающий данный метод, но множество возможных хеш-кодов ограничено примитивным типом int, а множество объектов ограничено только нашей фантазией. В итоге, имеем следующее:
если хеш-коды разные, то и объекты гарантированно разные если хеш-коды равны, то входные объекты не всегда равны (это обусловленно тем, что множество объектов мощнее множества хеш-кодод, т.к. множество возможных хеш-кодов ограничено примитивным типом 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() (причем если это пользовательский класс, то вам стоит переопределить данный метод и при этом не забыть про метод hashCode()).

Комментариев нет:

Отправить комментарий