Пишите компараторы правильно

    В Java для введения порядка среди определённых объектов можно написать компаратор — класс, содержащий функцию compare, которая сравнивает два объекта. Альтернативой компаратору является естественный порядок объектов: объект реализует интерфейс Comparable, который содержит метод compareTo, позволяющий сравнить этот объект с другим. Сравнивающая функция должна вернуть 0, если объекты равны, отрицательное число (обычно -1), если первый объект меньше второго, и положительное число (обычно 1), если первый больше. Обычно реализация такой функции не представляет сложностей, но имеется один случай, о котором многие забывают.

    Сравнение используется различными алгоритмами от сортировки и двоичного поиска до поддержания порядка в сортированных коллекциях вроде TreeMap. Эти алгоритмы завязаны на три важных свойства сравнивающей функции: рефлексивность (сравнение элемента с самим собой всегда даёт 0), антисимметричность (сравнение A с B и B с A должны дать разный знак) и транзитивность (если сравнение A с B и B с C выдаёт одинаковый знак, то и сравнение A с C должно выдать такой же). Если сравнивающая функция не удовлетворяет этим свойствам, алгоритм может выдать совершенно непредсказуемый результат. Причём скорее всего вы не получите никакого исключения, просто результат будет неверный.

    Как обнаружилось, несоблюдение этих свойств — не такая уж редкая ситуация. Проблема возникает при сравнении вещественных чисел — float или double.

    Предположим, у нас имеется класс с полем типа double, и мы хотим упорядочивать объекты этого класса в зависимости от значения поля. Нередко можно встретить такой код:
    public class DoubleHolder implements Comparable<DoubleHolder> {
    	double d;
    	
    	public DoubleHolder(double d) {
    		this.d = d;
    	}
    
    	@Override
    	public int compareTo(DoubleHolder o) {
    		return d > o.d ? 1 : d == o.d ? 0 : -1;
    	}
    
    	@Override
    	public String toString() {
    		return String.valueOf(d);
    	}
    }

    У приведённого метода compareTo есть две проблемы. Первая — незначительная — он не различает +0.0 и -0.0: new DoubleHolder(-0.0).compareTo(new DoubleHolder(+0.0)) вернёт 0. Иногда это нестрашно, но в случае сортировки элементы с +0.0 и -0.0 расположатся в произвольном порядке, что будет смотреться некрасиво. Тем не менее, всё это мелочи по сравнению с NaN. Число NaN (как в типе double, так и во float) — это довольно специфичная вещь. Оно не больше, не меньше и не равно никакому другому числу. В результате мы сразу получаем нарушение свойств сравнения:

    DoubleHolder nan = new DoubleHolder(Double.NaN);
    DoubleHolder zero = new DoubleHolder(0.0);
    System.out.println("nan.compareTo(nan): "+nan.compareTo(nan));
    System.out.println("nan.compareTo(zero): "+nan.compareTo(zero));
    System.out.println("zero.compareTo(nan): "+zero.compareTo(nan));

    nan.compareTo(nan): -1
    nan.compareTo(zero): -1
    zero.compareTo(nan): -1

    Ни рефлексивности, ни антисимметричности не наблюдается. Можно встретить и такую реализацию сравнения:
    public int compareTo(DoubleHolder o) {
        return d > o.d ? 1 : d < o.d ? -1 : 0;
    }

    Здесь все три предыдущих сравнения выдадут ноль, то есть как будто бы свойства соблюдаются. Но, конечно, радоваться рано:

    DoubleHolder nan = new DoubleHolder(Double.NaN);
    DoubleHolder zero = new DoubleHolder(0.0);
    DoubleHolder one = new DoubleHolder(1.0);
    System.out.println("zero.compareTo(nan): "+zero.compareTo(nan));
    System.out.println("nan.compareTo(one): "+nan.compareTo(one));
    System.out.println("zero.compareTo(one): "+zero.compareTo(one));
    

    zero.compareTo(nan): 0
    nan.compareTo(one): 0
    zero.compareTo(one): -1

    Здесь нарушается транзитивность: первый объект равен второму, второй равен третьему, но первый третьему не равен.

    Чем же это грозит простому обывателю? Чтобы понять это, создадим простой список и попробуем его перемешать и посортировать несколько раз:
    List<DoubleHolder> data = new ArrayList<>();
    for(int i=1; i<=10; i++) {
    	data.add(new DoubleHolder(i));
    }
    data.add(new DoubleHolder(Double.NaN));
    
    for(int i=0; i<10; i++) {
    	Collections.shuffle(data);
    	Collections.sort(data);
    	System.out.println(data);
    }

    Вывод в каждом запуске отличается и может выглядеть, например, так:
    [NaN, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
    [NaN, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
    [1.0, NaN, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
    [NaN, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
    [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, NaN]
    [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, NaN, 1.0, 9.0, 10.0]
    [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, NaN, 9.0]
    [NaN, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
    [NaN, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
    [1.0, NaN, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]

    Или для второй реализации compareTo:
    [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 9.0, NaN, 7.0, 10.0]
    [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, NaN]
    [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, NaN]
    [1.0, 2.0, 9.0, 10.0, NaN, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]
    [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, NaN]
    [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, NaN]
    [2.0, 6.0, NaN, 1.0, 3.0, 4.0, 5.0, 7.0, 8.0, 9.0, 10.0]
    [2.0, 4.0, NaN, 1.0, 3.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
    [1.0, 3.0, NaN, 2.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
    [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, NaN]

    Примерно в половине случаев элемент с NaN прибивается к началу или концу сортируемого списка, а в других случаях начинает блуждать, портя порядок окружающих элементов.

    С коллекциями, использующими DoubleHolder в качестве ключа, тоже ничего хорошего не произойдёт. Возьмём, к примеру, TreeSet. Со второй реализацией compareTo всё довольно просто: так как элемент, содержащий NaN, равен любому другому, то в непустое множество вставить его не получится, потому что оно решит, что такой элемент уже есть. Но берегитесь, если вы вставили NaN-элемент первым: после этого во множество не выйдет добавить ничего другого.

    Первый вариант сравнивающей функции психоделичнее. Напишем, например, такой тест:
    Set<DoubleHolder> set = new TreeSet<>();
    for(int i=0; i<50; i++) {
    	set.add(new DoubleHolder( i%10==0 ? Double.NaN : i%10 ));
    }
    System.out.println(set);

    Мы вставили по пять элементов, содержащих NaN, и по пять элементов, содержащих каждую цифру от 1 до 9. В результате имеем следующее:
    [NaN, NaN, 1.0, 2.0, 3.0, NaN, NaN, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, NaN]

    Вполне ожидаемо увидеть пять раз NaN: ведь они не равны друг другу. Но из-за неправильных сравнений и некоторые другие элементы вставились по нескольку раз. Можете сами посмотреть, что случится с TreeMap. Заметьте, что один случайно попавший NaN может испортить всю коллекцию, причём это не всегда легко отладить: коллекция может долго существовать в некорректном состоянии и делать вид, что всё нормально.

    Что любопытно, этой проблемы вообще не должно существовать. Ещё в JDK 1.4 появились специальные статические методы Float.compare и Double.compare, которые сделают всё за вас, корректно обработав специальные случаи. Надо лишь написать:
    public int compareTo(DoubleHolder o) {
    	return Double.compare(d, o.d);
    }

    Видимо, сказывается обманчивая простота алгоритма сравнения: порой программист считает, что проще написать самому, чем искать в документации стандартный способ сделать это.

    Примеры неправильного сравнения double/float в различных открытых проектах: JTS, Batik, Hadoop, Hudson, ICU4J, Lucene. Трудно определить, в каких случаях это может привести к проблемам, но это тот случай, когда я бы исправлял безусловно: правильный и надёжный вариант обычно при этом ещё и короче неправильного.

    Чтобы изменить ситуацию, я написал маленький детектор для FindBugs, который находит некорректно реализованные функции сравнения и предлагает использовать Float.compare/Double.compare.

    Вообще для всех примитивных типов есть подобные методы. Если вам надо сравнить несколько полей по очереди, можно написать так:
    public class MyObject implements Comparable<MyObject> {
    	double d;
    	int i;
    	String s;
    	char c;
    	
    	@Override
    	public int compareTo(MyObject o) {
    		int result;
    		result = Double.compare(d, o.d);
    		if(result != 0) return result;
    		result = Integer.compare(i, o.i);
    		if(result != 0) return result;
    		result = s.compareTo(o.s);
    		if(result != 0) return result;
    		result = Character.compare(c, o.c);
    		return result;
    	}
    }

    Смотрится лучше, чем куча веток с больше и меньше. А если вы пользуетесь Guava или чем-то подобным, тогда так:
    @Override
    public int compareTo(MyObject o) {
    	return ComparisonChain.start()
    			.compare(d, o.d)
    			.compare(i, o.i)
    			.compare(s, o.s)
    			.compare(c, o.c)
    			.result();
    }
    Поделиться публикацией
    Комментарии 36
      +10
      Тот случай, когда хочется плюсануть карму автора раз в пятнадцатый, но можно только один :)
        0
        Мне нравится как это реализовано в rust.
        Как раз для таких случаев у них есть 2 trait'a (почти что интерфейс в java) для сравнений:
        PartialOrd и Ord

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

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

          в связи с тем, что в Java 7 поменялся алгоритм, лежащий в основе Arrays.sort / Collections.sort.
            +2
            Думаю, это хорошо, что оно падает. Обычно это лучше, чем заведомо неверный результат.
              +2
              Далеко не всегда падает. Весьма редко, я бы сказал. Скорость сортировки важнее, чем проверка свойств компаратора.
            +1
            Полезная статья особенно для тех кто пришел в java с языков без таких особенностей с ±0
              0
              С каких например?
                0
                С python например. Там и NaN то спрятанный немного.
                  +1
                  М… в Питоне -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.
                    0
                    Да согласен, промазал )
                    «max» вернет согласно спецификации первый встреченный максимум из равных (так как -0.0 равно 0.0)
                      +1
                      Наверно, всё-таки как-то по-другому в спецификации должно быть. В 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(), вы можете обжечься.
                        0
                        Зато тут более стабильно :-) У меня тот же порядок в разных окружениях выходит. Насчет NaN согласен — вредная штуковина, хорошо хоть не так часто прилететь он может.

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

                        Подводя итог — данная статья я думаю может быть полезна отнюдь не только для java, учитывая что в питоне еще и перегрузка операторов есть.
                    +1
                    Да и 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
                    
                      0
                      Это я назвал спрятан немного. То есть его можно получить только явным заданием через строку.
                      +2
                      А вот это классно:

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

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

                      В Java? А разве это не реализация IEEE 754? Кстати, обычно сравнение двух чисел делают простым вычитанием a — b. Правда, я не в курсе как Java отреагирует на 5.0 — NaN — может исключение какое сгенериует.
                        +4
                        Естественно, это реализация 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)).
                          0
                          Естественно, это реализация IEEE 754. Это как-то противоречит моим словам? :-)

                          Ага. Можно было подумать, что такое представление чисел с плавающей точкой свойственно только для Java, в то время как на других языках все то же самое.

                          (даже не думайте писать return (int)(d-o.d))

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

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

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

                            Тогда у вас не только 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.
                          +3
                          А вообще сравнение вычитанием — это очень плохо: тут переполнение может возникнуть на ровном месте. Если у вас целые числа a = Integer.MIN_VALUE, b = Integer.MAX_VALUE, то a−b будет равно +1, хотя очевидно, что a<b.
                            0
                            плохо бесспорно, но для полноты картины надо отметить что в случ double переполнения не происходит, но результат все равно нелогичный:
                            > Double.compare(-Double.MAX_VALUE, Double.MIN_VALUE-Double.MAX_VALUE)
                            =>
                            0


                            т.е. Double.MIN_VALUE-Double.MAX_VALUE = -Double.MAX_VALUE
                              +2
                              Кажется, вы немного запутались. В 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, которые всегда работают :-)
                            0
                            del
                              0
                              del
                              0
                              Т.е. статья о том, что даблы нельзя сравнивать вычитанием? :)
                                0
                                Э? Про вычитание в статье ни слова не было.
                            +2
                            Отличные грабли автор описал! Спасибо!
                              –3
                              Статья претендует на обучение читателя правильному в области 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.
                                +3
                                У вас настолько оригинальная точка зрения, что я даже не знаю, как вам возразить :-)

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

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

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

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

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

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

                                        И вас с праздником :-)

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое