Примитивы Java против объектов
1. обзор
В этом уроке мы покажем все за и против использования примитивных типов Java и их свернутых аналогов.
2. Java Type System
Java имеет двойную систему типов, состоящую из примитивов, таких какint,boolean, и ссылочных типов, таких какInteger,Boolean. Каждый тип примитива соответствует ссылочному типу.
Каждый объект содержит одно значение соответствующего типа примитива. wrapper classes are immutable (чтобы их состояние не могло измениться после создания объекта) и являются окончательными (чтобы мы не могли наследовать от них).
Под капотом Java выполняет преобразование между примитивным и ссылочным типами, если фактический тип отличается от объявленного:
Integer j = 1; // autoboxing
int i = new Integer(1); // unboxing
Процесс преобразования примитивного типа в ссылочный называется автобоксом, противоположный процесс называется распаковкой.
3. Плюсы и минусы
Решение о том, какой объект следует использовать, основывается на том, какую производительность приложения мы пытаемся достичь, какой объем доступной памяти у нас есть, объем доступной памяти и какие значения по умолчанию мы должны обработать.
Если мы не столкнемся ни с одним из этих факторов, мы можем игнорировать эти соображения, хотя их стоит знать.
3.1. Отдельный элемент памяти
Для справки,primitive type variables имеют следующее влияние на память:
-
логическое значение - 1 бит
-
байт - 8 бит
-
короткий, символ - 16 бит
-
int, float - 32 бита
-
длинный, двойной - 64 бита
На практике эти значения могут варьироваться в зависимости от реализации виртуальной машины. В виртуальной машине Oracle логический тип, например, сопоставляется со значениями int 0 и 1, поэтому он занимает 32 бита, как описано здесь:Primitive Types and Values.
Переменные этих типов живут в стеке и, следовательно, доступны быстро. Для подробностей мы рекомендуем нашtutorial на модели памяти 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 (см. Наш другойtutorial о том, как получить размер объекта).
Оказывается, один экземпляр ссылочного типа на этой JVM занимает 128 бит, за исключениемLong иDouble, которые занимают 192 бита:
-
Логическое значение - 128 бит
-
Байт - 128 бит
-
Короткий, символьный - 128 бит
-
Integer, Float - 128 бит
-
Длинный, двойной - 192 бита
Мы видим, что одна переменная типаBoolean занимает столько же места, сколько 128 примитивных, а одна переменнаяInteger занимает столько же места, как четыреint.
3.2. След памяти для массивов
Ситуация становится более интересной, если сравнить, сколько памяти занимают массивы рассматриваемых типов.
Когда мы создаем массивы с различным количеством элементов для каждого типа, мы получаем график:
который демонстрирует, что типы сгруппированы в четыре семейства в зависимости от того, как памятьm(s) зависит от количества элементов s массива:
-
длинный, двойной: м (с) = 128 + 64 с
-
короткий, символ: м (с) = 128 + 64 [с / 4]
-
байт, логическое значение: m (s) = 128 + 64 [s / 8]
-
остальное: м (с) = 128 + 64 [с / 2]
где квадратные скобки обозначают стандартную функцию потолка.
Удивительно, но массивы примитивных типов long и double потребляют больше памяти, чем их классы-оболочкиLong иDouble.
Мы видим либо, чтоsingle-element arrays of primitive types are almost always more expensive (except for long and double) than the corresponding reference type.
3.3. Спектакль
Производительность Java-кода является довольно тонкой проблемой, она во многом зависит от аппаратного обеспечения, на котором выполняется код, от компилятора, который может выполнять определенные оптимизации, от состояния виртуальной машины, от активности других процессов в Операционная система.
Как мы уже упоминали, примитивные типы живут в стеке, а ссылочные типы - в куче. Это доминирующий фактор, определяющий скорость доступа к объектам.
Чтобы продемонстрировать, насколько операции для примитивных типов выполняются быстрее, чем для классов-оболочек, давайте создадим массив из пяти миллионов элементов, в котором все элементы равны, за исключением последнего; затем мы выполняем поиск этого элемента:
while (!pivot.equals(elements[index])) {
index++;
}
и сравнить производительность этой операции для случая, когда массив содержит переменные примитивных типов, и для случая, когда он содержит объекты ссылочных типов.
Мы используем хорошо известный инструмент тестированияJMH (см. Нашtutorial о том, как его использовать), и результаты операции поиска можно обобщить на этой диаграмме:
Даже для такой простой операции мы видим, что для выполнения операции для классов-оболочек требуется больше времени.
В случае более сложных операций, таких как суммирование, умножение или деление, разница в скорости может резко возрасти.
3.4. Значения по умолчанию
Значения по умолчанию для примитивных типов -0 (в соответствующем представлении, т.е. 0,0.0d и т.д.) для числовых типов,false для логического типа,