Это вторая из статей, посвященных сравнению объектов. В первой из них речь шла о теоретическом базисе сравнения – как это делается, почему и где используется. В этой же статье речь пойдет непосредственно о сравнении чисел, объектов, о частных случаях, тонкостях и неочевидных моментах. А если точнее, мы поговорим вот о чем:
Сравнение строк: '
Ах, эти строки... Один из наиболее часто используемых типов, вызывающих при этом немало проблем. В принципе, о них есть отдельная статья. А здесь я коснусь вопросов сравнения.
Разумеется, строки можно сравнивать с помощью Java 5.0. Производящие методы и сравнение через '
В проектировании есть шаблон, называемый производящий метод. Иногда его использование гораздо более выгодно, нежели использование конструктора. Приведу пример. Думаю, все хорошо знаю объектную оболочку Java 5.0. Autoboxing/Unboxing: '
Подозреваю, что производящие методы и кеш экземпляров были добавлены в оболочки для целочисленных примитивов ради оптимизации операций
- Сравнение строк: '
==
' иequals
- Метод
String.intern
- Сравнение вещественных примитивов
+0.0
и-0.0
- Значение
NaN
- Java 5.0. Производящие методы и сравнение через '
==
' - Java 5.0. Autoboxing/Unboxing: '
==
', '>=
' и '<=
' для объектных оболочек. - Java 5.0. сравнение элементов перечислений (тип
enum
)
Сравнение строк: '==
' и equals
Ах, эти строки... Один из наиболее часто используемых типов, вызывающих при этом немало проблем. В принципе, о них есть отдельная статья. А здесь я коснусь вопросов сравнения.
Разумеется, строки можно сравнивать с помощью equals
. Более того, их НУЖНО сравнивать через equals
. Однако, есть тонкости, которые стоит знать.
Прежде всего, одинаковые строки на самом деле являются единственным объектом. В чем легко убедиться, выполнив следующий код:
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
Результатом будет "the same".
Что означает, что ссылки на строки равны. Это сделано на уровне компилятора, очевидно, для экономии памяти. Компилятор создает ОДИН экземпляр строки, и присваивает str1
и str2
ссылку на этот экземпляр.
Однако, это относится только к строкам, объявленным как литералы, в коде. Если скомпоновать строку из кусков, ссылка на нее будет другой. Подтверждение – данный пример:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
Результатом будет "not the same".
Также можно создать новый объект с помощью копирующего конструктора:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
Результатом также будет "not the same".
Таким образом, иногда строки можно сравнивать и через сравнение ссылок. Но на это лучше не полагаться.
Я хотел бы затронуть один весьма любопытный метод, который позволяет получить так называемое каноническое представление строки – String.intern
. Поговорим о нем поподробнее.
Метод String.intern
Начнем с того, что классString
поддерживает пул строк. В этот пул добавляются все строковые литералы, определенные в классах, и не только они. Так вот, метод intern
позволяет получить из этого пула строку, которая равна имеющейся (той, у которой вызывается метод intern
) с точки зрения equals
. Если такой строки в пуле не существует, то туда помещается имеющаяся, и возвращается ссылка на нее. Таким образом, если даже ссылки на две равных строки разные (как в двух примерах выше), то вызовы у этих строк intern
вернут ссылку на один и тот же объект:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
Результатом выполнения этого фрагмента кода будет "the same".
Я не могу сказать точно, зачем это сделано так. Метод intern
– native, а в дебри С-кода мне, честно сказать, не хочется. Скорее всего это сделано для оптимизации потребления памяти и производительности. В любом случае, стоит знать об этой особенности реализации.
Переходим к следующей части.
Сравнение вещественных примитивов
Для начала я хочу задать вопрос. Очень простой. Чему равна следующая сумма – 0.3f + 0.4f? Чему? 0.7f? Проверим:float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(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);
Обратите внимание на приведение к double
. Это сделано для того, чтобы вывести побольше знаков после запятой. Результат:
f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
Собственно говоря, результат прогнозируемый. Представление дробной части осуществляется с помощью конечного ряда 2-n, а потому о точном представлении произвольно взятого числа говорить не приходится. Как видно из примера, точность представления float
– 7 знаков после запятой.
Строго говоря, в представлении float
на мантиссу отведено 24 бита. Таким образом минимальное по модулю число, которое можно представить с помощью float
(без учета степени, ибо мы говорим о точности) – это 2-24≈6*10-8. Именно с таким шагом реально идут значения в представлении float
. А поскольку есть квантование – есть и погрешность.
Отсюда вывод: числа в представлении float
можно сравнивать только с определенной точностью. Я бы рекомендовал округлять их до 6-го знака после запятой (10-6), либо, что предпочтительнее, проверял бы абсолютное значение разности между ними:
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 ));
В этом случае результат вселяет надежду:
|f3-f4|<1e-6: true
Разумеется, точно та же картина и с типом double
. С единственной разницей, что там на мантиссу отведено 53 бита, следовательно, точность представления – 2-53≈10-16. Да, величина квантования куда меньше, но она есть. И может сыграть злую шутку.
Кстати, в тестовой библиотеке JUnit в методах сравнения вещественных чисел точность указывается в явном виде. Т.е. метод сравнения содержит три параметра – число, чему оно должно быть равно и точность сравнения.
Еще кстати, хочу упомянуть о тонкости, связаной с записью чисел в научном формате, с указанием степени. Вопрос. Как записать 10-6? Практика показывает, что более 80% отвечают – 10e-6. Между тем, правильный ответ – 1e-6! А 10e-6 – это 10-5! Мы наступили на эти грабли в одном из проектов, довольно неожиданно. Ошибку искали очень долго, на константы смотрели раз 20. И ни у кого не возникло ни тени сомнения в их правильности, пока однажды, в большой степени случайно, константу 10e-3 не вывели на печать и не обнаружили у нее после запятой два знака вместо ожидавшихся трех. А потому – будьте бдительны!
Движемся дальше.
+0.0 и -0.0
В представлении вещественных чисел старший бит является знаковым. А что будет, если все остальные биты равны 0? В отличие от целых, где в такой ситуации получается отрицательное число, находящееся на нижней границе диапазона представления, вещественное число только со старшим битом, выставленным в 1, тоже обозначает 0, только со знаком минус. Таким образом, у нас есть два нуля – +0.0 и -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
Таким образом, в некоторых случаях есть смысл расценивать +0.0 и -0.0 как два разных числа. А если у нас есть два объекта, в одном из которых поле равно +0.0, а в другом -0.0 – эти объекты точно так же можно расценивать как неравные. Возникает вопрос – а как понять, что числа неравны, если их прямое сравнение виртуальной машиной дает true
?
Ответ таков. Несмотря на то, что виртуальная машина считает эти числа равными, представления у них все-таки отличаются. Поэтому – единственное, что можно сделать, это сравнить представления. А для того, чтобы его получить, существуют методы int Float.floatToIntBits(float)
и long Double.doubleToLongBits(double)
, которые возвращают битовое представление в виде int
и long
соответственно (продолжение предыдущего примера):
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
Таким образом, если у вас +0.0 и -0.0 – разные числа, то сравнивать вещественные переменные следует через их битовое представление.
С +0.0 и -0.0 вроде как разобрались. -0.0, однако, является не единственным сюрпризом. Есть еще такое явление как...
Значение NaN
NaN
расшифровывается как Not-a-Number
. Это значение появляется в результате некорректных математических операций, скажем, деления 0.0 на 0.0, бесконечности на бесконечность и т.п.
Особенностью этого значения является то, что оно не равно самому себе. Т.е.:
float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
... даст в результате...
x=NaN
x==x: false
Чем это может обернуться при сравнении объектов? Если поле объекта будет равно NaN
, то сравнение даст false
, т.е. объекты гарантированно будут считаться неравными. Хотя по логике вещей мы можем хотеть как раз обратного.
Добиться нужного результата можно, используя метод Float.isNaN(float)
. Он возвращает true
, если аргумент – NaN
. На сравнение битовых представлений я бы в этом случае не полагался, т.к. оно не стандартизовано.
Пожалуй, о примитивах хватит. Перейдем теперь к тонкостям, появившимся в Java с версии 5.0. И первый момент, которого я бы хотел коснуться –
Java 5.0. Производящие методы и сравнение через '==
'
В проектировании есть шаблон, называемый производящий метод. Иногда его использование гораздо более выгодно, нежели использование конструктора. Приведу пример. Думаю, все хорошо знаю объектную оболочку Boolean
. Этот класс неизменяемый, способен содержать всего два значения. Т.е., фактически, для любых нужд хватит всего-навсего двух экземпляров. И если их создать заранее, а потом просто возвращать, то это будет намного быстрее, чем использование конструктора. Такой метод у Boolean
есть: valueOf(boolean)
. Появился он в версии 1.4.
Подобные же производящие методы были введены с версии 5.0 и в классах Byte
, Character
, Short
, Integer
и Long
. При загрузке этих классов создаются массивы их экземпляров, соответствующие определенным диапазонам значений примитивов. Диапазоны эти следующие:
Означает это, что при использовании метода
valueOf(...)
при попадании аргумента в указанный диапазон всегда будет возвращаться один и тот же объект. Возможно, это и дает какое-то увеличение скорости. Но при этом появляются проблемы такого характера, что докопаться до сути бывает довольно сложно. Читайте об этом дальше.
Теоретически производящий метод valueOf
добавлен и в классы Float
и Double
. В их описании сказано, что если не нужен новый экземпляр, то лучше пользоваться этим методом, т.к. он может дать прибавку в скорости и т.д. и т.п. Однако в текущей (Java 5.0) реализации в этом методе создается новый экземпляр, т.е. прибавки в скорости его использование не даст гарантированно. Более того, мне сложно представить, как можно ускорить этот метод, ибо ввиду непрерывности значений кеш там не организуешь. Разве что для целых чисел. В смысле, без дробной части.
Java 5.0. Autoboxing/Unboxing: '==
', '>=
' и '<=
' для объектных оболочек.
Подозреваю, что производящие методы и кеш экземпляров были добавлены в оболочки для целочисленных примитивов ради оптимизации операций autoboxing/unboxing
. Напомню, что это такое. Если в операции должен участвовать объект, а участвует примитив, то этот примитив автоматически оборачивается в объектную оболочку. Это autoboxing
. И наоборот – если в операции должен участвовать примитив, то можно подставить туда объектную оболочку, и значение будет автоматически из нее развернуто. Это unboxing
.
Естественно, за такое удобство надо платить. Операции автоматического преобразования несколько замедляют скорость работы приложения. Однако к текущей теме это не относится, потому оставим этот вопрос.
Все хорошо до тех пор, пока мы имеем дело с операциями, однозначно относящимися к примитивам либо к оболочкам. А что будет с операцией '==
'? Допустим, у нас есть два объекта Integer
, с одинаковым значением внутри. Как они будут сравниваться?
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
Результат:
Результат:i1==i2: false Кто бы сомневался... Сравниваются они как объекты. А если так:
Integer i1 = 1; Integer i2 = 1; System.out.println("i1==i2: "+(i1==i2));
i1==i2: true
Вот это уже интереснее! При autoboxing
-е возвращаются одинаковые объекты!
Вот тут и находится ловушка. Однажды обнаружив, что возвращаются одинаковые объекты, мы начнем экспериментировать, чтобы проверить, всегда ли это так. И сколько мы проверим значений? Одно? Десять? Сто? Скорее всего ограничимся сотней в каждую сторону вокруг нуля. И везде получим равенство. Казалось бы, все хорошо. Однако, посмотрите чуть назад, вот сюда. Догадались, в чем подвох?..
Да, экземпляры объектных оболочек при autoboxing-е создаются с помощью производящих методов. Что хорошо иллюстрируется следующим тестом:
public class AutoboxingTest {
private static final int numbers[] = new int[]{-129,-128,127,128};
public static void main(String[] args) {
for (int number : numbers) {
Integer i1 = number;
Integer i2 = number;
System.out.println("number=" + number + ": " + (i1 == i2));
}
}
}
Результат будет таков:
number=-129: false
number=-128: true
number=127: true
number=128: false
Для попадающих в диапазон кеширования значений возвращаются одинаковые объекты, для находящихся вне него – разные. А следовательно, если где-то в приложении будут сравниваться оболочки вместо примитивов – есть шанс получить самую страшную ошибку: плавающую. Потому как тестировать код, скорее всего, тоже будут на ограниченом диапазоне значений, в котором эта ошибка не проявится. А в реальной работе она то будет проявляться, то исчезать, в зависимости от результатов каких-то вычислений. Проще сойти с ума, чем найти такую ошибку. А потому – я бы советовал избегать autoboxing-а где только можно.
И это не всё. Вспомним математику, не далее чем 5-го класса. Пусть выполняются неравенства A>=B
и А<=B
. Что можно сказать об отношении A
и B
? Только одно – они равны. Согласны? Думаю, да. Запускаем тест:
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1>=i2: "+(i1>=i2));
System.out.println("i1<=i2: "+(i1<=i2));
System.out.println("i1==i2: "+(i1==i2));
Результат:
i1>=i2: true
i1<=i2: true
i1==i2: false
И вот это для меня – самая большая странность. Я вообще не понимаю, зачем было вводить в язык эту возможность, если она вносит такие противоречия. В общем, повторю еще раз – если есть возможность обойтись без autoboxing/unboxing
, то стоит эту возможность использовать на полную катушку.
Последняя тема, которой я хотел бы коснуться, это...
Java 5.0. сравнение элементов перечислений (тип enum)
Как известно, с версии 5.0 в Java появился такой тип как enum – перечисление. Его экземпляры по умолчанию содержат имя и порядковый номер в объявлении экземпляра в классе. Соответственно, при изменении порядка объявления номера меняются. Однако, как я уже говорил в статье 'Сериализация как она есть', это не вызывает проблем. Все элементы перечисления существуют в единственном экземпляре, это контролируется на уровне виртуальной машины. Поэтому их можно сравнивать напрямую, по ссылкам.
* * *
Пожалуй, это всё на сегодня о практической стороне реализации сравнения объектов. Возможно, я что-то упустил. Как обычно, жду комментариев! А пока позвольте откланяться. Всем спасибо за внимание!
Ссылка на первоисточник: Сравнение объектов: практика
В общем вещественные числа float и double в Java из — за:
Могут давать погрешность даже при простых на первый взгляд операциях… собственно ещё и поэтому их крайне не рекомендуется использовать при работе с финансовыми задачами.
и при этом:
true = (d == c);
В примерах своего кода я показал, что даже четное/нечетное не имеет значения, в моем случае везде было true. И только пример из статьи дает false. Как я понял, нет никакого правила, просто определенные числа «не работают». Для финансов я бы просто создал бы новый объект Grivna с полями int money и int coins, и при достижении coins значения 100, обнулял и добавлял 1 к money. Ах да, еще бы создал поле ExchangeRates, и изменил бы его 1:1 к доллару :))
Обычно, насколько я знаю, в банковских операциях используют нечто вроде бигдецемала… но могу и ошибаться. Построение отдельных… денежных величин тянет за собой целую цепочку построения различных математических финансовых операций, которые отнють не ограничиваются простыми операциями сложения умножения и деления…
Сравнение вещественных примитивов
Выход:
//=======================
вывод>
//=======================
вывод:
*Задумчиво чешется пятка*