Привет! Во время прохождения 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
.
Объем занимаемой примитивами памяти можно сравнить с матрешками:
![Расширение и сужение примитивных типов - 2]()
Внутри матрешки есть свободное место. Чем больше матрешка — тем больше места.
Внутрь большой матрешки
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 бита, и в двоичной форме оно выглядело так:
![Расширение и сужение примитивных типов - 3]()
Мы записываем это значение в переменную
short
, но она может хранить только 16 бит!
Соответственно, только первые 16 бит нашего числа и будут туда перемещены, остальные — отбросятся.
В итоге в переменную
short
попадет значение
![Расширение и сужение примитивных типов - 4]()
,
которое в десятичной форме как раз равно -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 — это стандарт кодирования символов, включающий в себя знаки почти всех письменных языков мира.
Иными словами, это список специальных кодов, в котором найдется код почти для любого символа из любого языка.
Общая таблица Юникодов очень большая, и, конечно, ее не нужно учить наизусть.
Вот, например, ее кусочек:
![Расширение и сужение примитивных типов - 5]()
Главное — понимать принцип хранения значений
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, обозначающие Юникод нашего символа.
![Расширение и сужение примитивных типов - 6]()
Так вот.
Когда мы производим сложение
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 :)
Механизм работы, как видишь, достаточно простой.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
0000 0000 = 0 0000 1001 = 9 0111 1111 = 127
Обрати внимание на старший бит. Он во всех положительных числах равен нулю. И, соответственно, для отрицательных чисел остаются только те, в которых этот старший бит равен 1. Не буду утомлять особенностями хранения таких значений, скажу лишь, что для того, чтобы изменить знак числа нужно проинвертировать все биты и прибавить 1. То есть:0000 1001 = 9 // инвертируем 1111 0110 = -10 // прибавим 1 1111 0111 = -9 // и повторим в обратном порядке, инвертируем снова 0000 1000 = 8 // прибавим 1 0000 1001 = 9
Теперь можешь на бумаге ещё раз пересчитать и убедиться, что данное число при хранении в знаковом типе будет -27008, а вот если сделать через char, который не умеет хранить отрицательные числа, ты увидишь именно то, что насчитал на листе:System.out.println((short)10000000); // -27008 System.out.println((int)(char)10000000); // 38528
0000 00010000 0000 Остаётся: 0000 0000 Т.е. ноль. Можешь проверить всё это в редакторе кода. Это по вопросу отбрасывания левой части.int
будет 32768 без минуса. если преобразовать кshort
10030000 то будет чуть меньше 3000 в общем.