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 бит
- короткие, 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 бита:
- Логическое значение – 128 бит
- Байт – 128 бит
- Короткий, символьный – 128 бит
- Целое число, с плавающей точкой – 128 бит
- Длинные, двойные 192 бита
Мы видим, что одна переменная типа Boolean занимает столько же места, сколько 128 примитивных, в то время как одна переменная Integer занимает столько же места, сколько четыре int единицы.
3.2. Объем памяти для массивов
Ситуация становится более интересной, если мы сравним, сколько памяти занимают массивы рассматриваемых типов.
Когда мы создаем массивы с различным количеством элементов для каждого типа, мы получаем график:
это демонстрирует, что типы сгруппированы в четыре семейства в зависимости от того, как память m(s) зависит от количества элементов s массива:
- длинный, двойной: + 64 с
- короткий, char: + 64 [s/4]
- байт, логическое значение: + 64 [s/8]
- остальные: + 64 [s/2]
где квадратные скобки обозначают стандартную функцию потолка.
Удивительно, но массивы примитивных типов long и double потребляют больше памяти, чем их классы-оболочки Long и Double .
Мы можем видеть, что одноэлементные массивы примитивных типов почти всегда дороже (за исключением длинных и двойных), чем соответствующий ссылочный тип .
3.3. Производительность
Производительность Java-кода-довольно тонкая проблема, она очень сильно зависит от аппаратного обеспечения, на котором выполняется код, от компилятора, который может выполнять определенные оптимизации, от состояния виртуальной машины, от активности других процессов в операционной системе.
Как мы уже упоминали, примитивные типы живут в стеке, в то время как ссылочные типы живут в куче. Это доминирующий фактор, определяющий скорость доступа к объектам.
Чтобы продемонстрировать, насколько операции для примитивных типов быстрее, чем для классов-оболочек, давайте создадим массив из пяти миллионов элементов, в котором все элементы равны, за исключением последнего; затем мы выполним поиск для этого элемента:
while (!pivot.equals(elements[index])) { index++; }
и сравните производительность этой операции для случая, когда массив содержит переменные примитивных типов, и для случая, когда он содержит объекты ссылочных типов.
Мы используем хорошо известный инструмент JMH бенчмаркинга (см. Наш учебник о том, как его использовать), и результаты операции поиска можно обобщить на этой диаграмме:
Даже для такой простой операции мы видим, что для выполнения операции для классов-оболочек требуется больше времени.
В случае более сложных операций, таких как суммирование, умножение или деление, разница в скорости может резко возрасти.
3.4. Значения по умолчанию
Значения по умолчанию примитивных типов 0 (в соответствующем представлении, т. е. 0 , 0.0 d etc) для числовых типов, false для логического типа, \u0000 для типа char. Для классов-оболочек значение по умолчанию равно null .
Это означает, что примитивные типы могут получать значения только из своих доменов, в то время как ссылочные типы могут получать значение ( null ), которое в некотором смысле не принадлежит их доменам.
Хотя не считается хорошей практикой оставлять переменные неинициализированными, иногда мы можем присвоить значение после его создания.
В такой ситуации, когда переменная примитивного типа имеет значение, равное значению ее типа по умолчанию, мы должны выяснить, действительно ли переменная была инициализирована.
С переменными класса-оболочки такой проблемы нет, поскольку значение null является вполне очевидным признаком того, что переменная не была инициализирована.
4. Использование
Как мы уже видели, примитивные типы намного быстрее и требуют гораздо меньше памяти. Поэтому мы, возможно, предпочтем использовать их.
С другой стороны, текущая спецификация языка Java не позволяет использовать примитивные типы в параметризованных типах (универсальных), в коллекциях Java или API отражения.
Когда нашему приложению нужны коллекции с большим количеством элементов, мы должны рассмотреть возможность использования массивов с максимально “экономичным” типом, как это показано на графике выше.
5. Заключение
В этом уроке мы проиллюстрировали, что объекты в Java медленнее и оказывают большее влияние на память, чем их примитивные аналоги.
Как всегда, фрагменты кода можно найти в нашем репозитории на GitHub.