Перейти к содержанию

Примитивы против объектов в Java

В этом руководстве покажем плюсы и минусы использования примитивных типов Java и их обернутых аналогов.

Система типов Java

Java имеет двойную систему типов, состоящую из примитивов, таких как int, boolean, и ссылочных типов, таких как Integer, Boolean. Каждый примитивный тип соответствует ссылочному типу.

Каждый объект содержит одно значение соответствующего примитивного типа. Классы-оболочки являются неизменяемыми (immutable) (поэтому их состояние не может измениться после создания объекта) и окончательными (final) (поэтому нельзя наследоваться от них).

Под капотом Java выполняет преобразование между примитивными и ссылочными типами, если фактический тип отличается от объявленного:

Integer j = 1;          // автоупаковка
int i = new Integer(1); // распаковка

Процесс преобразования примитивного типа в эталонный называется автоупаковкой (autoboxing), противоположный процесс называется распаковкой (unboxing).

Плюсы и минусы

Решение о том, какой объект следует использовать, основано на том, какую производительность приложения пытаемся достичь, сколько доступной памяти есть, объем доступной памяти и какие значения по умолчанию должны обрабатывать.

Если не столкнемся ни с одним из них, можно игнорировать эти соображения, хотя знать их стоит.

Объем памяти для одного элемента

Переменные примитивного типа имеют следующее влияние на память:

  • boolean – 1 бит;
  • byte – 8 бит;
  • short, char – 16 бит;
  • int, float – 32 бита;
  • long, double – 64 бита.

На практике эти значения могут различаться в зависимости от реализации виртуальной машины. Например, в виртуальной машине Oracle boolean сопоставляется со значениями int 0 и 1, поэтому он занимает 32 бита, как описано здесь: Примитивные типы и значения.

Переменные этих типов находятся в стеке и поэтому доступны быстро. Для получения подробной информации смотри здесь.

Ссылочные типы – это объекты, они живут в куче и относительно медленны для доступа. У них есть определенные накладные расходы по сравнению с их примитивными аналогами.

Конкретные значения накладных расходов обычно зависят от JVM. Здесь представлены результаты для 64-битной виртуальной машины со следующими параметрами:

java 10.0.1 2018-04-17
Java(TM) SE Runtime Environment 18.3 (build 10.0.1+10)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.1+10, mixed mode)

Чтобы получить внутреннюю структуру объекта, можно использовать инструмент Java Object Layout (см. Как получить размер объекта в Java).

Получается, что один экземпляр ссылочного типа на этой JVM занимает 128 бит, кроме Long и Double, которые занимают 192 бита:

  • Boolean – 128 бит;
  • Byte – 128 бит;
  • Short, Character – 128 бит;
  • Integer, Float – 128 бит;
  • Long, Double – 192 бита.

Одна переменная типа Boolean занимает столько же места, сколько 128 примитивных, а одна переменная Integer занимает столько же места, сколько четыре int.

Объем памяти для массивов

Ситуация становится более интересной, если сравнить, сколько памяти занимают массивы рассматриваемых типов. Когда создаем массивы с различным количеством элементов для каждого типа, получаем такой график:

График демонстрирует, что типы сгруппированы в четыре семейства в зависимости от того, как память m(s) зависит от количества элементов s массива:

  • long, double: m(s) = 128 + 64 s
  • short, char: m(s) = 128 + 64 [s/4]
  • byte, boolean: m(s) = 128 + 64 [s/8]
  • the rest: m(s) = 128 + 64 [s/2]

Квадратные скобки обозначают стандартную функцию предела.

Удивительно, но массивы примитивных типов long и double потребляют больше памяти, чем их классы-оболочки Long и Double.

Одноэлементные массивы примитивных типов почти всегда дороже (кроме long и double), чем соответствующий ссылочный тип.

Производительность

Производительность Java-кода – довольно тонкая проблема, она во многом зависит от аппаратного обеспечения, на котором выполняется код, от компилятора, который может выполнять определенные оптимизации, от состояния виртуальной машины, от активности других процессов в операционной системе.

Примитивные типы живут в стеке, а ссылочные типы – в куче. Это доминирующий фактор, определяющий скорость доступа к объектам.

Чтобы продемонстрировать, насколько операции для примитивных типов быстрее операций для классов-оболочек, создадим массив из пяти миллионов элементов, в котором все элементы равны, кроме последнего, затем выполним поиск этого элемента:

while (!pivot.equals(elements[index])) {
    index++;
}

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

Для этого используем известный инструмент сравнительного анализа JMH (см. руководство о том, как его использовать). Результаты операции поиска можно обобщить на этой диаграмме:

Даже для такой простой операции требуется больше времени для выполнения операции для классов-оболочек.

В случае более сложных операций, таких как суммирование, умножение или деление, разница в скорости может резко возрасти.

Значения по умолчанию

Значения по умолчанию примитивных типов 0 (в соответствующем представлении, т.е. 0, 0.0d и т. д.) для числовых типов, false для boolean, \u0000 для типа char. Для классов-оболочек значение по умолчанию равно null.

Это означает, что примитивные типы могут получать значения только из своих доменов, тогда как ссылочные типы могут получать значение (null), которое в каком-то смысле не принадлежит их доменам.

Хотя не рекомендуется оставлять переменные неинициализированными, иногда можно присвоить значение после его создания.

В такой ситуации, когда переменная примитивного типа имеет значение, равное ее типу по умолчанию, необходимо выяснить, действительно ли была инициализирована переменная. С переменными класса-оболочки такой проблемы нет, поскольку значение null является вполне очевидным признаком того, что переменная не была инициализирована.

Использование

Примитивные типы намного быстрее и требуют гораздо меньше памяти. Поэтому можно предпочесть их использование. С другой стороны, текущая спецификация языка Java не позволяет использовать примитивные типы в параметризованных типах (дженериках), в коллекциях Java или Reflection API.

Если приложению нужны коллекции с большим количеством элементов, необходимо рассмотреть возможность использования массивов как можно более «экономного» типа, как это показано на графике выше.

Заключение

В этом руководстве было показано, что объекты в Java работают медленнее и имеют большее влияние на память, чем их примитивные аналоги.

Фрагменты кода можно найти на GitHub.

Оригинал