Сколько памяти будут занимать примитивные типы в классе
Примитивные типы в Java — важная, часто используемая часть языка, которая требует особого обращения. В этой публикации попробуем ответить на все вопросы связанные с примитивными типами, механизмом их работы, обработкой. Статья предназначена как для новичков, которые только стали на тернистый путь изучения Java (к ним отношусь и я), так и для людей, достаточно знакомым с языком, которые смогут освежить знания и, надеюсь, найдут что-то интересное для себя.
Примитивные типы
Примитивные типы немного нарушают объектную ориентированность языка Java, так так представляют одиночные (простые) значения. Эта особенность объясняется желанием обеспечить максимальную эффективность. Создавать объект простой переменной с помощью new недостаточно эффективно, так как new перемещает объект в кучу. Вместо этого создается «автоматическая» переменная, которая не является ссылкой на объект. Переменная хранит единственное значение и располагается в стеке. Стек — это область хранения данных, расположена в RAM. Процессор имеет прямой доступ до этой области через указатель на стек, поэтому стек — очень быстрый и эффективный способ хранения данных. По скорости стек уступает только регистрам (логично, так как регистры расположены внутри процессора).
Все размеры примитивных типов строго фиксированы и не зависят от машинной архитектуры. Это одна с причин улучшенной переносимости Java-программ.
В Java определено восемь примитивных типов, которые можно разбить на четыре группы:
Целые числа | Числа с плавающей точкой | Символы | Логические значения |
---|---|---|---|
byte, short, int, long | float, double | char | boolean |
Целые числа
Для целых чисел определены четыре примитивных типа: byte, short, int, long. Все эти типы представляют целочисленные значения со знаком: положительные или отрицательные. В Java нет положительных целочисленных значений без знака (unsigned). Как было сказано раньше, все размеры примитивных типов фиксированы:
Тип | Длина в байтах | Длина в битах | Диапазон |
---|---|---|---|
byte | 1 | 8 | [-128, 127] или [-2 7 , 2 7 -1] |
short | 2 | 16 | [-32768, 32767] или [-2 15 , 2 15 -1] |
int | 4 | 32 | [-2147483648, 2147483647] или [-2 31 , 2 31 -1] |
long | 8 | 64 | [-9223372036854775808, 9223372036854775807] или [-2 63 , 2 63 -1] |
Числа с плавающей точкой
Числа с плавающей точкой (или действительные числа) представлены типами float и double. Используются для хранения значений с точностью до определенного знака после десятичной точки.
Тип | Длина в байтах | Длина в битах | Диапазон |
---|---|---|---|
float | 4 | 32 | [1.4e -45 , 3.4028235e 38 ] |
double | 8 | 64 | [4.9e -324 , 1.7976931348623157 308 ] |
Символы
В спецификации примитивный тип char принадлежит к целочисленным типам (или integral types), но поскольку он играет немного другую роль, можно выделить для него собственную категорию. Его роль — представлять символы Unicode. Для хранения символов требуется 16 бит. Странно, ведь для представления символов основных языков (например, английского, французского, испанского) достаточно 8 бит. Но такая цена интернационализации. Unicode использует полный набор международных символов на всех известных языках мира.
Тип | Длина в байтах | Длина в битах | Диапазон |
---|---|---|---|
char | 2 | 16 | ['\u0000', '\uffff'] или [0, 65535] |
Логические азначения
Примитивный тип boolean предназначен для хранения логических значений. Данный тип может принимать одно из двух возможных значений: true (истина) или false (ложь). Значения boolean возвращаются со всех логических операций (например, операции сравнения). Является обязательным при построении циклов, операторов (например, for, if).
Литералы
Значения примитивных типов данных в большинстве случаев инициализируются с помощью литералов. Рассмотрим их.
Целочисленные литералы
Наиболее часто используемые литералы. Любое целочисленное значение является числовым литералом (например, -10, 10 — десятичные значения). Можно использовать восьмеричные, шестнадцатеричные и двоичные литералы:
Все целочисленные литералы представляют значения int. Если значение литерала лежит в диапазоне byte, short или char, то его можно присвоить переменной этого типа без приведения типов. Для создания литерала типа long, необходимо явно указать компилятору, дополнив литерал буквой 'l' или 'L':
Литералы с плавающей точкой
Существует две формы записи литеров с плавающей точкой: стандартная и экспоненциальная:
Всех литералам с плавающей точкой по-умолчанию присваивается тип double. Поэтому чтобы создать литерал типа float, нужно после литерала указать букву 'f' или 'F'. К литералам также можно добавлять букву 'd' или 'D', сообщая, что это литерал типа double, но зачем?
Можно использовать шестнадцатеричные литералы с плавающей точкой, например:
Для удобности чтения длинных литералов в 7 версии языка была добавлена возможность использовать символ '_' внутри литерала:
Символьные литералы
Символьные литералы заключаются в одинарные кавычки. Все отображаемые символы можно задавать таким способом. Если символ нельзя ввести непосредственно, используют управляющее последовательности начинающиеся с символа '\'. Хотя все эти последовательности можно заменить соответствующим Unicode кодом. Также символьный литерал можно создать используя восьмеричную ('\xxx') и шестнадцатеричную форму ('\uxxxx').
Существуют также строковые литералы. Информацию о них можно получить тут.
Логические литералы
С логическими операторами все просто. Существует только два логических литерала:
Логические литералы можно присваивать только переменным типа boolean. Также важно понимать, что false не равен 0, а true не равен 1. Преобразовать переменную типа boolean в другие примитивные типы не выйдет.
Продвижение
Когда требуемая точность промежуточного значения выходит за пределы допустимого диапазона значений любого с операндов в выражении используется автоматическое продвижение типов.
Правила продвижения хорошо демонстрирует следующая диаграмма:
Операции
Над целочисленными типами
- операторы сравнения (>, , >=, ) и равенства (==, !=)
- унарные операторы (+, -)
- мультипликативные (*, /, %) и аддитивные (+, -) операторы
- инкремент (++) и декремент (--) в префиксной и постфиксной формах
- знаковые (>>, <) и без знаковые (>>>) операторы сдвига
- побитовые операторы (~, &, ^, |)
- условный оператор (?: )
- оператор приведения типов
Над Floating-Point типами
Над числами с плавающей точкой можно проводить все те же операции, что и с целыми числами, за исключением побитовых операторов и операторов сдвига.
Над логическим типом
- операторы равенства (== и !=)
- логические операторы (!,&, |, ^)
- условные логические операторы (&&, ||)
- условный оператор (?: )
Классы-обертки
Примитивы имеют свои классы-обертки, чтобы можно было работать с ними как с объектами. То есть, для каждого примитивного типа существует, соответствующий ему ссылочный тип. Классы-обертки являются immutable (неизменяемыми): это означает, что после создания объекта его состояние — значение поля value — не может быть изменено. Классы-обертки задекларированы как final: объекты, так сказать, read-only. Также хотелось бы упомянуть, что от этих классов невозможно наследоваться. Java автоматически делает преобразования между примитивными типами и их обертками: Процесс преобразования примитивных типов в ссылочные (int->Integer) называется autoboxing (автоупаковкой), а обратный ему — unboxing (автораспаковкой). Эти классы дают возможность сохранять внутри объекта примитив, а сам объект будет вести себя как Object (ну как любой другой объект). При всём этом мы получаем большое количество разношерстных, полезных статических методов, как например — сравнение чисел, перевод символа в регистр, определение того, является ли символ буквой или числом, поиск минимального числа и т.п. Предоставляемый набор функционала зависит лишь от самой обертки. Пример собственной реализации обёртки для int: В основном пакете, java.lang, уже есть реализации классы Boolean, Byte, Short, Character, Integer, Float, Long, Double, и нам не нужно ничего городить своего, а только переиспользовать готовое. К примеру, такие классы дают нам возможность создать, скажем, List , ведь List должен содержать только объекты, чем примитивы не являются. Для преобразования значения примитивного типа есть статические методы valueOf, например, Integer.valueOf(4) вернёт объект типа Integer. Для обратного преобразования есть методы intValue(), longValue() и т. п. Компилятор вставляет вызовы valueOf и *Value самостоятельно, это и есть суть autoboxing и autounboxing. Как выглядит пример автоупаковки и автораспаковки, представленный выше, на самом деле: Подробнее про автоупаковку и автораспаковку можно почитать вот в этой статье.
Примитивы
В языке Java существует 8, оскорбленных сообществом, примитивных типов данных. Их также называют простыми. И вот какие они бывают:
Целые числа со знаком: byte , short , int , long ;
Числа с плавающей точкой: float , double ;
Логические значения: boolean .
В дальнейшем, комбинируя эти самые примитивы, мы сможем получать более сложные структуры. Но об этом нам еще рано беспокоиться. Сейчас же рассмотрим каждый из примитивов подробнее.
Тип byte
Тип short
16-разрядный тип данных в диапазоне от -2^15 до 2^15-1. Может хранить значения от -32768 до 32767. Самый редко применяемый тип данных.
Тип int
Наиболее часто употребляемый тип данных. Содержит 32 разряда и помещает числа в диапазоне от -2^31 до 2^31-1. Другими словами, может хранить значения от -2147483648 до 2147483647.
Тип long
64-разрядный целочисленный тип данных с диапазоном от -2^63 до 2^63-1. Может хранить значения от -9223372036854775808 до 9223372036854775807. Удобен при работе с большими целыми числами.
Используются при точных вычислениях, которые требуют результата с точностью до определенного знака после десятичной точки (вычисление квадратного корня, функции синуса или косинуса и прочего).
Тип float
32-разрядный тип данных с плавающей точкой. Требует в два раза меньше памяти и в некоторых процессорах выполняется быстрее, по сравнению с double. Но если значения слишком велики или слишком малы, то не обеспечивает требуемую точность вычислений. Используется когда нужно число с дробной частью, но без особой точности.
Тип double
На хранение требуется 64 бита. Рационально пользоваться double, когда нужно сохранить точность многократно повторяющихся вычислений или манипулировать большими числами.
Тип char
16-разрядный тип данных в диапазоне от 0 до 2^16. Хранит значения от 0 до 65536. Этот тип может хранить в себе полный набор международных символов на всех известных языках мира (кодировка Unicode). То есть, по сути, каждый символ представляет из себя какое-то число. А тип данных char позволяет понять, что это число является символом.
Тип boolean
Может принимать только 2 значения true или false. Употребляется в условных выражениях. К примеру 1 > 10 вернет false, а 1 < 10 - true.
На этом примитивные типы данных в Java закончились. В следующей статье мы будем объявлять переменные конкретного типа данных. Поговорим о том, что такое литералы. А еще узнаем, что такое приведение типов данных. Вообщем, следующая статья будет очень насыщенной и познавательной!
Думаю, после краткого экскурса в возможности Java и прочитывания пары десятков строк кода примеров, вам захотелось узнать, чем должен уметь оперировать каждый Java-программист. Что ж, давайте поговорим о примитивных типах, классах (в том числе нескольких основных), сравнении, передаче параметров и простых структурах в Java.
Базовые типы
- boolean;
- цыферки: byte, char, short, int, long;
- нецелые цыферки: float, double.
boolean, что логично предположить, может быть true/false, byte — -128..127 (1 байт), char — 0..65536 (2 байта), short — -32768..32767 (2 байта).
В 4 байта int можно запихать числа до 2 с небольшим миллиардов, в 8 байт long — до 9*10 18 .
Нецелые float и double состоят из 4 и 8 байт соответственно (подробнее — тут).
Как вы видите, единственный беззнаковый тип — char. В нём хранятся символы, причём сразу в Unicode.
Все эти типы передаются исключительно по значению, и никаких «ссылок на int» у вас нет и не будет, если в механизм Reflection не уползёте.
Каждый из типов чётко ограничен по размеру, что обеспечивает нам возможность одинаково использовать их на любой Java-платформе. Единственное исключение — в первых спецификациях J2ME не было нецелых чисел, но сейчас с этим всё в порядке.
В общем случае, вы можете быть уверены, что написав
* This source code was highlighted with Source Code Highlighter .
вы получите именно 200, а не что-то ещё — переменные будут автоматически приведены к нужному типу.
Есть несколько способов задавать значение целочисленным переменным, например:
byte a;
a = 1;
a = 07; //восьмеричненько
a = 0x7; //hex
a = 'a' ; //будет взят код символа
a = 1.0; //упадёт с ошибкой - так можно делать только с float/double
* This source code was highlighted with Source Code Highlighter .
Эти типы называются базовыми или примитивными, они не могут быть унаследованы, да и вообще не являются объектами в Java. Никакой информации о сущности такого типа кроме его значения мы получить не можем. Да и не надо, в общем-то.
Операции с базовыми типами
- присваивать значение через =;
- сравнивать через == и != (не-равно).
- инкремент/декремент через ++ / --;
- увеличвать/уменьшать/умножать/делить очевидными способами (деление — /);
- находить остаток от деления через %;
- (кроме float/double) сдвигать используя >> и
- (кроме float/double) применять двоичную логику через &, |, ^ (xor), ~ (не) .
Классы
- Объект умеет возвращать свой класс по команде getClass.
- Ради возможности сравнения существуют методы equals и hashCode.
- Объект может уметь клонировать себя через метод clone.
- Объект может переопределить метод toString и возвращать умную информацию о себе для дебага.
Кроме того, есть ещё метод finalize, который стоит переопределить, если перед смертью ваш объект должен «написать завещание» и набор методов для работы с потоками, которые могут усыпить/разбудить текущий поток.
Как с этим обращаться?
Итак, у нас есть набор базовых типов и класс, от которого наследуется всё, кроме этих типов. Что нам ещё нужно для счастья?
Нам нужно то, что их объединяет. Знакомьтесь — классы-обёртки.
Для того, чтобы иметь возможность оперировать с простыми числами (и boolean) как с объектами были придуманы классы-обёртки.
Из названия: Byte, Short, Integer, Long, Float, Double, Boolean, Character (единственный тип, который «переименовали»).
В новых версиях Java вы можете использовать их параллельно с примитивными типами, абсолютно прозрачно:
* This source code was highlighted with Source Code Highlighter .
В старых версиях приходилось «оборачивать» примитивные типы в обёртки, а затем «разворачивать» их оттуда. Например так:
Integer x = new Integer(1);
int r = x.intValue();
* This source code was highlighted with Source Code Highlighter .
Сравнение
Сравнение на больше-меньше есть только для чисел, и механизм его работы очевиден. С равенством интереснее.
В Java есть два способа сравнить объекты на равенство, == и метод equals.
== используется для примитивных типов. За его использование с объектами умные люди либо бьют по ушам либо всячески благодарят — для объектов == это исключительно сравнение по ссылке, т.е. объекты должны быть одним объектом, а не разными (даже если у них одинаковые поля и прочее — они всё равно не являются одним объектом).
Для остального надо использовать метод .equals.
Кроме того, метод hashCode служит (теоретически) для той же цели. Хорошим тоном считается переопределять его, если вы переопределили equals.
Дело в том, что переопределяя equals вы придумываете свои правила сравнения. Вы можете, например, учесть лишь часть полей класса, и использовать только их для сравнения.
Золотое правило сравнения:
Если после инициализации неких объектов a и b выражение a.equals(b) вернёт true, то a.hashCode() должен быть равен b.hashCode().
Почему нельзя писать == для объектов
Единственное, с чем == прокатывает — числа и строки, потому что они «кешируются». И то не все. Каждый Integer rrr = 1; это для Java одна и та же переменная, но это работает на ограниченном круге значений. Если я верно помню, значения больше 127 и меньше -128 — не кешируются.
Хотите покажу шутку в стиле «а в PHP 0 равно '0'»?
Integer a = new Integer(1);
Integer b = new Integer(1);
System. out .println(a>b);
System. out .println(a System. out .println(a==b);
* This source code was highlighted with Source Code Highlighter .
Это чудо вернёт вам 3 раза false. Потому что мы явно указали создание новых объектов с одним значением, и сравнение по ссылке вернуло false.
А в первых двух случаях произошло развёртывание, так как операция сравнения определена только для примитивных типов.
Мораль: == только для примитивных типов.
«Кстати, строки»
String a = "hi!" ;
String b = new String ( "hi!" );
* This source code was highlighted with Source Code Highlighter .
Здесь, как вы видите приведены 2 способа инициализации, второй при этом хоть и избыточен, но гарантирует что ваша строка будет «новой».
Строго говоря, любая конструкция в Java в двойных кавычках — уже новая строка, созданная и закешированная (см. ниже).
Кроме того, для строк есть операция соединения (конкатенации):
String r = «Привет » + «мир»;
Конструкторы, ссылки и значения: куда что идёт, и куда оно уходит.
Когда вы создаёте новый объект, вы начинаете его жизненный цикл.
Начинается он с вызванного вами конструктора. Да? Нет.
При первом упоминании класса вызываются по порядку все его статик-блоки вида static <. >лежащие в классе.
Статик-блоки родителей вызываются по мере их упоминания.
Затем вызываются по порядку все блоки вида <. >верхнего родителя.
Далее — вызывается конструктор этого же родителя.
Затем последние 2 шага повторяются для каждого класса иерархии.
В последнюю очередь вызываются <>-блоки и конструктор вашего класса.
Далее — ваш объект живёт до тех пор, пока на него есть ссылки.
Как только ссылки на объект теряются — он подхватывается сборщиком мусора и уничтожается. Никаких a = null чтобы «стереть ссылку» писать не нужно — java и так знает, когда вы перестали использовать объект.
Перед уничтожением вызывается метод finalize вашего милого объекта.
Кстати, иногда встречается такая ошибка: человеку в метод передают некий объект, а он присваивает ему null, думая, что таким образом он объект сотрёт из всех ссылок. Нет, этого не будет, будет уничтожена только это ссылка. Однако, если ссылка была всего одна — разумеется, в этом случае объекту придёт конец.
Как выбираются конструкторы.
В предыдущем параграфе были упомянуты конструкторы. Их у класса может быть много, у каждого — свои параметры.
По умолчанию, если вы ручками не написали ни одного, то будет создан пустой конструктор без парамертов.
Каждый из них обязан вызывать конструктор класса-родителя.
Чтобы это сделать, вы должны дописать первой строкой в конструктор super-вызов:
public HelloWorld() super();
>
super-конструктор — это конструктор родителя. Вы можете использовать любой, из тех что у него есть.
На самом деле, если вы его не напишете — за вас это сделает компилятор. А что будет если у класса-родителя нет конструктора без параметров?
В этом случае вы должны явно вызывать super-конструктор всегда, и передавать ему параметры как для конструктора класса-родителя.
Что у вас там про кеширование?
Я упоминал кеширование объектов. Немного поясню. Дело в том, что упоминание чисел/строк — рутина, которая сваливается на нас постоянно. Чтобы не хранить в памяти тысячу интеджеров с 1 в качестве значения был сделан механизм кеширования. Он гарантирует вам, что на каждую вашу еденичку или строку будет создан ровно один объект в памяти, а при автоматическом развёртывании он и будет использован. Шутка в том, что разработчики платформы ограничили количество кешируемых чисел пределами, упомянутыми выше.
«Кстати, строки», часть 2 — будьте аккуратны.
Помните, каждая строка — неизменяемый объект.
Делая так: String x = «ads» + «dsada»; вы сжираете не 8*2 а 16*2 байт. Сначала происходит создание первых двух строк, затем третьей.
Чтобы избежать этого были придуманы StringBuffer и StringBuilder. Если вам нужно строить длинные строки — используйте эти штуки. Они быстрее и менее требовательны к памяти (так как не копируют строки), а StringBuffer ещё и потоко-безопасен (зато чуть больше тормозит).
Пример:
StringBuilder hi = new StringBuilder ;
hi.append( "Привет, мир" );
hi.append( ". " );
hi.append( "Как твои дела?" );
System. out .println(hi.toString());
* This source code was highlighted with Source Code Highlighter .
Надеюсь, смог дать понять, как работать с простыми объектами. На этом мог бы и закончить про них, но вспомнил про массивы.
Массивы
Массивы в Java во многом напоминают массивы в C, но при этом они не оперируют арифметикой указателей.
Примеры массивов:
char [] asd = new char [5]; //пустой массив на 5 символов
char [] ghj = new char []< 'a' , 'n' >;//массив на 2 символа
* This source code was highlighted with Source Code Highlighter .
Из приятных бонусов, массивы имеют поле length, то есть длину массива вы знаете всегда и она фиксирована.
Базовые конструкции Java
if(любое boolean-выражение или переменная)<>else<>
for(действия до начала цикла; условие продолжения цикла; действие после каждого шага)<>
while(условие продолжения цикла)<>
do<>while(условие продолжения цикла)
switch(переменная числового или enum)
for(Тип имяПеременной: массив)<>
На последнем хотел бы остановиться подробнее. В Java так выглядит цикл for-each. Он перебирает каждый элемент массива, например так:
char [] chars = new char []< 'a' , 'n' >;
for ( char a : chars) System. out .println(a);
>
* This source code was highlighted with Source Code Highlighter .
Кроме того, for-each применим для любой коллекции, и не только для них. Об этом расскажу потом.
P.S. Чем можно нормально подсветить Java-код? Source Code Highlighter для Java, к сожалению, не прездназначен.
Литералы
- Десятеричная система: 10
- Шестнадцатеричная система: 0x1F4, начинается с 0x
- Восьмеричная система: 010, начинается с нуля.
- Двоичная система (начиная с Java7): 0b101, начинается с 0b
Приведение типов
При работе с примитивами существует такое понятие как приведение типов, одно из не очень приятных свойств C++, тем не менее приведение типов сохранено и в языке Java. Иногда мы сталкиваемся с такими ситуациями, когда нам нужно совершать взаимодействия с данными разных типов. И очень хорошо, что в некоторых ситуациях это возможно. В случае с ссылочными переменными, там свои особенности, связанные с полиморфизмом и наследованием, но сегодня мы рассматриваем простые типы и соответственно приведение простых типов. Существует преобразование с расширением и преобразование сужающее. Всё на самом деле просто. Если тип данных становится больше (допустим, был int, а стал long), то тип становится шире (из 32 бит становится 64). И в этом случае мы не рискуем потерять данные, т.к. если влезло в int, то в long влезет тем более, поэтому данное приведение мы не замечаем, так как оно осуществляется автоматически. А вот в обратную сторону преобразование требует явного указания от нас, данное приведение типа называется — сужение. Так сказать, чтобы мы сами сказали: «Да, я даю себе отчёт в этом. В случае чего — виноват сам». Чтобы потом в таком случае не говорили что «Ваша Джава плохая», когда получат внезапно -128 вместо 128 ) Мы ведь помним, что в байте 127 верхнее значение и всё что находилось выше него соответственно можно потерять. Когда мы явно превратили наш int в байт, то произошло переполнение и значение стало -128.
Integer vs int
Все мы знаем, что в java — everything is an object. Кроме, пожалуй, примитивов и ссылок на сами объекты. Давайте рассмотрим две типичных ситуации:
В этих простых строках разница просто огромна, как для JVM так и для ООП. В первом случае, все что у нас есть — это 4-х байтная переменная, которая содержит значение из стека. Во втором случае у нас есть ссылочная переменная и сам объект, на который эта переменная ссылается. Следовательно, если в первом случае мы определено знаем, что занимаемый размер равен:
Забегая вперед скажу — во втором случае количество потребляемой памяти приблизительно в 5 раз больше и зависит от JVM. А теперь давайте разберемся, почему разница настолько огромна.
Из чего же состоит объект?
- Заголовок объекта;
- Память для примитивных типов;
- Память для ссылочных типов;
- Смещение/выравнивание — по сути, это несколько неиспользуемых байт, что размещаются после данных самого объекта. Это сделано для того, чтобы адрес в памяти всегда был кратным машинному слову, для ускорения чтения из памяти + уменьшения количества бит для указателя на объект + предположительно для уменьшения фрагментации памяти. Стоит также отметить, что в java размер любого объекта кратен 8 байтам!
Структура заголовка объекта
- Маркировочное слово (mark word) — к сожалению мне так и не удалось найти назначение этой информации, подозреваю что это просто зарезервированная на будущее часть заголовка.
- Hash Code — каждый объект имеет хеш код. По умолчанию результат вызова метода Object.hashCode() вернет адрес объекта в памяти, тем не менее некоторые сборщики мусора могут перемещать объекты в памяти, но хеш код всегда остается одним и тем же, так как место в заголовке объекта как раз может быть использовано для хранения оригинального значения хеш кода.
- Garbage Collection Information — каждый java объект содержит информацию нужную для системы управления памятью. Зачастую это один или два бита-флага, но также это может быть, например, некая комбинация битов для хранения количества ссылок на объект.
- Type Information Block Pointer — содержит информацию о типе объекта. Этот блок включает информацию о таблице виртуальных методов, указатель на объект, который представляет тип и указатели на некоторые дополнительные структуры, для более эффективных вызовов интерфейсов и динамической проверки типов.
- Lock — каждый объект содержит информацию о состоянии блокировки. Это может быть указатель на объект блокировки или прямое представление блокировки.
- Array Length — если объект — массив, то заголовок расширяется 4 байтами для хранения длины массива.
Спецификация Java
Известно, что примитивные типы в Java имеют предопределенный размер, этого требует спецификация для переносимости кода. Поэтому не будем останавливаться на примитивах, так как все прекрасно описано по ссылке выше. А что же говорит спецификация для объектов? Ничего, кроме того, что у каждого объекта есть заголовок. Иными словами, размеры экземпляров Ваших классов могут отличатся от одной JVM к другой. Собственно, для простоты изложения я буду приводить примеры на 32-х разрядной Oracle HotSpot JVM. А теперь давайте разберем самые используемые классы Integer и String.
Integer и String
Итак, давайте попробуем подсчитать сколько же будет занимать объект класса Integer в нашей 32-х разрядной HotSpot JVM. Для этого нужно будет заглянуть в сам класс, нам интересны все поля, которые не объявлены как static. Из таких видим только одно — int value. Теперь исходя из информации выше получаем:
Теперь заглянем в класс строки:
И подсчитаем размер:
Ну и это еще не все… Так как строка содержит ссылку на массив символов, то, по сути, мы имеем дело с двумя разными объектами — объектом класса String и самим массивом, который хранит строку. Это, как бы, верно с точки зрения ООП, но если посмотреть на это со стороны памяти, то к полученному размеру нужно добавить и размер выделенного для символов массива. А это еще 12 байт на сам объект массива + 2 байта на каждый символ строки. Ну и, конечно же, не забываем добавлять выравнивание для кратности 8 байтам. Итого в конечном итоге простая, казалось бы, строка new String(«a») выливается в:
Важно отметить, что new String(«a») и new String(«aa») будут занимать одинаковое количество памяти. Это важно понимать. Типичный пример использования этого факта в свою пользу — поле hash в классе String. Если бы его не было, то объект строки так или иначе занимал бы 24 байта, за счет выравнивания. А так получается что для этих 4-х байтов нашлось очень достойное применение. Гениальное решение, не правда ли?
Размер ссылки
Немножко хотел бы оговорится о ссылочных переменных. В принципе, размер ссылки в JVM зависит от ее разрядности, подозреваю, что для оптимизации. Поэтому в 32-х разрядных JVM размер ссылки обычно 4 байта, а в 64-х разрядных — 8 байт. Хотя это условие и не обязательно.
Группировка полей
Зачем все это?
Иногда возникает ситуация в которой Вам необходимо прикинуть приблизительный объем памяти для хранения тех или иных объектов, например словаря, эта маленькая справка поможет быстро сориентироваться. Также, это потенциально возможный способ оптимизации, особенно в том окружении, где доступ к его настройкам не доступен.
Выводы
Тема памяти в java очень интересна и обширна, когда я начинал писать эту статью, то думал что уложусь в пару примеров с выводами. Но чем дальше и глубже копаешь, тем больше и интересней становится. Вообще, знать как выделяется память для объектов очень полезная вещь, так как поможет Вам сэкономить память, предотвратить подобные проблемы или оптимизировать вашу программу в местах, где это казалось невозможным. Конечно, места где можно использовать такие оптимизации — очень редки, но все же… Надеюсь статья была Вам интересной.
В этой статье мы не будем использовать ранее установленную IDE и JDK. Однако не беспокойтесь, ваш труд не был напрасным. Уже в следующей статье мы будем изучать переменные в Java и активно кодить в IDEA. Эта же статья является обязательным этапом. И в начале вашего обучения, вы, возможно, будете не раз к ней возвращаться.
1998 - пин-код от моей кредитки является ничем иным как числом. По-крайней мере для нас - для людей. 36,5 - температура, которую показывают все термометры в разных ТРЦ. Для нас это дробное число или число с плавающей запятой. "Java Core для самых маленьких" - а это название данной серии статей, и мы воспринимаем это как текст. Так к чему же я веду. А к тому, что Джаве (так правильно произносить, на тот случай если кто-то произносит "ява"), как и человеку, нужно понимать с чем она имеет дело. С каким типом данных предстоит работать.
Что для нас означает строгая типизация? Это значит, что все данные и каждое выражение имеет конкретный тип, который строго определен. А также то, что все операции по передаче данных будут проверяться на соответствие типов. Поэтому давайте поскорее узнаем какие типы данных представлены в Java!
Примитивные типы
Примитивные типы немного нарушают объектную ориентированность языка Java, так так представляют одиночные (простые) значения. Эта особенность объясняется желанием обеспечить максимальную эффективность. Создавать объект простой переменной с помощью new недостаточно эффективно, так как new перемещает объект в кучу. Вместо этого создается «автоматическая» переменная, которая не является ссылкой на объект. Переменная хранит единственное значение и располагается в стеке. Стек — это область хранения данных, расположена в RAM. Процессор имеет прямой доступ до этой области через указатель на стек, поэтому стек — очень быстрый и эффективный способ хранения данных. По скорости стек уступает только регистрам (логично, так как регистры расположены внутри процессора).
Все размеры примитивных типов строго фиксированы и не зависят от машинной архитектуры. Это одна с причин улучшенной переносимости Java-программ.
В Java определено восемь примитивных типов, которые можно разбить на четыре группы:
Целые числа | Числа с плавающей точкой | Символы | Логические значения |
---|---|---|---|
byte, short, int, long | float, double | char | boolean |
Целые числа
Для целых чисел определены четыре примитивных типа: byte, short, int, long. Все эти типы представляют целочисленные значения со знаком: положительные или отрицательные. В Java нет положительных целочисленных значений без знака (unsigned). Как было сказано раньше, все размеры примитивных типов фиксированы:
Тип | Длина в байтах | Длина в битах | Диапазон |
---|---|---|---|
byte | 1 | 8 | [-128, 127] или [-2 7 , 2 7 -1] |
short | 2 | 16 | [-32768, 32767] или [-2 15 , 2 15 -1] |
int | 4 | 32 | [-2147483648, 2147483647] или [-2 31 , 2 31 -1] |
long | 8 | 64 | [-9223372036854775808, 9223372036854775807] или [-2 63 , 2 63 -1] |
Числа с плавающей точкой
Числа с плавающей точкой (или действительные числа) представлены типами float и double. Используются для хранения значений с точностью до определенного знака после десятичной точки.
Тип | Длина в байтах | Длина в битах | Диапазон |
---|---|---|---|
float | 4 | 32 | [1.4e -45 , 3.4028235e 38 ] |
double | 8 | 64 | [4.9e -324 , 1.7976931348623157 308 ] |
Символы
В спецификации примитивный тип char принадлежит к целочисленным типам (или integral types), но поскольку он играет немного другую роль, можно выделить для него собственную категорию. Его роль — представлять символы Unicode. Для хранения символов требуется 16 бит. Странно, ведь для представления символов основных языков (например, английского, французского, испанского) достаточно 8 бит. Но такая цена интернационализации. Unicode использует полный набор международных символов на всех известных языках мира.
Тип | Длина в байтах | Длина в битах | Диапазон |
---|---|---|---|
char | 2 | 16 | ['\u0000', '\uffff'] или [0, 65535] |
Логические азначения
Примитивный тип boolean предназначен для хранения логических значений. Данный тип может принимать одно из двух возможных значений: true (истина) или false (ложь). Значения boolean возвращаются со всех логических операций (например, операции сравнения). Является обязательным при построении циклов, операторов (например, for, if).
short
Лимит значений из byte довольно мал. Поэтому, для следующего типа данных решили увеличить количество бит вдвое. То есть теперь не 8 бит, а 16. То есть 2 байта. Значения можно посчитать так же. 2^(16-1) = 2 ^ 15 = 32768. Значит, диапазон от -32768 до 32767. Используют его совсем редко для каких-либо специальных случаев. Как говорит нам документация языка Java: «you can use a short to save memory in large arrays».
Вот мы и добрались до самого частоиспользуемого типа. Занимает он 32 бита, или 4 байта. В общем, мы продолжаем удваивать. Диапазон значений от -2^31 до 2^31 – 1.
Преобразование
Существует три типа преобразований:
- расширяющее преобразование (widening)
- суживающее преобразование (narrowing)
- widening + narrowing (преобразование byte к char, сначала byte преобразовываем в int, а потом int — в char)
Расширяющее преобразование
Если оба типа совместимы и длина целевого типа больше длины исходного типа выполняется расширяющее преобразование (например byte преобразуется в int). Следующая таблица демонстрирует все возможные расширяющее преобразования. Курсовом помечены типы, преобразования в которые, возможно, приведут к потери данных.
byte | short, int, long, float, double |
---|---|
short | int, long, float, double |
char | int, long, float, double |
int | long, float, double |
long | float, double |
float | double (если использовать strictfp потери данных не будет) |
Суживающее преобразование
При суживающем преобразовании возможна потеря информации об общей величине числового значения, также можно потерять точность и диапазон. Все возможные суживающее преобразования показаны в таблице:
short | byte, char |
---|---|
char | byte, short |
int | byte, short, char |
long | int, byte, short, char |
float | long, int, byte, short, char |
double | float, long, int, byte, short, char |
Чтобы выполнить преобразование двух несовместимых типов необходимо воспользоваться приведением (casting). Если значения исходного целочисленного типа больше допустимого диапазона значений целевого типа, то оно будет сведено к результату деления по модулю на диапазон целевого типа. Если же значения типа с плавающей точкой приводится к значению целочисленного типа, то происходит усечение (отбрасывается дробная часть).
Java boolean
Следующий тип – булевский (логический тип). Он может принимать значения только true или false, которые являются ключевыми словами. Используется в логических операциях, таких как циклы while, и в ветвлении при помощи if, switch. Что тут можно интересного узнать? Ну, например, теоретически, нам достаточно 1 бита информации, 0 или 1, то есть true или false. Но на самом деле Boolean будет занимать больше памяти и это будет зависеть от конкретной реализации JVM. Обычно на это тратится столько же, сколько на int. Как вариант – использовать BitSet. Вот краткое описание из книги «Основы Java»: BitSet
Максимальное значение int
Продолжаем удваивать. 32 умножаем на 2 и получаем 64 бита. По традиции, это 4 * 2, то есть 8 байт. Диапазон значений от -2^63 до 2^63 – 1. Более чем достаточно. Данный тип позволяет считать большие-большие числа. Часто используется при работе со временем. Или с большими расстояниями, например. Для обозначения того, что число это long после числа ставят литерал L – Long. Пример: Хочется забежать вперёд. Далее мы будем рассматривать тот факт, что для примитивов есть соответствующие обёртки, которые дают возможность работать с примитивами как с объектами. Но есть интересная особенность. Вот пример: На том же Tutorialspoint online compiler можете проверить такой вот код: Данный код работает без ошибок, всё хорошо. Но стоит в методе printLong заменить тип с long на Long (т.е. тип становится не примитивным, а объектным), как становится джаве непонятно, какой параметр мы передаём. Она начинает считать, что передаётся int и будет ошибка. Поэтому, в случае с методом необходимо будет явно указывать 4L. Очень часто long используется как ID при работе с базами данных.
Java char
- Таблица Unicode символов
- Таблица символов ASCII
Пример в студию: Кстати, char, являясь по своей сути всё таки числом, поддерживает математические действия, такие как сумма. А иногда это может привести к забавным последствиям: Настоятельно советую проверить в онлайн IDE от tutorialspoint. Когда я увидел этот пазлер на одной из конференций мне это подняло настроение. Надеюсь, Вам пример тоже понравится) UPDATED: Это было на Joker 2017, доклад: "Java Puzzlers NG S03 — Откуда вы все лезете-то?!".
Java float и Java double
Классы-обертки
Для представления примитивных типов как объектов было сделаны классы-обертки (wrapper classes). Какие преимущества дают нам классы-обертки?
Читайте также: