JavaRush /Java Blog /Random-TK /Сравнение объектов: практика
articles
Dereje

Сравнение объектов: практика

Toparda çap edildi
Это вторая из статей, посвященных сравнению an objectов. В первой из них речь шла о теоретическом базисе сравнения – How это делается, почему и где используется. В этой же статье речь пойдет непосредственно о сравнении чисел, an objectов, о частных случаях, тонкостях и неочевидных моментах. А если точнее, мы поговорим вот о чем:
Сравнение an objectов: практика - 1
  • Сравнение строк: '==' и equals
  • Метод String.intern
  • Сравнение вещественных примитивов
  • +0.0 и -0.0
  • Значение NaN
  • Java 5.0. Производящие методы и сравнение через '=='
  • Java 5.0. Autoboxing/Unboxing: '==', '>=' и '<=' для an objectных оболочек.
  • Java 5.0. сравнение элементов перечислений (тип enum)
Итак, приступим!

Сравнение строк: '==' и equals

Ах, эти строки... Один из наиболее часто используемых типов, вызывающих при этом немало проблем. В принципе, о них есть отдельная статья. А здесь я коснусь вопросов сравнения. Разумеется, строки можно сравнивать с помощью equals. Более того, их НУЖНО сравнивать через equals. Однако, есть тонкости, которые стоит знать. Прежде всего, одинаковые строки на самом деле являются единственным an objectом. В чем легко убедиться, выполнив следующий code:

String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
Результатом будет "the same". What означает, что ссылки на строки равны. Это сделано на уровне компилятора, очевидно, для экономии памяти. Компилятор создает ОДИН экземпляр строки, и присваивает str1 и str2 ссылку на этот экземпляр. Однако, это относится только к строкам, объявленным How литералы, в codeе. Если скомпоновать строку из кусков, link на нее будет другой. Подтверждение – данный пример:

String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
Результатом будет "not the same". Также можно создать новый an object с помощью копирующего конструктора:

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. Если такой строки в пуле не существует, то туда помещается имеющаяся, и возвращается link на нее. Таким образом, если даже ссылки на две равных строки разные (How в двух примерах выше), то вызовы у этих строк intern вернут ссылку на один и тот же an object:

String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
Результатом выполнения этого фрагмента codeа будет "the same". Я не могу сказать точно, зачем это сделано так. Метод intern – native, а в дебри С-codeа мне, честно сказать, не хочется. Скорее всего это сделано для оптимизации потребления памяти и производительности. В любом случае, стоит знать об этой особенности реализации. Переходим к следующей части.

Сравнение вещественных примитивов

Для начала я хочу задать вопрос. Очень простой. Чему равна следующая сумма – 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), либо, что предпочтительнее, проверял бы абсолютное meaning разности между ними:

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! Мы наступor на эти грабли в одном из проектов, довольно неожиданно. Ошибку искали очень долго, на константы смотрели раз 20. И ни у кого не возникло ни тени сомнения в их правильности, пока однажды, в большой степени случайно, константу 10e-3 не вывели на печать и не обнаружor у нее после запятой два знака instead of ожидавшихся трех. А потому – будьте бдительны! Движемся дальше.

+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 How два разных числа. А если у нас есть два an object, в одном из которых поле равно +0.0, а в другом -0.0 – эти an objectы точно так же можно расценивать How неравные. Возникает вопрос – а How понять, что числа неравны, если их прямое сравнение виртуальной machine дает 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 вроде How разобрались. -0.0, однако, является не единственным сюрпризом. Есть еще такое явление How...

Значение NaN

NaN расшифровывается How Not-a-Number. Это meaning появляется в результате некорректных математических операций, скажем, деления 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
Чем это может обернуться при сравнении an objectов? Если поле an object будет равно NaN, то сравнение даст false, т.е. an objectы гарантированно будут считаться неравными. Хотя по логике вещей мы можем хотеть How раз обратного. Добиться нужного результата можно, используя метод Float.isNaN(float). Он возвращает true, если аргумент – NaN. На сравнение битовых представлений я бы в этом случае не полагался, т.к. оно не стандартизовано. Пожалуй, о примитивах хватит. Перейдем теперь к тонкостям, появившимся в Java с версии 5.0. И первый момент, которого я бы хотел коснуться –

Java 5.0. Производящие методы и сравнение через '=='

В проектировании есть шаблон, называемый производящий метод. Иногда его использование гораздо более выгодно, нежели использование конструктора. Приведу пример. Думаю, все хорошо знаю an objectную оболочку Boolean. Этот класс неизменяемый, способен содержать всего два значения. Т.е., фактически, для любых нужд хватит всего-навсего двух экземпляров. И если их создать заранее, а потом просто возвращать, то это будет намного быстрее, чем использование конструктора. Такой метод у Boolean есть: valueOf(boolean). Появился он в версии 1.4. Подобные же производящие методы были введены с версии 5.0 и в классах Byte, Character, Short, Integer и Long. При загрузке этих классов создаются массивы их экземпляров, соответствующие определенным диапазонам значений примитивов. Диапазоны эти следующие:
Сравнение an objectов: практика - 2
Означает это, что при использовании метода valueOf(...) при попадании аргумента в указанный диапазон всегда будет возвращаться один и тот же an object. Возможно, это и дает Howое-то увеличение скорости. Но при этом появляются проблемы такого характера, что докопаться до сути бывает довольно сложно. Читайте об этом дальше. Теоретически производящий метод valueOf добавлен и в классы Float и Double. В их описании сказано, что если не нужен новый экземпляр, то лучше пользоваться этим методом, т.к. он может дать прибавку в скорости и т.д. и т.п. Однако в текущей (Java 5.0) реализации в этом методе создается новый экземпляр, т.е. прибавки в скорости его использование не даст гарантированно. Более того, мне сложно представить, How можно ускорить этот метод, ибо ввиду непрерывности значений кеш там не организуешь. Разве что для целых чисел. В смысле, без дробной части.

Java 5.0. Autoboxing/Unboxing: '==', '>=' и '<=' для an objectных оболочек.

Подозреваю, что производящие методы и кеш экземпляров были добавлены в оболочки для целочисленных примитивов ради оптимизации операций autoboxing/unboxing. Напомню, что это такое. Если в операции должен участвовать an object, а участвует примитив, то этот примитив автоматически оборачивается в an objectную оболочку. Это autoboxing. И наоборот – если в операции должен участвовать примитив, то можно подставить туда an objectную оболочку, и meaning будет автоматически из нее развернуто. Это unboxing. Естественно, за такое удобство надо платить. Операции автоматического преобразования несколько замедляют speed работы applications. Однако к текущей теме это не относится, потому оставим этот вопрос. Все хорошо до тех пор, пока мы имеем дело с операциями, однозначно относящимися к примитивам либо к оболочкам. А что будет с операцией '=='? Допустим, у нас есть два an object Integer, с одинаковым meaningм внутри. Как они будут сравниваться?

Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
Результат:
i1==i2: false

Кто бы сомневался... Сравниваются они How an objectы. А если так:Integer i1 = 1;
Integer i2 = 1;
System.out.println("i1==i2: "+(i1==i2));
Результат:

i1==i2: true
Вот это уже интереснее! При autoboxing-е возвращаются одинаковые an objectы! Вот тут и находится ловушка. Однажды обнаружив, что возвращаются одинаковые an objectы, мы начнем экспериментировать, чтобы проверить, всегда ли это так. И сколько мы проверим значений? Одно? Десять? Сто? Скорее всего ограничимся сотней в каждую сторону вокруг нуля. И везде получим equalsство. Казалось бы, все хорошо. Однако, посмотрите чуть назад, вот сюда. Догадались, в чем подвох?.. Да, экземпляры an objectных оболочек при autoboxing-е создаются с помощью производящих методов. What хорошо иллюстрируется следующим тестом:

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
Для попадающих в диапазон кеширования значений возвращаются одинаковые an objectы, для находящихся вне него – разные. А следовательно, если где-то в приложении будут сравниваться оболочки instead of примитивов – есть шанс получить самую страшную ошибку: плавающую. Потому How тестировать code, скорее всего, тоже будут на ограниченом диапазоне значений, в котором эта ошибка не проявится. А в реальной работе она то будет проявляться, то исчезать, в зависимости от результатов Howих-то вычислений. Проще сойти с ума, чем найти такую ошибку. А потому – я бы советовал избегать autoboxing-а где только можно. И это не всё. Вспомним математику, не далее чем 5-го класса. Пусть выполняются неequalsства A>=B и А<=B. What можно сказать об отношении 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 появился такой тип How enum – перечисление. Его экземпляры по умолчанию содержат Name и порядковый номер в объявлении экземпляра в классе. Соответственно, при изменении порядка объявления номера меняются. Однако, How я уже говорил в статье 'Сериализация How она есть', это не вызывает проблем. Все элементы перечисления существуют в единственном экземпляре, это контролируется на уровне виртуальной машины. Поэтому их можно сравнивать напрямую, по linkм. * * * Пожалуй, это всё на сегодня о практической стороне реализации сравнения an objectов. Возможно, я что-то упустил. Как обычно, жду комментариев! А пока позвольте откланяться. Всем спасибо за внимание! Ссылка на первоисточник: Сравнение an objectов: практика
Teswirler
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION