Pull to refresh

Comments 36

Тот случай, когда хочется плюсануть карму автора раз в пятнадцатый, но можно только один :)
Мне нравится как это реализовано в rust.
Как раз для таких случаев у них есть 2 trait'a (почти что интерфейс в java) для сравнений:
PartialOrd и Ord

Причем второй реализован только для линейно упорядоченных множеств

Так же есть отдельные trait'ы для равенств и неравенств:
doc.rust-lang.org/std/cmp/index.html
Есть еще одна особенность таких «неправильных» компараторов.
Код, который нормально работал на Java 6, может внезапно начать падать на Java 7 c
java.lang.IllegalArgumentException: Comparison method violates its general contract!

в связи с тем, что в Java 7 поменялся алгоритм, лежащий в основе Arrays.sort / Collections.sort.
Думаю, это хорошо, что оно падает. Обычно это лучше, чем заведомо неверный результат.
Далеко не всегда падает. Весьма редко, я бы сказал. Скорость сортировки важнее, чем проверка свойств компаратора.
Полезная статья особенно для тех кто пришел в java с языков без таких особенностей с ±0
С python например. Там и NaN то спрятанный немного.
М… в Питоне -0.0 вроде тоже есть:
$ python
Python 2.6.5 (r265:79063, Apr 16 2010, 13:09:56)
>>> 0.0
0.0
>>> -0.0
-0.0
>>> 1*(-0.0)
-0.0
>>> 1*(+0.0)
0.0
>>> max(-0.0,0.0)
-0.0


Последнее, кстати, забавно. В Java Math.max вернёт 0.0.
Да согласен, промазал )
«max» вернет согласно спецификации первый встреченный максимум из равных (так как -0.0 равно 0.0)
Наверно, всё-таки как-то по-другому в спецификации должно быть. В max(float('nan'),0) и max(0,float('nan')) величины в скобках не равны очевидно, но возвращается первая. И вот ещё прикол аналогичный тому, что у меня в статье:

>>> sorted([10.0,5.0,float('nan'),3.0,4.0,2.0,8.0,7.0,1.0,9.0,6.0])
[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 10.0, nan, 7.0, 8.0, 9.0]


Так или иначе, nan — коварная штука. Если ваше приложение получает числа из недоверенных источников в виде строк и конвертит их через float(), вы можете обжечься.
Зато тут более стабильно :-) У меня тот же порядок в разных окружениях выходит. Насчет NaN согласен — вредная штуковина, хорошо хоть не так часто прилететь он может.

min и max используют вроде больше и меньше и соответсвенно для max, например, если следующий элемент не больше уже выбранного, то берется выбранный.

Подводя итог — данная статья я думаю может быть полезна отнюдь не только для java, учитывая что в питоне еще и перегрузка операторов есть.
Да и nan вроде не совсем спрятан, хотя функции чаще кидают исключения, чем возвращают nan.

>>> float('nan')
nan
>>> float('nan') > 0
False
>>> float('nan') < 0
False
>>> float('nan') == float('nan')
False
>>> from math import isnan
>>> isnan(float('nan'))
True
Это я назвал спрятан немного. То есть его можно получить только явным заданием через строку.
А вот это классно:

>>> max(float('nan'),0)
nan
>>> max(0,float('nan'))
0
>>> min(float('nan'),0)
nan
>>> min(0,float('nan'))
0

Питон тоже весёлый язык по части сравнений :-)
Все согласно спецификации :-) Так 0 > float(«nan») == False то для max он выбирает первое. Такая же логика для min.
Автор, операция сравнения, сама по себе, простая.
Просто для NaN не выполняются правила математики.
Не используя компилятор, отсортируйте значения: [Infinity, -Infinity, -1, 0, 1, NaN]
В каком порядке вы ожидаете их увидеть? Будут ли для этого массива проходить проверки на отсортированность?
Начнём с того, что то, что в Java называется float и double, это вообще не числа с точки зрения математики. Для них не выполняются даже самые основные законы вроде ассоциативности сложения и умножения. Это просто множество объектов, удобных для выполнения определённых вычислений инженерами. А так это просто множество и если хочется его сортировать, то на нём надо ввести линейный порядок. Люди часто вводят то, что линейным порядком не является, и в результате могут получить проблемы. Об этом и статья. Обычно меня действительно не очень волнует, окажется NaN перед всеми числами или после всех, но меня волнует, чтобы единица была перед двойкой а в TreeMap не было повторных ключей. Если же я не учту NaN в компараторе, то у меня и единица с двойкой в отсортированном массиве могут встать наоборот. Пусть кому-то не нравится стандартный порядок, который предоставляют Double.compare/Float.compare (они размещают NaN в самом конце, после +Infinity). Пусть кто-то хочет видеть NaN между -0.0 и +0.0. На здоровье, тогда надо написать сравнение по-другому вручную, но всё равно потребуется NaN учесть.
UFO just landed and posted this here
Естественно, это реализация IEEE 754. Это как-то противоречит моим словам? :-)

Вариант с вычитанием тоже встречается, хотя не уверен, что он «обычнее» того, что представлен в статье. Такой вариант подвержен абсолютно той же проблеме, можете это легко проверить. В Java реализована семантика quiet-NaN: практически любая операция, включающая NaN, выдаст NaN в качестве результата. И потом вы должны будете вернуть целое число (потому что сравнивающая функция возвращает целое число), поэтому вам придётся написать что-то типа:
double diff = d - o.d;
return diff < 0 ? -1 : diff > 0 ? 1 : 0;

Тут будет абсолютно та же проблема, только вид сбоку.

(даже не думайте писать return (int)(d-o.d)).
UFO just landed and posted this here
Можно было подумать

Нет, не думайте так :-)

Почему нельзя? Именно так бы я и сделал :) 

Тогда у вас не только 0.1 будет равно 0.2 или 0.3, но и нарушится транзитивность во многих случаях (например, 0.1 == 0.4, 0.4 == 0.8, 0.8 == 1.2, но 0.1 < 1.2). В результате при сортировке вы получите полную кашу даже без NaN. А главное, проблем с NaN это никак не решает. В Java (int)Double.NaN == 0.
А вообще сравнение вычитанием — это очень плохо: тут переполнение может возникнуть на ровном месте. Если у вас целые числа a = Integer.MIN_VALUE, b = Integer.MAX_VALUE, то a−b будет равно +1, хотя очевидно, что a<b.
плохо бесспорно, но для полноты картины надо отметить что в случ double переполнения не происходит, но результат все равно нелогичный:
> Double.compare(-Double.MAX_VALUE, Double.MIN_VALUE-Double.MAX_VALUE)
=>
0


т.е. Double.MIN_VALUE-Double.MAX_VALUE = -Double.MAX_VALUE
Кажется, вы немного запутались. В Java Double.MIN_VALUE — это не наименьшее double-число, а наименьшее положительное double-число (что-то вроде 4.9×10^-324). Double.MIN_VALUE-Double.MAX_VALUE — это действительно -Double.MAX_VALUE, потому что, естественно, точности не хватает, чтобы выполнить ваше вычитание правильно.

Видимо, вы хотели проверить -Double.MAX_VALUE-Double.MAX_VALUE. Тут ожидаемо получится -Infinity, переполнения в обычном смысле действительно не будет. Но для double/float при вычитании не решаются проблемы с NaN, а для целочисленного сравнения возникает переполнение, поэтому я не вижу места, где бы было разумно использовать вычитание для сравнения. Разве что если вы заранее знаете, что числа в определённом диапазоне. Но зачем такие предположения, когда есть готовые методы compare, которые всегда работают :-)
UFO just landed and posted this here
Т.е. статья о том, что даблы нельзя сравнивать вычитанием? :)
Э? Про вычитание в статье ни слова не было.
Отличные грабли автор описал! Спасибо!
Статья претендует на обучение читателя правильному в области Java — это хорошо. Думаю, что в такой статье всё должно быть правильно во всех аспектах.

У меня возникло подозрение, что не соблюдается выполнение соглашения Java для имен в примере со строками

DoubleHolder nan = new DoubleHolder(Double.NaN);
DoubleHolder zero = new DoubleHolder(0.0);

Ведь «переменные» nan и zero в левых частях обоих выражений это на самом деле константы, а, следовательно, их имена должны быть написаны буквами в верхнем регистре.
См. www.oracle.com/technetwork/java/javase/documentation/codeconventions-135099.html#367

Конечно, моя претензия к примеру будет некорректной, если найдется хотя бы один случай, когда переменные в коде всё-таки изменят свои значения на отличные от инициализированных. Моей фантазии сейчас не хватило, что бы изобрести такой корректный случай.

Кроме того, если эти «переменные» будут признаны константами, то правильно их объявление делать на уровне класса, добавляя модификаторы final static.
У вас настолько оригинальная точка зрения, что я даже не знаю, как вам возразить :-)

Константа — это статическое поле класса, она существует на протяжении всего времени существования класса. Если мне эти объекты нужны только в одном методе, зачем их держать в памяти постоянно, хоть они и неизменяемые? Переменной в Java называют всё, что объявлено в методе вне зависимости от того изменяемое оно или нет. В Java можно и переменную объявить final, но это, на мой взгляд, загрязняет код. Тут дело вкуса, но следует вспомнить, что в JDK 8 ввели понятие effectively-final как бы согласившись с тем, что писать final для каждой неизменяемой переменной, переданной в лямбду, это некрасиво. Но я не спорю с теми, кто так делает: в конце концов, в IDE можно одной кнопкой подписать final ко всем неизменяемым переменным. Однако на final-переменные не распространяется code style относительно заглавных букв.

Во всяком случае, в демонстрационном коде важна лаконичность и ясность. У меня ни объявления класса, ни объявления метода нету там, где nan/zero используются. Примеры были бы вдвое длиннее, если выносить эти переменные в константы, а смысла в этом никакого нет.

Интересно, вот предположим, вы юнит-тесты пишете. Там обычно создаётся немало неизменяемых тестовых объектов, инициализируемых единственным образом независимо от пользовательского ввода или фазы луны. Вы тоже их всех в статические поля выносите? Можно пример вашего кода увидеть? :-)
Подумал, что если уж начать придираться к моему коду, то есть куда более очевидные проблемы. Например, в классе DoubleHolder определён метод compareTo, но не определён equals. В результате объекты одинаковые по compareTo могут оказаться неодинаковыми по equals. Это очевидно bad practice, про это в официальной документации говорится. А если определить equals, то по-хорошему надо будет определить hashCode. Я опустил это опять же для ясности изложения. Могу большими буквами написать: ДЕТИ, НЕ ДЕЛАЙТЕ ТАК!
Вы как те феминистки, которые пристали к рубашке Мэтта Тейлора.

PS И не нужно путать что такое code conventions и что такое «правильно».
Цветочки на рубашке звёздного ученого мужа не интересны — они никак не влияют на производительность систем. Константа завернута в класс для получения дополнительных методов работы с ней. Ссылка в переменной на инстанцию такого класса также будет также иметь характер константы. Создание\удаление инстанций классов это дорогостоящая операция. Сборщик мусора потратит дополнительно и время и память на такую инстанцию, что однозначно снизит производительность. Метод класса, где происходит создание, может вызываться очень многократно. Для большой развивающейся системы на этапе проектирования класса мы чаще всего даже не знаем сколько раз такой метод будет вызван и сколько временных инстанций будет создаваться. Конечно, можно пользоваться только вкусом и о проблеме с производительностью, связанной с порождением кучи временных инстанций, «вдруг» узнать перед сдачей проекта в процессе профилирования. Но, можно попробовать и избегать подобных неожиданностей, выбирая несколько иной вкус и стиль.

Естественно, все эти вкусы и стили обязаны выбираться не из личных предпочтений, а исключительно на основе технической целесообразности.

Всех с Новым годом и желаю Вам делать системы с достаточной производительностью, чтобы ваши пользователи никогда не вспоминали про тормоза!
Создание\удаление инстанций классов это дорогостоящая операция. Сборщик мусора потратит дополнительно и время и память на такую инстанцию, что однозначно снизит производительность.
Вы явно не знаете, как работают современные виртуальные машины, а они гораздо умнее! Конкретно в данном случае (вкупе с инлайном конструктора DoubleHolder и метода compareTo, а также escape-анализом) виртуальная машина в состоянии догадаться, что объект не утекает из метода и вообще не создавать его в куче, а разместить даже не на стеке, а в регистрах (тут потребуется всего один 64-битный регистр). Если даже он попадёт в кучу, это будет локальная кэш-линия данного ядра, которая до момента удаления объекта из кучи скорее всего не попадёт даже в основную память. Неубегающие объекты из первого поколения удаляются очень быстро. Наоборот, при загрузке с помощью getstatic мы вряд ли сразу удачно попадём в кэш (между загрузкой класса и выполнением метода могло пройти много времени и вообще это могло выполняться на разных процессорах). Тут мы схватим cache miss и будем дожидаться подгрузки кэш-линии добрую сотню тактов. В общем, я совсем не уверен, что getstatic здесь стопроцентно отработает быстрее. Преждевременная оптимизация — корень зла.

И вас с праздником :-)
Sign up to leave a comment.

Articles

Change theme settings