Кэширование класса Integer

В Java 5 для оптимизации памяти и улучшения производительности появилась функция кеширования класса-оболочки Integer. Прежде чем говорить об этой функции, рассмотрим следующий пример :

public class IntegerCacheExample
{
    public static void main(String[] args) 
    {
        Integer integer1 = 3;
        Integer integer2 = 3;

        Integer integer3 = 129;
        Integer integer4 = 129;

        if (integer1 == integer2)
            System.out.println("integer1 == integer2");
        else
            System.out.println("integer1 != integer2");

        if (integer3 == integer4)
            System.out.println("integer3 == integer4");
        else
            System.out.println("integer3 != integer4");
	}
}

В примере, на первый взгляд, нет ничего необычного. Все тривиально просто : определены 4 переменные, которым попарно присвоены одинаковые значения. С точки зрения Java, это четыре разных объекта. Поэтому попарные сравнения оператором '==' теоретически должны вернуть логическое false.

Примечание : необходимо отметить, что оператор сравнения '==' проверяет ссылки на объекты; значения объектов проверяются оператором equals. Поэтому в примере для целочисленных объектов лучше было бы использовать сравнение с оператором equals, т.е. if (integer3.equals(integer4))

В примере результаты сравнения возвращают разные значения и в консоль выводятся следующие сообщения :


integer1 == integer2
integer3 != integer4
 

Что странно в этом примере, так это разное поведение операторов сравнения : два похожих сравнения целочисленных объектов в операторах if возвращают различные логические значения. Но, вот, если значения переменных второй пары снизить до 125, т.е. определить значения в диапазоне от -128 до +127, то результаты сравнений будут одинаковы положительные (true).

Данное поведение целочисленных объектов объясняется использованием функции кэширования, введенное в Java 5. Целочисленные объекты кэшируются внутри, и повторно используются через те же связанные объекты. Это применимо для значений в диапазоне от -128 до + 127.

Автоупаковка и автораспаковка объектов

Целочисленное кэширование работает только при автоупаковке. При создании целочисленного объекта с помощью конструктора кэширование не работает. Автоупаковка (autoboxing) – это автоматическое преобразование, выполненное компилятором Java из примитива в соответствующий ему тип класса оболочки Java. Это все равно, что использовать valueOf следующим образом :

// Автоупаковка
Integer integer1 = 3;
Integer integer2 = Integer.valueOf(3);

Автораспаковка (unboxing) – это преобразование класса-обёртки в соответствующий ему примитивный тип. Если при распаковке класс-обёртка был равен null, произойдет исключение java.lang.NullPointerException.

Поскольку арифметические операторы и операторы сравнения (исключение == и !=) применяются только к примитивным типам, приходилось делать распаковку вручную, что заметно снижало читабельность выражений, делая их громоздкими.

Integer i1 = new Integer(14);
Integer i2 = new Integer(28);
System.out.println(i1.intValue() > i2.intValue());

Благодаря автораспаковке можно смело использовать выражения, пренебрегая методами конвертации. Теперь это контролирует компилятор Java.

System.out.println(i1 > i2);

Таким образом, чтобы в примере достичь ожидаемого результата, следовало бы использовать конструктор, а не автоупаковку и автораспаковку, т.е. одну из переменных (или обе) инициализировать конструктором :

Integer integer1 = 3;
Integer integer2 = new Integer(3);

Таким образом, теперь мы знаем, где это кэширование должно быть реализовано в источнике Java JDK. Давайте посмотрим на исходный код метода valueOf от JDK (сборка 25 Java JDK 1.8.0).

public static Integer valueOf(int i) {
  if ((i >= IntegerCache.low) && (i <= IntegerCache.high))
      return IntegerCache.cache[i + (-IntegerCache.low)];
  return new Integer(i);
}

Вот мы и вышли на класс IntegerCache, определяющий различное поведение целочисленных объектов.

Класс IntegerCache

IntegerCache – это внутренний статический класс объекта Integer. Он хорошо документирован в JDK. Посмотрим на его код :

/**
 * Кэш поддержки семантики автоупаковки для значений 
 * идентификаторов объектов -128 и 127 (включительно) 
 * в соответствии с требованиями JLS.
 *
 * Кэш инициализируется при первом использовании. Размер 
 * кэша может управляться с помощью опции
 * {@code-XX: AutoBoxCacheMax=}. Во время инициализации 
 * виртуальной машины, java.lang.Integer.IntegerCache.high 
 * может быть установлен и сохранен в свойствах частной 
 * системы в sun.misc.VM class.
*/

private static class IntegerCache 
{
    static final int low = -128;
    static final int high;
    static final Integer cache[];
    static {
        int h = 127;
        String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty(
                    "java.lang.Integer.IntegerCache.high");

        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Максимальный размер массива Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) { }
        }
        high = h;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
        // диапазон [-128, 127] должен быть интернирован (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }
    private IntegerCache() {}
}

В комментарии Javadoc ясно говорится, что данный класс предназначен для кэширования и для поддержки автоупаковки значений в диапазоне от -128 до 127. Максимальное значение 127 может быть изменено с помощью VM аргумента -XX:AutoBoxCacheMax=size. Таким образом, кэширование происходит в цикле for-loop. Он просто работает от минимального до максимального значений и создает целочисленные экземпляры, которые хранит в массиве 'cache'. Кэширование выполняется при первом использовании класса Integer. После этого кэшированные экземпляры используются вместо создания нового экземпляра (во время autoboxing).

Когда функция кэширования была впервые представлена в Java 5, диапазон значений был зафиксирован от -128 до +127. Позже в Java 6, максимальное значение диапазона было соотнесено с java.lang.Integer.IntegerCache.high, а аргумент VM позволяет установить бо́льшее значение, что придает гибкости для настройки производительности в соответствии с использованием приложения.

Другие кэшированные классы-оболочки

Кэширование касается не только класса-оболочки Integer. Имеются аналогичные реализации кэширования для других классов-оболочек целочисленных типов :

  • ByteCache
  • ShortCache
  • LongCache
  • CharacterCache

Классы Byte, Short, Long имеют фиксированный диапазон кэширования, т. е. значения от -128 до 127 включительно. Класс Character кэширует значения в диапазоне от 0 до 127 включительно. Диапазон кэширования этих классов не может быть изменен с помощью аргумента VM, как это делается для Integer.

Принудительное использование кэша в спецификации языка Java

В спецификации языка Java отмечено, что если значение p является целочисленным литералом типа int в диапазоне от -128 до 127 включительно (§3.10.1), или логическим литералом 'true' или 'false' (§3.10.3), или символьным литералом между ‘\u0000’ и ‘\u007f’ включительно (§3.10.4), то пусть тогда a и b являются результатами любых двух преобразований упаковки p. В этом случае всегда будет выполняться условие a == b.

Приведённая инструкция гарантирует, что ссылка на объекты со значениями от -128 до 127 должна быть одинаковой.

В идеале, упаковка примитивного значения всегда будет иметь идентичную ссылку. На практике это может оказаться невозможным с использованием существующих методов реализации. Правило выше – это прагматический компромисс, требующий, чтобы некоторые общие ценности всегда были упакованы в неразличимые объекты. Реализация может кэшировать их (быстро или медленно). Для других значений правило запрещает любые предположения относительно идентичности упакованных значений со стороны программиста. Это позволяет (но не требует) совместное использование некоторых или всех этих ссылок. Обратите внимание, что целочисленные литералы типа long могут быть общими, но не обязательными.

Для не очень мощных устройств в большинстве случаях кэширование обеспечивает желанное поведение. Менее ограниченные по памяти устройства (VM) могли бы, например, кэшировать все char и короткие значения, а также int и длинные значения в диапазоне от -32K до +32K.


Материал для статьи был получен от Юрия Пахолкова (admin@upread.ru). С другими статьями автора можно познакомиться на сайте https://upread.ru/blog/new .

  Рейтинг@Mail.ru