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

Примитивы 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. След памяти для массивов

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

Когда мы создаем массивы с различным количеством элементов для каждого типа, мы получаем график:

image

который демонстрирует, что типы сгруппированы в четыре семейства в зависимости от того, как память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 о том, как его использовать), и результаты операции поиска можно обобщить на этой диаграмме:

image

 

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

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

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

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

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

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

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

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

4. использование

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

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

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

5. Заключение

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

Как всегда, фрагменты кода можно найти в нашихrepository on GitHub.