logo
Ещё

Типы данных в Java

У начинающих программистов, не знакомых с C или C++, типы данных в Java нередко вызывают трудности, потому что виды данных в Java организованы, мягко говоря, не очень удобно: есть примитивы и ссылки, примитивы могут конвертироваться в ссылочные типы и обратно, нужно соблюдать статическую типизацию, легко выстрелить себе в ногу неявным преобразованием и так далее. Ниже мы рассмотрим каждый тип данных и постараемся структурировать информацию про типы данных так, чтобы вам было легко ее запомнить.


Типы данных в Java

В процессе программирования на Java вам всегда нужно указывать тип данных объявляемой переменной – так работает статическая типизация, вы не можете положить в переменную с типом Boolean число «53». И эти типы делятся на 2 больших лагеря: примитивные типы и ссылки. 

Примитивные типы – это что-то простое, состоящее из одной сущности. Все числа – это примитивные типы, одиночная буква – тоже примитив, true/false – примитив. Когда программирование только зарождалось, примитивных типов было более чем достаточно, поскольку компьютеры использовались для расчетов. Тогда же и выработался очень простой метод хранения примитивов – всех их стали хранить в стеке оперативной памяти. Стек – это как обойма, вы объявили переменную x – в оперативную память по адресу 0 положили переменную x, после этого вы объявили переменную y – в оперативную память по адресу 1 положили переменную y.

При таком формате хранения переменные очень легко искать в памяти: нужно просто запомнить, на каком расстоянии от нулевой ячейки стека лежит искомая переменная.

Но со временем все чаще начала возникать ситуация, когда нужно объединить несколько примитивных значений одного типа в какую-то одну структуру данных: несколько символов объединить в строку символов; объединить данные о дате рождения 10 человек; … Решение нашли довольно простое: 

  1. Заводим несколько примитивных переменных.
  2. Кладем их последовательно.
  3. Записываем адрес первой примитивной переменной.
  4. Записываем длину всего стека структуры данных.

Так работают массивы в C и C++. При создании массива вы получаете адрес первого элемента массива (ссылку на этот элемент), когда вы обращаетесь, например, ко второму элементу – вам нужно взять ссылку, прибавить к ней длину первого элемента в байтах – и вы получите адрес второго элемента в памяти, потому что элементы расположены один за другим. Но массивы не решают проблему хранения разных типов данных – в них можно засовывать только значения одного примитивного типа.

Проблему разных типов данных решили «в лоб»: в массиве просто указывается примитивный тип данных для разных ячеек. Эта реализация работает хорошо, но теперь для хранения такого массива стек не подходит – это как если бы мы пытались запихнуть в обойму патроны разного калибра. Выходом стала новая область памяти – куча (heap). Название очень хорошо описывает этот тип хранения данных, потому что куча – это буквально свалка данных. Конечно, компилятор и сборщик мусора ее оптимизируют так, чтобы в кучу влезло как можно больше данных, но сути это не меняет – никакой четкой организации в куче нет. Главный плюс кучи – ввиду отсутствия организации в кучу можно положить объект (набор примитивных данных) какой угодно длины и структуры (правда, быстродействие снижается по сравнению со стеком). Для того, чтобы пользоваться объектом, находящимся в куче, вам нужно иметь на него ссылку – то есть ссылку на его физическое расположение в памяти.

Итак, ликбез по основам Computer Science закончен, переходим к практике. В рамках платформы Java 2 типа данных: примитивы (значения в стеке) и ссылки (ссылки на объекты в куче). Примитивы:

  • byte/short/int (integer)/long: целочисленный тип данных, различие – в минимальном/максимальном значении, которое может вместить переменная.
  • char: char используется для хранения одного символа (буква, знак или еще что-нибудь). По факту представляет собой целое число в 2 байта, которое конвертируется в символ по Unicode или ASCII.
  • float/double: число с плавающей запятой. Очень проблемный тип, о котором мы поговорим в отдельном разделе.
  • boolean: true или false. Незаменимый тип для проверки истинности.

По размерам целочисленных типов:

  • byte занимает 1 байт, допустимые значения: от -127 до 128.
  • short занимает 2 байта, допустимые значения: от -32_768 до 32_767
  • int занимает 4 байта, допустимые значения: от -2_147_483_648 до 2_147_483_647.
  • long занимает 8 байта, допустимые значения: от -9_223_372_036_854_775_808 до 9_223_372_036_854_775_807.

Типы объявляются в момент создания переменных, в случае с long нужно поставить L в конце литерала:

long testLong = 897204352096L

При задании литерала (значения) для char нужно обязательно помещать литерал в одинарные кавычки:

char testChar = ‘a’

В отличие от C, boolean не является надстройкой над byte, а представляет собой полноценный отдельный тип данных. Имеет только 2 значения – true или false. С помощью boolean можно управлять операторами ветвления:

if (5 > 4) { … }
Результат оператора сравнения – true, и блок if исполняется.

Что касается ссылочных типов данных, то нужно разделять ссылку и объект. Ссылка на объект String (строку) объявляется так:

String testString;
Как видите, при объявлении нужно указать тип объекта и имя ссылки, как и с примитивами. Но если с примитивами справа от знака «=» мы просто указываем литерал (значение), то ссылке нужно назначить объект, а объект сначала нужно создать в куче:
new String();
Полная версия, объявляем ссылку и сразу назначаем ей новый объект из кучи:
String testString = new String();
Для продвинутых: объявлять строку таким способом не имеет смысла, потому что строки в Java – неизменяемые, и вы просто создадите пустую строку, которая будет занимать место, пока не будет уничтожена. Лучше сразу создавать строку, которой вы будете пользоваться:
String testString = “Hello world!”; System.out.println(testString);

Значения переменных по умолчанию

Отдельной болью в C была инициализация переменных по умолчанию – ее просто не было, и если вы забыли проинициализировать int нулем, то в переменной могло оказаться любое число, записанное в этой области памяти ранее (кстати, на этом принципе основана одна очень сложная хакерская атака, Buffer Overflow). В Java все куда проще – переменные, описанные вне метода (то есть поля класса) автоматически инициализируются нулевыми значениями, а переменные методов нужно инициализировать вручную, иначе компилятор Java SE выдаст ошибку. Стандартные значения:

  • 0 для целочисленных типов;
  • 0.0 для чисел с плавающей точкой;
  • ‘\u0000’ для char;
  • false для boolean.

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

Использование целочисленных переменных

Самый частый в использовании тип – int, и мы рекомендуем использовать именно его, если у вас нет четкой уверенности, что нужно использовать другой тип. Long нужен для действительно больших чисел, byte и short – для оптимизации нагрузки на оперативную память. Если вы не пишете приложение для микроконтроллера – используйте int. При необходимости вы потом сможете заменить его на другой целочисленный тип, если в этом появится нужда.

Числа с плавающей точкой

Числа с плавающей точкой – это float и double. Они хранят числа в виде значащей части, мантиссы и экспоненты. Например, 1 250 000 = 1.25e6, то есть 1.25, умноженное на 10 в степени 6. Значащая часть – 1, мантисса – 25, экспонента – 6. Числа с плавающей точкой могут вмещать в себя очень большие значения (до 2 в степени 63 для double), но страдает точность – мантисса обрезается и округляется, если ее длина превышает определенное значение (25 цифр для double). Поэтому:

  • всегда используйте double для чисел с плавающей точкой – double более вместителен, чем float, поскольку имеет размер в 2 раза больше;
  • будьте крайне аккуратны при сравнении двух числе с плавающей точкой – сравнение вида
    if (firstDouble == secondDouble) {}
    может вернуть false при одинаковых присвоенных значениях переменных, потому что округление сработает неправильно;
  • не используйте числа с плавающей точкой для программ, в которых нужно считать деньги или производить другие точные расчеты – для работы в этом случае есть специальные математические классы с фиксированной точкой и длинной арифметикой.

Логический и символьный типы данных

Про них особо рассказывать нечего – boolean абсолютно прост и предсказуем, а char умеет хранить 1 символ из Unicode. Если вам любопытно, то на самом деле char – это short (целочисленный тип объемом в 2 байта), который компилятор воспринимает по особенному – тип char сигнализирует о том, что этот short нужно воспринимать как символ из Юникода.

Значения по умолчанию для ссылочных типов данных

Всегда null. Из-за этого самое распространенное исключение в Java, которое вы будете встречать – NullPointerException, оно показывает, что где-то есть ссылка с null, и с этой ссылкой что-то пытались делать (вызвать метод, например).

Boxing и Unboxing

Чтобы объяснить Boxing и Unboxing, нужно сначала объяснить приведение типов. Поскольку Java – это язык со статической типизацией, тип переменной должен совпадать с типом данных. И вправду,

boolean testBool = 5;
не имеет смысла, потому что boolean может быть true или false, но никак не 5. А что делать, если типы данных относятся к одной категории? Например:
byte b = 5; int i = b;
И byte, и int – целочисленные типы, которые могут хранить 5, при этом типы все же разные.

Чтобы решать такие вопросы, в Java есть 2 вида приведения типов: явное и неявное. Явное – это когда вы прямо говорите компилятору, что нужно привести тип к какому-то другому:

int i = 5; byte b = (byte) i;
Здесь мы говорим, что нужно int конвертировать в byte, если мы уберем (byte) – компилятор выдаст ошибку. Неявное приведение – это когда компилятор делает все сам за нас:
byte i = 5; int b = i;
Как видите, мы не писали никаких дополнительных указаний, но Java сама сконвертировала byte в int.

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

byte -> (char <-> short) -> int -> long -> float -> double
Если мы приводим byte к double – все окей, если приводим int к byte – это надо сделать явно. Совет начинающим: всегда используйте int и более объемные типы, избегайте сужающего приведения (из int в byte, к примеру). Сужающие приведения могут генерировать трудновычисляемые баги.

Приведение работает не только для примитивных типов, но и для ссылок. Описывать логику – долго, потому что придется захватывать темы наследования и полиморфизма, что выходит далеко за рамки этого материала. Но общая логика такова: ссылочный тип можно неявно приводить к родительскому типу (классу) или явно приводить к дочернему типу (классу).

А теперь – про упаковку и распаковку. В Java есть ряд встроенных библиотек, которые наотрез отказываются работать с примитивными типами, как пример – List. Чтобы обойти эту проблему, разработчики языка для каждого примитивного типа ввели его объектный (ссылочный) аналог. Если тип называется double, то класс называется Double и так далее, исключения: int = Integer, char = Character. Таким образом вы можете передавать в методы объекта List обернутые в объект (ящик) примитивы. Чтобы упростить жизнь разработчикам, создатели языка ввели процедуру упаковки и распаковки – когда вы, например, даете объекту List примитив int, Java сама оборачивает int в Integer (boxing), а при возврате значения распаковывает Integer в int (unboxing).

Простое правило: не используйте методы примитивов, если не уверены, что вам это нужно. Объект класса Byte, например, создается довольно муторно:

Byte x = new Byte((byte) 2);
Как вы можете заметить, приходится приводить литерал к byte, чтобы создать Byte (по умолчанию все литералы в Java – int). Доверьтесь автоматической упаковке и распаковке.

Курс «Инженер по тестированию» от Нетология

Школа

Нетология

Стоимость

98 600 руб

Цена в рассрочку

2 883 руб/мес

Длительность курса

8 месяцев

Программа трудоустройства

Есть

Формат

Запись лекций, Онлайн занятия с преподавателем

Курс «Java-разработчик» от Skillfactory

Школа

Skillfactory

Стоимость

131 235 руб

Цена в рассрочку

4 050 руб/мес

Длительность курса

14 месяцев

Программа трудоустройства

Есть

Формат

Запись лекций, Онлайн занятия с преподавателем

Курс «Инженер по тестированию» от Skillbox

Школа

Skillbox

Стоимость

96 439 руб

Цена в рассрочку

4 384 руб/мес

Длительность курса

10 месяцев

Программа трудоустройства

Есть

Формат

Запись лекций

Что почитать по теме

FAQ

Что такое «Передача по значению» и «Передача по ссылке»?

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

Можно ли хранить в char целочисленные значения?

Технически это возможно, но делать так нельзя. Используйте short или int.

Подведем итоги

Тезисно:

  • В Java есть примитивные типы данных (хранятся в стеке) и ссылочные типы (хранятся в куче).
  • Примитивы – это всегда единичные числа, просто в разных вариациях.
  • Ссылочные типы данных – это группы значений (примитивов или других ссылок).
  • Для полей класса переменные инициализируются нулевыми значениями автоматически, для переменных методов инициализацию нужно производить вручную.
  • Избегайте ручной упаковки/распаковки, использования чисел с плавающей точкой для точных вычислений и сужающего приведения.
Часто ищут