Java примитивы и классы-обертки: плюсы и минусы

В этом руководстве мы рассмотрим плюсы и минусы использования примитивов в Java и соответствующих им классов-оберток.
1. Типы переменных в Java
В языке Java существует два типа переменных: примитивные, например int и boolean, а также ссылочные типы вроде Integer и Boolean (классы-обертки). Для каждого примитивного типа существует, соответствующий ему ссылочный тип.
Классы-обертки являются неизменяемыми: это означает, что после создания объекта его состояние (значение поля value) не может быть изменено; и задекларированы, как final (от этих классов невозможно наследоваться).
Java автоматически производит преобразования между примитивными типами и их обертками:

Integer j = 1;          // autoboxing
int i = new Integer(1); // unboxing
Процесс преобразования примитивных типов в ссылочные называется автоупаковкой (autoboxing), а обратный ему — автораспаковкой (unboxing).
2. Плюсы и минусы
Решение, переменные каких типов использовать, основывается на необходимой нам производительности приложения, объеме доступной памяти и значений по умолчанию с которыми мы собираемся работать.
2.1 Использование памяти единичными объектами
В зависимости от реализации виртуальной машины, эти значения могут изменяться. Например, в виртуальной машине Oracle значения типа boolean сопоставляются со значениями 0 и 1 типа int (это связано с тем, что в VM нет инструкций для работы с булевыми значениями) и, как результат, занимают в памяти 32 бита.
Переменные примитивных типов хранятся в стеке, позволяя иметь к ним быстрый доступ.
Ссылочные переменные ссылаются на объекты, которые хранятся в куче, к ним мы имеем более медленный доступ (рекомендуем ознакомиться с понятиями Стек и Куча, для лучшего усвоения материала). Ссылочные типы имеют определенные накладные расходы относительно их примитивных аналогов.
Накладные расходы зависят от реализации конкретной JVM. Здесь мы приведем результаты для 64-х битной виртуальной машины со следующими параметрами:
В результате, один экземпляр ссылочного типа на данной JVM занимает 128 бит. За исключением Long и Double, которые занимают 192 бита:
  • Boolean — 128 бит
  • Byte — 128 бит
  • Short, Character — 128 бит
  • Integer, Float — 128 бит
  • Long, Double — 192 бита
Обратите внимание, переменная типа Boolean занимает в 128 раз больше места чем соответствующий ей примитив, тогда как Integer занимает памяти как 4 int переменные.
2.2 Использование памяти массивами
Более интересно обстоят дела с объемом памяти который занимают массивы, рассматриваемых нами типов.
При создании массивов с различным количеством элементов для каждого типа, мы получаем график:
который демонстрирует как массивы различных типов потребляют память (m) в зависимости от количества содержащихся элементов (e):
  • long, double: m = 128 + 64e
  • short, char: m = 128 + 64(e/4)
  • byte, boolean: m) = 128 + 64(e/8)
  • the rest: m = 128 + 64(e/2)
где значения в скобках округляются до ближайшего меньшего.
Это может показаться неожиданным, но массивы примитивных типов long и double занимают больше памяти чем их классы-обертки Long и Double соответственно.
2.3 Производительность
Производительность Java кода, довольно тонкий вопрос, сильно зависящий от аппаратной части устройства на котором он исполняется, от различных оптимизационных процессов, выполняемых компилятором, от конкретной JVM, а также от других процессов, происходящих в операционной системе.
Мы уже упоминали ранее, что переменные примитивных типов живут в стеке, тогда как ссылочные переменные хранят ссылки на объекты расположенные в куче. Это важное свойство, определяющее скорость доступа к данным.
Чтобы продемонстрировать насколько операции над примитивными типами быстрее чем над их классами-обертками, создадим массив из миллиона элементов в котором все элементы одинаковы, кроме последнего. Затем мы попытаемся найти этот элемент и сравним результаты производительности работы массива переменных примитивных типов с массивом ссылочных переменных.
Мы используем известный инструмент тестирования производительности JMH, а результаты операции суммируем в диаграмме:
Как мы видим, даже выполнение такой простой операции требует значительно больше времени для классов-оберток, чем для их примитивных аналогов.
В случае более трудоемких операций, таких как сложение, умножение или деление разница в скорости может значительно увеличиться.
2.4 Значения по умолчанию
Значение по умолчанию для примитивных числовых типов равно 0 (0 для целочисленных, 0.0d и 0.0f для double и float соответственно), для boolean — false, а для типа char — \u0000. Для классов-оберток значение по умолчанию равно null.
Не считается хорошей практикой оставлять переменные неинициализированными, хотя мы можем присваивать значение переменной после ее создания.
В некоторых случаях, когда переменная примитивного типа имеет значение, равное значению по умолчанию, приходится выяснять, действительно ли она была проинициализирована.
С переменными ссылочных типов такой проблемы не возникает, так как значение null является очевидным признаком того, что переменная не была проинициализирована.
3. Использование
Как мы видели, примитивные типы предлагают более быстрый доступ к данным и потребляют меньше памяти. Исходя из этого, их использование более предпочтительно.
С другой стороны, текущая спецификация языка Java не позволяет использовать примитивные типы при параметризации (generics), в коллекциях Java или в Reflection API.
Когда в приложении нам нужно использовать коллекции с большим количеством элементов, первым делом необходимо рассмотреть вариант использования массивов с наиболее экономичным типом переменных.
Вывод
В этом уроке мы продемонстрировали, что объекты в Java медленнее и нуждаются в больших ресурсах памяти, чем их примитивные аналоги.

Код и тесты данной статьи можно найти на GitHub
Оригинал статьи «Java Primitives versus Objects»
Оцените статью, если она вам понравилась!