JavaRush /Java Blog /Random-TL /Paghahambing ng mga bagay: pagsasanay
articles
Antas

Paghahambing ng mga bagay: pagsasanay

Nai-publish sa grupo
Ito ang pangalawa sa mga artikulong nakatuon sa paghahambing ng mga bagay. Ang una sa kanila ay tinalakay ang teoretikal na batayan ng paghahambing - kung paano ito ginagawa, bakit at saan ito ginagamit. Sa artikulong ito ay direktang pag-uusapan natin ang tungkol sa paghahambing ng mga numero, mga bagay, mga espesyal na kaso, mga subtlety at hindi halatang mga punto. Mas tiyak, narito ang pag-uusapan natin:
Сравнение an objectов: практика - 1
  • Paghahambing ng string: ' ==' atequals
  • PamamaraanString.intern
  • Paghahambing ng totoong primitives
  • +0.0At-0.0
  • Ibig sabihinNaN
  • Java 5.0. Pagbuo ng mga pamamaraan at paghahambing sa pamamagitan ng ' =='
  • Java 5.0. Autoboxing/Unboxing: ' ==', ' >=' at ' <=' para sa mga object wrapper.
  • Java 5.0. paghahambing ng mga elemento ng enum (uri enum)
Kaya simulan na natin!

Paghahambing ng string: ' ==' atequals

Ah, ang mga linyang ito... Isa sa mga pinakakaraniwang ginagamit na uri, na nagdudulot ng maraming problema. Sa prinsipyo, mayroong isang hiwalay na artikulo tungkol sa kanila . At dito ko hipuin ang mga isyu sa paghahambing. Siyempre, maihahambing ang mga string gamit ang equals. Bukod dito, DAPAT silang ikumpara sa pamamagitan ng equals. Gayunpaman, may mga subtleties na nagkakahalaga ng pag-alam. Una sa lahat, ang magkaparehong mga string ay talagang isang solong bagay. Madali itong ma-verify sa pamamagitan ng pagpapatakbo ng sumusunod na code:
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
Magiging "pareho" ang resulta . Na nangangahulugan na ang mga sanggunian ng string ay pantay. Ginagawa ito sa antas ng compiler, malinaw naman upang makatipid ng memorya. Lumilikha ang compiler ng ISANG instance ng string, at nagtatalaga str1ng str2reference sa instance na ito. Gayunpaman, nalalapat lamang ito sa mga string na idineklara bilang mga literal sa code. Kung bubuo ka ng isang string mula sa mga piraso, mag-iiba ang link dito. Pagkumpirma - halimbawang ito:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
Ang magiging resulta ay "hindi pareho" . Maaari ka ring lumikha ng bagong object gamit ang copy constructor:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
Ang magiging resulta ay "hindi pareho" . Kaya, kung minsan ang mga string ay maaaring ihambing sa pamamagitan ng reference na paghahambing. Ngunit ito ay mas mahusay na hindi umasa dito. Gusto kong hawakan ang isang napaka-kagiliw-giliw na paraan na nagbibigay-daan sa iyo upang makuha ang tinatawag na canonical representasyon ng isang string - String.intern. Pag-usapan natin ito nang mas detalyado.

String.intern na pamamaraan

Magsimula tayo sa katotohanan na Stringsinusuportahan ng klase ang isang string pool. Ang lahat ng string literal na tinukoy sa mga klase, at hindi lamang ang mga ito, ay idinagdag sa pool na ito. Kaya, ang pamamaraan internay nagbibigay-daan sa iyo upang makakuha ng isang string mula sa pool na ito na katumbas ng umiiral na isa (ang isa kung saan ang pamamaraan ay tinatawag na intern) mula sa punto ng view ng equals. Kung ang naturang hilera ay hindi umiiral sa pool, pagkatapos ay ang umiiral na isa ay inilalagay doon at isang link dito ay ibinalik. Kaya, kahit na ang mga sanggunian sa dalawang magkaparehong mga string ay magkaiba (tulad ng sa dalawang halimbawa sa itaas), ang mga tawag sa mga string na ito internay magbabalik ng isang sanggunian sa parehong bagay:
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ов: практика
Mga komento
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION