Перейти к основному содержимому

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

· 6 мин. чтения

Задача: Наибольшая подстрока палиндром

Для заданной строки s, верните наибольшую подстроку палиндром входящую в s. Подстрока — это непрерывная непустая последовательность символов внутри строки. Стока является палиндромом, если она читается одинаково в обоих направлениях...

ANDROMEDA 42

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. Объем памяти для массивов

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

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

./293ebc800a82f2a990da7d3c56334741.gif

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

./89c2197e9a6b0729373e6d60836ec8ea.gif

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

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

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

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

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

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

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

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

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

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

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

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

5. Вывод

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

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