Однажды я потратил три часа на поиск бага. Три чёртовых часа! Программа вроде работала, но результаты расчётов постоянно плавали. И только случайно я заметил, что 0.3 + 0.4 у меня не равно 0.7. Вот тогда-то я и понял, что сравнение в Java – это не такая простая штука, как кажется.
Это продолжение первой статьи про сравнение, где мы разбирали теорию. А сейчас давай копнём глубже и посмотрим, где Java может тебя неприятно удивить. Поговорим про строки, вещественные числа и всякие неочевидные моменты, о которых на собеседованиях обожают спрашивать.![Сравнение объектов: практика - 1]()

Строки и оператор ==: история одного предательства
Знаешь, строки в Java – это как хороший друг, который иногда ведёт себя странно. Вроде всё нормально, но потом бац – и сюрприз. Смотри, что происходит:String str1 = "JavaRush";
String str2 = "JavaRush";
System.out.println(str1 == str2 ? "одна и та же" : "разные");
Запусти этот код. Результат будет "одна и та же". Погоди, разве мы не учили, что строки надо сравнивать через equals? Да, но тут есть нюанс. Компилятор Java – тот ещё хитрец. Он видит два одинаковых литерала и думает: "Зачем создавать два объекта? Создам один, а переменным дам ссылки на него". Экономия памяти, всё дела.
Но попробуй вот так:
String str1 = "JavaRush";
String str2 = "Java";
String str3 = "Rush";
System.out.println(str1 == (str2 + str3) ? "одна и та же" : "разные");
Теперь уже "разные"! Потому что конкатенация создаёт новый объект. И ссылки получаются разные.
А можно и вообще так:
String str1 = "JavaRush";
String str2 = new String("JavaRush");
System.out.println(str1 == str2 ? "одна и та же" : "разные");
Опять "разные". Ключевое слово new говорит Java: "Я хочу НОВЫЙ объект, даже если такая строка уже есть".
Вот так и попадаются джуны на собеседованиях. Используют == для строк, код вроде работает, а потом бац – и баг в продакшене. На курсе JavaRush мы этому посвящаем целый модуль, потому что штука реально важная.
String.intern() – магия пула строк
А теперь покажу тебе трюк, который знают далеко не все. У класса String есть метод intern(), который работает с пулом строк. Представь себе такой склад, где Java хранит все строковые литералы. Когда ты вызываешь intern(), Java идёт на этот склад и проверяет: есть там такая строка или нет? Если есть – возвращает ссылку на неё. Если нет – добавляет твою строку в пул и возвращает ссылку.String str1 = "JavaRush";
String str2 = new String("JavaRush");
System.out.println(str1 == str2); // false
System.out.println(str1.intern() == str2.intern()); // true
Теперь они указывают на один объект! Но честно говоря, я за семь лет работы использовал intern() от силы раза три. Это такая оптимизация, которая нужна только в специфических случаях.Вещественные числа: добро пожаловать в ад
Окей, держись. Сейчас я покажу тебе что-то, что взорвёт твой мозг. Готов? Чему равно 0.3f + 0.4f? Не спеши отвечать "0.7". Проверь:float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1 == f2: " + (f1 == f2));
Результат? false.
Я серьёзно. Запусти и убедись сам. Первый раз, когда я это увидел, я минут пятнадцать пялился в монитор и думал, что у меня с головой что-то не так.
Давай разберёмся, что творится. Выведем эти числа с большей точностью:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("f1 = " + (double)f1);
System.out.println("f2 = " + (double)f2);
System.out.println("f3 = " + (double)f3);
System.out.println("f4 = " + (double)f4);
Вывод:
f1 = 0.30000001192092896
f2 = 0.4000000059604645
f3 = 0.7000000476837158
f4 = 0.699999988079071
Видишь? Числа разные! И это не баг – это особенность представления дробных чисел в памяти компьютера.
Представь, что ты пытаешься нарезать пиццу на идеально ровные кусочки, но у тебя только обычный нож (не лазерный). Каждый раз будет небольшая погрешность. Вот и компьютер хранит дроби с погрешностью, потому что память конечна. У float на мантиссу отведено 24 бита, точность примерно 7 знаков после запятой. У double получше – 53 бита, точность около 16 знаков. Но погрешность есть всегда.Как сравнивать вещественные числа правильно
Раз прямое сравнение не работает, надо сравнивать с погрешностью. Вот так:float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("|f3 - f4| < 1e-6: " + (Math.abs(f3 - f4) < 1e-6));
Результат: true. Вот теперь работает!
Мы проверяем, что разница между числами меньше одной миллионной. Для float достаточно 1e-6, для double можно взять 1e-15.
Кстати, в библиотеке JUnit методы сравнения чисел так и работают – они принимают три параметра: два числа и точность сравнения. Библиотека знает, о чём говорит.
И ещё один момент. Когда пишешь числа в научной нотации, будь внимателен. Хочешь написать одну миллионную? Правильно так: 1e-6. А вот 10e-6 – это уже 10 умножить на 10 в минус шестой, то есть 0.00001. Я на этом спалился на одном проекте, искали баг полдня. Не повторяй моих ошибок!Плюс ноль и минус ноль: да, они разные
Думаешь, ноль – это просто ноль? Как бы не так. В представлении вещественных чисел есть знаковый бит. И если все остальные биты нули, а знаковый бит единица, получается -0.0. Звучит безумно, правда? Но посмотри:float f1 = 0.0f / 1.0f;
float f2 = 0.0f / -1.0f;
System.out.println("f1 = " + f1);
System.out.println("f2 = " + f2);
System.out.println("f1 == f2: " + (f1 == f2));
float f3 = 1.0f / f1;
float f4 = 1.0f / f2;
System.out.println("f3 = " + f3);
System.out.println("f4 = " + f4);
Вывод:
f1 = 0.0
f2 = -0.0
f1 == f2: true
f3 = Infinity
f4 = -Infinity
Видишь? Java считает их равными, но деление на них даёт разные результаты! Один даёт положительную бесконечность, другой – отрицательную.
Если тебе нужно различать +0.0 и -0.0, сравнивай битовые представления:
int i1 = Float.floatToIntBits(f1);
int i2 = Float.floatToIntBits(f2);
System.out.println("i1 (+0.0): " + Integer.toBinaryString(i1));
System.out.println("i2 (-0.0): " + Integer.toBinaryString(i2));
System.out.println("i1 == i2: " + (i1 == i2));
Результат:
i1 (+0.0): 0
i2 (-0.0): 10000000000000000000000000000000
i1 == i2: false
Теперь Java их различает.NaN: число, которое не равно само себе
NaN расшифровывается как Not-a-Number, "не число". Получается при каких-то некорректных операциях типа деления нуля на ноль. И у него есть одна дикая особенность – оно не равно само себе!float x = 0.0f / 0.0f;
System.out.println("x = " + x);
System.out.println("x == x: " + (x == x));
Результат:
x = NaN
x == x: false
Представляешь? Переменная не равна сама себе! Это единственное значение в Java с таким поведением.
Если в твоём объекте какое-то поле станет NaN, сравнение объектов через equals всегда будет давать false. Даже если все остальные поля идентичны!
Проверить, является ли число NaN, можно методом Float.isNaN():
float x = 0.0f / 0.0f;
System.out.println(Float.isNaN(x)); // trueAutoboxing и кеширование: неожиданные сюрпризы
С Java 5 появился autoboxing – автоматическое преобразование примитивов в объекты и обратно. Казалось бы, удобная штука. Но есть подвох. Классы-обёртки Integer, Long, Short, Byte, Character кешируют некоторые значения. Например, Integer кеширует числа от -128 до 127. Это значит, что при создании объектов с такими значениями Java возвращает ссылку на один и тот же объект из кеша.Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false
Видишь разницу? Для 127 сработал кеш, и a и b указывают на один объект. А для 128 создались два разных объекта.
Это оптимизация от создателей Java. Они решили, что маленькие числа используются часто, поэтому кешировать их выгодно. Но это может привести к странным багам, если ты не в курсе.
На курсе JavaRush мы разбираем эту тему в разделе про коллекции и обёртки – там много практических задач на понимание autoboxing.Enum: сравнивай спокойно
А вот с перечислениями (enum) всё просто и хорошо. Каждый элемент enum существует в единственном экземпляре, это гарантирует виртуальная машина. Поэтому их можно спокойно сравнивать через ==:enum Season {
WINTER, SPRING, SUMMER, AUTUMN
}
Season s1 = Season.SUMMER;
Season s2 = Season.SUMMER;
System.out.println(s1 == s2); // true
Никаких подвохов. Enum – один из немногих типов, где == работает предсказуемо и надёжно.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ