1. Обзор
В этом руководстве мы покажем плюсы и минусы использования примитивных типов Java и их обернутых аналогов.
2. Система типов Java
Java имеет двойную систему типов, состоящую из примитивов, таких как int
, boolean
и ссылочных типов, таких как Integer,
Boolean
. Каждый примитивный тип соответствует ссылочному типу.
Каждый объект содержит одно значение соответствующего примитивного типа. Классы- оболочки являются неизменяемыми (поэтому их состояние не может измениться после создания объекта) и окончательными (поэтому мы не можем наследовать от них).
Под капотом Java выполняет преобразование между примитивными и ссылочными типами, если фактический тип отличается от объявленного:
Integer j = 1; // autoboxing
int i = new Integer(1); // unboxing
Процесс преобразования примитивного типа в эталонный называется автоупаковкой, противоположный процесс называется распаковкой.
3. Плюсы и минусы
Решение о том, какой объект следует использовать, зависит от того, какую производительность приложения мы пытаемся достичь, сколько доступной памяти у нас есть, объем доступной памяти и какие значения по умолчанию мы должны обрабатывать.
Если мы не столкнемся ни с одним из них, мы можем игнорировать эти соображения, хотя знать их стоит.
3.1. Объем памяти для одного элемента
Просто для справки, переменные примитивного типа имеют следующее влияние на память:
- логическое значение — 1 бит
- байт – 8 бит
- короткий, символьный — 16 бит
- целое число, число с плавающей запятой — 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 бита:
- Булево значение — 128 бит.
- Байт – 128 бит
- Короткий, символьный — 128 бит
- Целое число, число с плавающей запятой — 128 бит
- Длинный, двойной — 192 бита
Мы видим, что одна переменная типа Boolean
занимает столько же места, сколько 128 примитивных, а одна переменная Integer
занимает столько же места, сколько четыре int
.
3.2. Объем памяти для массивов
Ситуация становится более интересной, если сравнить, сколько памяти занимают массивы рассматриваемых типов.
Когда мы создаем массивы с различным количеством элементов для каждого типа, мы получаем график:
это демонстрирует, что типы сгруппированы в четыре семейства в зависимости от того, как память m (s)
зависит от количества элементов s массива:
- длинный, двойной: м(с) = 128 + 64 с
- короткий, char: m(s) = 128 + 64 [s/4]
- байт, логическое значение: m(s) = 128 + 64 [s/8]
- остальное: м(с) = 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.