Привет! Во время прохождения JavaRush ты не раз сталкивался с примитивными типами.
Вот краткий список того, что мы о них знаем:
Но, помимо значений, типы отличаются еще и размером в памяти.
Для типа
- Они не являются объектами и представляют собой значение, хранящееся в памяти
- Примитивные типы бывают нескольких видов:
- Целые числа —
byte
,short
,int
,long
- Числа с плавающей точкой (дробные) —
float
иdouble
- Логический —
boolean
- Символьный (для обозначения букв и цифр) —
char
- Целые числа —
- Каждый из них имеет свой диапазон значений:
Примитивный тип | Размер в памяти | Диапазон значений |
---|---|---|
byte | 8 бит | от -128 до 127 |
short | 16 бит | до -32768 до 32767 |
char | 16 бит | от 0 до 65536 |
int | 32 бита | от -2147483648 до 2147483647 |
long | 64 бита | от -9223372036854775808 до 9223372036854775807 |
float | 32 бита | от (2 в степени -149) до ((2-2 в степени -23)*2 в степени 127) |
double | 64 бита | от (-2 в степени 63) до ((2 в степени 63) - 1) |
boolean | 8 (при использовании в массивах), 32 (при использовании не в массивах) | true или false |
int
занимает больше, чем byte
.
А long
— больше, чем short
.
Объем занимаемой примитивами памяти можно сравнить с матрешками:
Внутри матрешки есть свободное место. Чем больше матрешка — тем больше места.
Внутрь большой матрешки long
мы легко можем положить меньшую по размеру int
. Она легко уместится, и ничего делать дополнительно не нужно.
В Java при работе с примитивами это называется автоматическим преобразованием. По-другому его называют расширением.
Вот простой пример расширения:
public class Main {
public static void main(String[] args) {
int bigNumber = 10000000;
byte littleNumber = 16;
bigNumber = littleNumber;
System.out.println(bigNumber);
}
}
Здесь мы присваиваем значение byte
в переменную int
. Присваивание прошло успешно и безо всяких проблем: значение, хранящееся в byte
, занимает меньший объем в памяти, чем “влезает” в int
.
“Маленькая матрешка” (значение byte
) легко влезает в “большую матрешку” (переменную int
).
Другое дело, когда ты пытаешься сделать наоборот — положить значение большого размера в переменную, которая на такие размеры не рассчитана.
С настоящими матрешками такой номер в принципе не пройдет, а в Java — пройдет, но с нюансами.
Давай попробуем положить значение int
в переменную short
:
public static void main(String[] args) {
int bigNumber = 10000000;
short littleNumber = 1000;
littleNumber = bigNumber;//ошибка!
System.out.println(bigNumber);
}
Ошибка!
Компилятор понимает, что ты пытаешься сделать что-то нестандартное, и засунуть большую матрешку (int
) внутрь маленькой (short
).
Ошибка компиляции в данном случае — предупреждение от компилятора: “Эй, ты точно уверен, что хочешь это сделать?”
Если ты уверен, говоришь об этом компилятору: “Все ок, я знаю, что делаю!”
Этот процесс называется явным преобразованием типов, или сужением.
Чтобы сделать сужение, тебе необходимо явно указать тип, к которому ты хочешь привести свое значение.
Иными словами, ответить компилятору на его вопрос: “Ну и в какую из этих маленьких матрешек ты хочешь засунуть эту большую матрешку?”
В нашем случае это будет выглядеть так:
public static void main(String[] args) {
int bigNumber = 10000000;
short littleNumber = 1000;
littleNumber = (short) bigNumber;
System.out.println(littleNumber);
}
Мы явно указали, что хотим уместить значение int
в переменную short
и берем ответственность на себя. Компилятор, видя явное указание на более узкий тип, проводит преобразование.
Каков же будет результат?
Вывод в консоль:
-27008
Немного неожиданно. Почему именно такой? На самом деле все просто.
У нас было изначальное значение — 10000000
Оно хранилось в переменной int
, которая занимала 32 бита, и в двоичной форме оно выглядело так:
Мы записываем это значение в переменную short
, но она может хранить только 16 бит!
Соответственно, только первые 16 бит нашего числа и будут туда перемещены, остальные — отбросятся.
В итоге в переменную short
попадет значение
,
которое в десятичной форме как раз равно -27008
Именно поэтому компилятор “просил подтверждения” в форме явного приведения к конкретному типу. Во-первых, оно показывает, что ты берешь ответственность за результат на себя, а во-вторых, указывает компилятору сколько места выделить при приведении типов. Ведь если бы мы в последнем примере приводили int
к типу byte
, а не к short
, в нашем распоряжении было бы только 8 бит, а не 16, и результат был бы уже другим.
Для дробных типов (float
и double
) сужение происходит по-своему. Если попытаться привести такое число к целочисленному типу, у него будет отброшена дробная часть.
public static void main(String[] args) {
double d = 2.7;
long x = (int) d;
System.out.println(x);
}
Вывод в консоль:
2
Тип данных char
Ты уже знаешь, что тип char используется для отображения отдельных символов.public static void main(String[] args) {
char c = '!';
char z = 'z';
char i = '8';
}
Но у него есть ряд особенностей, которые важно понимать.
Давай еще раз посмотрим в таблицу с диапазонами значений:
Примитивный тип | Размер в памяти | Диапазон значений |
---|---|---|
byte | 8 бит | от -128 до 127 |
short | 16 бит | от -32768 до 32767 |
char | 16 бит | от 0 до 65536 |
int | 32 бита | от -2147483648 до 2147483647 |
long | 64 бита | от -9223372036854775808 до 9223372036854775807 |
float | 32 бита | от (2 в степени -149) до ((2-2 в степени -23)*2 в степени 127) |
double | 64 бита | от (-2 в степени 63) до ((2 в степени 63)-1) |
boolean | 8 (при использовании в массивах), 32 (при использовании не в массивах) | true или false |
char
указан числовой диапазон — от 0 до 65536.
Но что это значит? Ведь char
— это не только цифры, но и буквы, знаки препинания…
Дело в том, что значения char
хранятся в Java в формате Юникода.
Мы уже сталкивались с Юникодом в одной из прошлых лекций. Ты, наверное, помнишь, что Unicode — это стандарт кодирования символов, включающий в себя знаки почти всех письменных языков мира.
Иными словами, это список специальных кодов, в котором найдется код почти для любого символа из любого языка.
Общая таблица Юникодов очень большая, и, конечно, ее не нужно учить наизусть.
Вот, например, ее кусочек:
Главное — понимать принцип хранения значений char
, и помнить, что зная код конкретного символа всегда можно получить его в программе.
Давай попробуем это сделать с каким-нибудь случайным числом:
public static void main(String[] args) {
int x = 32816;
char c = (char) x ;
System.out.println(c);
}
Вывод в консоль:
耰
Именно в таком формате в Java хранятся символы char
. Каждому символу соответствует число — числовой код размером 16 бит, или два байта. Юникоду 32816 соответствует иероглиф 耰.
Обрати внимание вот на какой момент.
В этом примере мы использовали переменную int
. Она занимает в памяти 32 бита, в то время как char
— 16.
Здесь мы выбрали int
, потому что нужное нам число 32816 находится за пределами диапазона short
. Хотя размер char
, как и short, равен 16 битам, но в диапазоне char
нет отрицательных чисел, поэтому “положительный” диапазон char
в два раза больше (65536 вместо 32767 у short
).
Мы можем использовать int
, пока наш код укладывается в диапазон до 65536.
Но если создать число int >65536
, оно будет занимать больше 16 битов.
И при сужении типов:
char c = (char) x;
лишние биты будут отброшены, и результат будет весьма неожиданным.
Особенности сложения char и целых чисел
Давай рассмотрим вот такой необычный пример:public class Main {
public static void main(String[] args) {
char c = '1';
int i = 1;
System.out.println(i+c);
}
}
Вывод в консоль:
50
O_О
Где логика?
1+1, откуда взялось 50?!
Ты уже знаешь, что значения char
хранятся в памяти как числа в диапазоне от 0 до 65536, обозначающие Юникод нашего символа.
Так вот.
Когда мы производим сложение char
и какого-то целочисленного типа, char
преобразуется к числу, которое соответствует ему в Юникоде.
Когда в нашем коде мы складывали 1 и ‘1’ — символ ‘1’ преобразовался к своему коду, который равен 49 (можешь проверить в таблице выше).
Поэтому результат и стал равен 50.
Давай еще раз возьмем для примера нашего старого друга — 耰, и попробуем сложить его с каким-нибудь числом.
public static void main(String[] args) {
char c = '耰';
int x = 200;
System.out.println(c + x);
}
Вывод в консоль:
33016
Мы уже выяснили, что 耰 соответствует коду 32816. А при сложении этого числа и 200 мы получаем как раз наш результат — 33016 :)
Механизм работы, как видишь, достаточно простой.