«1. Обзор
В этом руководстве мы покажем плюсы и минусы использования примитивных типов Java и их обернутых аналогов.
2. Система типов Java
Java имеет двойную систему типов, состоящую из примитивов, таких как int, boolean, и ссылочных типов, таких как Integer, Boolean. Каждый примитивный тип соответствует ссылочному типу.
Каждый объект содержит одно значение соответствующего примитивного типа. Классы-оболочки являются неизменяемыми (поэтому их состояние не может измениться после создания объекта) и окончательными (поэтому мы не можем наследовать от них).
Под капотом Java выполняет преобразование между примитивным и ссылочным типами, если фактический тип отличается от объявленного:
Integer j = 1; // autoboxing
int i = new Integer(1); // unboxing
Процесс преобразования примитивного типа в ссылочный называется автоупаковкой, противоположный процесс называется распаковкой.
3. Плюсы и минусы
Решение о том, какой объект использовать, зависит от того, какую производительность приложения мы пытаемся достичь, сколько доступной памяти у нас есть, объем доступной памяти и какие значения по умолчанию мы должны обрабатывать.
Если мы не столкнемся ни с одним из них, мы можем игнорировать эти соображения, хотя знать их стоит.
3.1. Объем памяти для одного элемента
Просто для справки, переменные примитивного типа оказывают следующее влияние на память:
-
boolean — 1 бит byte — 8 бит short, char — 16 бит int, float – длина 32 бита, двойная – 64 бита
На практике эти значения могут различаться в зависимости от реализации виртуальной машины. Например, в виртуальной машине Oracle логический тип сопоставляется со значениями int 0 и 1, поэтому он занимает 32 бита, как описано здесь: Примитивные типы и значения.
Переменные этих типов находятся в стеке и поэтому доступны быстро. Для получения подробной информации мы рекомендуем наш учебник по модели памяти Java.
Ссылочные типы — это объекты, они живут в куче и относительно медленны в обращении. У них есть определенные накладные расходы по сравнению с их примитивными аналогами.
Конкретные значения накладных расходов обычно зависят от 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 (см. другой наш учебник о том, как получить размер объект).
Оказывается, один экземпляр ссылочного типа на этой JVM занимает 128 бит, кроме Long и Double, которые занимают 192 бита:
-
Boolean — 128 бит Byte — 128 бит Short, Character — “ 128 бит Integer, Float – 128 бит Long, Double – 192 бит
Мы видим, что одна переменная типа Boolean занимает столько же места, сколько 128 примитивных, а одна переменная Integer занимает столько же места, сколько четыре внутр.
3.2. Объем памяти для массивов
Ситуация становится более интересной, если мы сравним, сколько памяти занимают массивы рассматриваемых типов.
Когда мы создаем массивы с разным количеством элементов для каждого типа, мы получаем график:
который демонстрирует, что типы сгруппированы в четыре семейства в зависимости от того, как память m(s) зависит от количества элементы массива s:
-
long, double: m(s) = 128 + 64 s, short, char: m(s) = 128 + 64 [s/4] байт, boolean: m(s) = 128 + 64 [с/8] остальные: m(s) = 128 + 64 [с/2]
где квадратные скобки обозначают стандартную функцию потолка.
Удивительно, но массивы примитивных типов long и double потребляют больше памяти, чем их классы-оболочки Long и Double.
Мы видим либо то, что одноэлементные массивы примитивных типов почти всегда дороже (кроме long и double), чем соответствующий ссылочный тип.
3.3. Производительность
Производительность Java-кода — довольно тонкая проблема, она во многом зависит от аппаратного обеспечения, на котором выполняется код, от компилятора, который может выполнять определенные оптимизации, от состояния виртуальной машины, от активности другие процессы операционной системы.
«Как мы уже упоминали, примитивные типы живут в стеке, а ссылочные типы — в куче. Это доминирующий фактор, определяющий скорость доступа к объектам.
Чтобы продемонстрировать, насколько операции для примитивных типов быстрее операций для классов-оболочек, давайте создадим массив из пяти миллионов элементов, в котором все элементы равны, кроме последнего; затем мы выполняем поиск этого элемента:
while (!pivot.equals(elements[index])) {
index++;
}
и сравниваем производительность этой операции для случая, когда массив содержит переменные примитивных типов, и для случая, когда он содержит объекты ссылочных типов.
Мы используем хорошо известный инструмент бенчмаркинга JMH (о том, как его использовать, см. в нашем руководстве), и результаты операции поиска можно обобщить на этой диаграмме:
Даже для такой простой операции, мы видим, что для выполнения операции для классов-оболочек требуется больше времени.
В случае более сложных операций, таких как суммирование, умножение или деление, разница в скорости может резко возрасти.
3.4. Значения по умолчанию
Значения по умолчанию примитивных типов: 0 (в соответствующем представлении, т.е. 0, 0.0d и т. д.) для числовых типов, false для логического типа, \\u0000 для типа char. Для классов-оболочек значение по умолчанию равно null.
Это означает, что примитивные типы могут получать значения только из своих доменов, тогда как ссылочные типы могут получать значение (null), которое в каком-то смысле не принадлежит их доменам.
Хотя не рекомендуется оставлять переменные неинициализированными, иногда мы можем присвоить значение после их создания.
В такой ситуации, когда переменная примитивного типа имеет значение, равное ее типу по умолчанию, мы должны выяснить, действительно ли была инициализирована переменная.
С переменными класса-оболочки такой проблемы нет, так как значение null является очевидным признаком того, что переменная не была инициализирована.
4. Использование
Как мы видели, примитивные типы намного быстрее и требуют гораздо меньше памяти. Поэтому мы можем предпочесть их использование.
С другой стороны, текущая спецификация языка Java не позволяет использовать примитивные типы в параметризованных типах (дженериках), в коллекциях Java или API Reflection.
Когда нашему приложению нужны коллекции с большим количеством элементов, мы должны рассмотреть возможность использования массивов как можно более «экономного» типа, как показано на графике выше.
5. Заключение
В этом уроке мы показали, что объекты в Java работают медленнее и имеют большее влияние на память, чем их примитивные аналоги.
Как всегда, фрагменты кода можно найти в нашем репозитории на GitHub.
«