Про примитивы в Java, их жирненькие обертки и не только
Примитивы и их обертки вроде как очевидны.
Примитивы и обертки являются базовыми элементами в Java. Тем не менее многие, даже опытные специалисты, имеют пробелы в этой теме. В этой статье мы рассмотрим самые важные аспекты примитивов и их оберток.
Примитивы в Java включают в себя 8 типов, из которых 6 относятся к численным.
Целочисленные:
BYTE: тип с диапазоном от -128 до 127
SHORT: тип с диапазоном -32768 до 32767
INT: один из самых популярных типов, диапазон от -2^31 до 2^31-1
LONG: тип с диапазоном значений от -2^63 до 2^63-1
Числа с плавающей точкой.
Куда хитрее стоит дело с числами с плавающей точкой. Тут пользователю дают 32 бита (float) или 64 бита (double), и он может тратить их на целую часть или на часть после плавающей точки. Оценить масштабность этих числе мы можем следующим образом:
Минималный "шаг" между двумя такими числами будет авен:
Float.MIN_VALUE ~ 1.40239846e-45
Double.MIN_VALUE ~ 4.9e-324
Максимальное число, если после запятой не будет дробных частей (те мы словно станем целочисленной переменной):
Float.MAX_VALUE ~ 3.4e+38
Double.MAX_VALUE ~ 1.79e+308
Резюмируя: чем более точно наше число (то есть, чем больше чисел после плавающей точки), тем меньше его абсолютное значение. Поскольку числа "до" запятой, то есть целочисленная часть, уменьшаются, увеличивается точность представления дробной части.
Булевы и Символные примитивы.
Ну и на последок - логические и символьные типы:
boolean - имеет значение true/false
char - символьный примитив может представлять максимум 65536 символов.
Что нужно знать о примитивах?
Основные характеристики примитивов:
Быстры в работе
Легковестны
Переполнение нам не друг. В крайнем случае, мы спасаемся через BigInteger/BigDecimal.
Но нужно помнить, что примитивы (и их обертки, кстати) могут легко выйти за свои границы. Например, предположим, у нас есть код, в котором происходит умножение типа на самого себя, тогда:
Если умножить два Short, каждый из которых равен 200, то мы выйдем за границу 32768, и вместо 40000 мы получим -25536.
Если мы умножим два Integer (которые могут покрывать 2 миллиарда), каждый из которых равен 50000, то мы получим -1794967296 вместо 2.5 миллиарда.
Вывод - заранее подумай о границах значений. Если данные не влазят ни в один из типов то BigDecimal или BigInteger придут на помощь.
Обертки примитивов. Вроде и жирненькие но необходимые.
Для каждого примитива в Java существует его обертка. Например:
int обернут в Integer
double в Double
и так далее для каждого из 8 типов
Основная необходимость наличия оберток - использование их в коллекциях. Коллекции в Java, по дизайну, работают только с объектами. Такой код даже не скомпилируется:
Что дает класс обертка? Посмотрим на примере Integer
Классы-обертки в Java, такие как Integer, предоставляют ряд преимуществ и дополнительных функциональностей:
Конвертация типов: Методы, такие как Integer.valueOf(), предоставляют возможность конвертировать значения одного типа в другой.
Autoboxing/Unboxing: Обертки поддерживают автоматическую упаковку (autoboxing) и распаковку (unboxing) значений, обеспечивая плавную конвертацию между примитивами и объектами.
Реализация equals/hashcode: Классы-обертки переопределяют методы equals() и hashCode(), что позволяет использовать их в качестве ключей в коллекциях, таких как HashMap, где хранение и поиск элементов основаны на хэш-функции.
Имплементация Comparable: Классы-обертки реализуют интерфейс Comparable, что делает их подходящими для использования в упорядоченных коллекциях, таких как TreeMap.
Дополнительные методы: Классы-обертки предоставляют различные вспомогательные методы, такие как min(), max(), bitCount() и другие.
И так один из самых важных моментов - обертки работают из коробки для коллекций. Но есть ли минусы у них?
Недостатки оберток. Жирнота, медлительность.
Обертки инкапсулируют в себя примитивы это мы уже знаем. И как следствие мы:
Тратим память на саму обертку, а учитывая минимальный размер слова в 64-битных операционных системах (32-битные не берем в расчет, они уходят в прошлое) - обертка просто непростительно разрастается.
Время на доступ к примитиву возрастает, так как добавляется лишний прыжок по ссылке обертки.
Время на autoboxing/unboxing
Давайте оценим размер примитива и размер обертки для 64х битных систем:
Итак, потеря в памяти довольно высока. Минимум, мы теряем от 3 до 16 раз. И, возможно, здесь стоит отметить, что, на практике, большинство программ не постоянно хранят большие объемы данных, и в большинстве случаев созданные объекты удаляются. Кроме того, современная память относительно дешева.
Что делать, если размер памяти критичен?
Однако, если вы храните большие объемы числовых данных и эффективное использование памяти критично, у вас есть два варианта:
Производительность.
Вопрос о скорости работы коллекций по сравнению с массивами довольно тонкий. Однозначно можно сказать, что коллекции обычно медленнее. Вот тут есть несколько бенчмарков. Проседание в среднем не так заметно. Но вот та гибкость и возможности которые дают коллекции перевешивают их недостатки (хотя если вы пишите что нибудь low latency то с коллекциями вам, наверно не по пути).
Надеюсь вам понравился разбор. Больше материалов по джаве и смежным технологиям вы можете найти в моем канале. Спасибо за внимание.



