Блок 1. Раздел 1. Тема 5

Типы данных в Java
Приведение типов
Коварное деление нацело

Мы уже знаем несколько типов данных - int, double, char. Есть еще и другие типы, созданные под конкретные цели. Выражение одного типа можно "приводить" к выражению другого типа. Например, приводя дробные к целым, мы отбрасываем дробную часть. Java также не учитывает дробную часть, когда делит одно целое число на другое. Если важен дробный результат, то следует пользоваться дробным типом.
В следующей таблице приведены все основные типы данных Java. Указано, сколько бит выделяется под переменную каждого из типов. Биты – это цифры двоичной системы исчисления. В привычной человеку системе десять цифр, а в двоичной – только две, это 1 и 0. Все числа в машине переводятся в двоичную систему. Если в типе данных 8 бит, то на это число отводится 8 двоичных цифр. Вы не можете записать число из девяти цифр – девятая цифра не поместится в ячейку и реально в числе сохранится не настоящий результат, а совершенно ложный - без одной из цифр. Поэтому у каждого типа данных есть максимальное и минимальное допустимое значение. Они доступны во всех справочниках, я привожу их здесь для общего понимания.

Сначала приведем таблицу типов данных для целых чисел - byte, short, char, int, long.

Если вы уверены, что вам нужны совсем небольшие числа, то можете сэкономить оперативную память и использовать byte. Если же напротив, вы делаете расчет с числами больше двух миллиардов, то вам нужен long. Поначалу вполне можно ограничиться средним вариантом - привычным int.
Дробные типы данных – float для небольших чисел и double для больших. Например, мы пишем:

float a = 2.0f; 
/* 2.0f – буква f в конце указывает, что 2.0 понимается как float,
иначе 2.0 будет воспринято как double и Java выдаст ошибку, 
потому что, может быть, указанное число не влезает в количество цифр float */
double b = 2.0;
// по умолчанию любая дробь – это double, поэтому типы double b и 2.0 совпадают,
// и присваивание пройдёт без ошибок.
Дроби часто записывают в "научной нотации", например, 15e3 означает 15 умножить на 10 в степени 3, то есть 15000. Первое число 15 называется мантисса, а второе число 3 называется порядок. Порядок может быть отрицательным, например, 15e-3 будет означать 15 поделить на десять в третьей степени, то есть 0.015 (15 тысячных). По сути порядок описывает сколько нулей нужно к числу приписать, или же где в числе поставить точку десятичной дроби. Именно в таком виде дроби хранятся в памяти компьютера.
Есть специальный тип boolean, который служит для хранения результатов логических выражений. Эти выражения мы подробно рассмотрим в следующем разделе, а пока укажу, что их результат всегда либо правда (true), либо ложь (false). То есть логическое выражение или истинно, или ложно. Поэтому в Java мы можем написать:

boolean res = true; // переменная res хранит "правду" - true
res = false; // теперь res хранит "ложь" - false

int x = 10;

res = x > 20; // 10 меньше 20, поэтому сравнение x больше 20 дает ложь
Приводим один тип к другому
Что если вы хотите переменную или число одного типа записать в переменную другого типа? Если точность наверняка не страдает, то Java разрешит вам это сделать с помощью присваивания. Если же точность может упасть, например, при отбрасывании дробной части, Java просит программиста удостоверить, что он действительно хочет преобразовать один тип в другой. Это делается так:

double a = 2.2;
// привести дробное a к целому b
int b = (int) a;
// скобки слева от имени переменной или от какого-либо числового выражения
// обозначают "привести это выражение к новому типу, указанному в скобках".
Определение. Когда перед переменной или выражением слева в скобках указывается тип, то эта переменная или выражение приводится к данному типу. Может быть понижена точность вычислений. Так программист удостоверяет, что он и в самом деле хочет совершить небезопасную операцию.
Еще пример:

// привести точное выражение double к менее точному float
double a = 2.2
float x = (float) a*10;
Изменение типа будет осуществлено только при определённых условиях. Главное правило: если точность не теряется, то преобразование типов пройдет само собой (неявно, то есть скобки с типом можно не указывать), если же точность может пострадать, например, в присваивании long к int, double к int, то нужно привести типы явно.
Если может потеряться точность, то необходимо привести типы явно
Отдельное правило в Java касается типа boolean - к нему нельзя приводить переменные других типов, а также переменные типа boolean не приводятся к другим типам. Этот тип логический, содержит всего два возможных значения - true, false, поэтому по сути не является числом. Как мы увидим в следующих разделах многие типы никак не приводятся друг к другу, если такое преобразование неадекватно и компилятору не понятно, как его проводить. Например, набор целых чисел (массив) нельзя привести просто к int, то есть как-то превратить набор чисел в одно число.

Деление нацело (int на int) -
популярный источник ошибок

Когда мы используем оператор / для целых чисел (int), то Java считает, что должно снова получиться целое число – дробная часть полностью отбрасывается, например:

int x = 1/3; 
// Будет ровно ноль! Дробная часть отбрасывается в int.
// Когда мы пишем числа без десятичной точки, то они сразу считаются как int
// 10 - это int
// 10.2 - это double

// сразу разберем нюансы:
int y = 10.2; // ошибка - справа дробное число 10.2 double, нужно явно привести тип к int
int z = (int) 10.2; // так правильно, дробная часть 0.2 будет отброшена

double u = 100; // сначала вычисляется выражение, здесь это просто число 100 - целое
// но чтобы приравнять дробное u к целому 100, делается неявное приведение типа.
// Поэтому u как была объявлена дробной double, так и останется ей навсегда.
Что же, 1/3 = 0 - это логично, в целых числах не могут храниться дробные и мы вынуждены отбрасывать дробные доли. Но довольно часто это приводит к огромным ошибкам, полностью уничтожающим логику приложения. Например, вы делаете калькулятор людей в Москве, владеющих автомобилем. Вы знаете, что всего есть порядка 15000000 жителей и допустим пользователь-статист ввёл, что треть из них владеют автомобилями. Тогда программа должна посчитать «15 миллионов умножить на одну треть» и распечатать пять миллионов, но легко ошибиться:

int x = (1/3) * 15000000; 
//Будет снова ноль! Дробная часть отбрасывается в int еще при расчете 1/3

/*
 К слову, если в выражении есть double, то операции с ним уже будут идти как с дробным
 числом, то есть int на double - это double. Но важно учесть, что компилятор вычисляет 
 выражение слева направо, и если было int на int пока он еще не дошел до дробного числа, 
 то эта часть пройдет в целых числах.
 Например:
*/
int y = (1/3) * 15000000.0 ; // справа сначала 1/3 будет 0, на что ни умножай. 
// при этом мы помножили на дробное double, поэтому int на double будет double
// и компилятор скажет - ошибка, ведь чтобы int присвоить к double нужно явно привести тип:
int z = (int) ((1/3) * 15000000.0); // теперь ошибки нет, но z по-прежнему равно нулю

int u = (int) ((1.0/3) * 15000000); // теперь правильно 1.0/3 это одна треть, а не ноль,
// поскольку 1.0 - дробное и стоит в начале выражения, так что и все остальные 
// операции будут с дробными числами. 
// Снова явно приводим к int, хотим приравнять int u к double.
В этом примере главное (1/3) – это выражение независимо от всего остального понимается Java как целое делить на целое (int на int), а значит вычислится с отбрасыванием дробной части и обратится в ноль. При этом ноль умножить на любое число будет ноль, хотя мы умножили на 15 миллионов.

Еще несколько замечаний по этому примеру:

int a = 15000000 / 3; // мы сразу получим нужные 5 миллионов, вообще без дробных чисел

int b = (int) ( (1/3.0) * 15000000 ); 
// 3.0 – это дробное, тогда и 1/3.0 – это треть, а не ноль.
// Благодаря скобкам уже после расчета всего выражения 5 млн будет приведено к целым, 
// но оно и так целое, так что мы все посчитали точно.

int с = (int) (1.0/3) * 15000000; // будет опять чистый ноль, 
// так как сначала треть приведется к целому типу и обнулится - при равных приоритетах 
// без дополнительных скобок выражение вычисляется слева направо, то есть (int) раньше.

int d = (1.0/3) * 15000000; // ошибка компиляции, справа дробное, слева целое - 
// компилятор боится потерять точность без разрешения программиста.


Теперь получится 5 миллионов. Представьте, если это был финансовый робот в банке? Он мог взять и обнулить 5 миллионов фунтов стерлингов у вас на счету! Поэтому в таких вычислениях нужно использовать дробные, а лучше ещё более точные специальные числа типа BigDecimal и тому подобных. О них речь будет позже, пока что освоим обычные числа и заставим их вычисляться хотя бы приблизительно правильно.

Другой вариант той же ошибки:

int x = 1; 
int y = 3;
double z = 15000000 * x / y; // сначала вычисляется выражение с его собственным типом
// а сейчас в выражении целое x делится на целое y и получается машинный ноль.
// то что слева стоит double не поможет - уже вычисленное выражение равно нулю.


Здесь мы объявили две целочисленные переменные x и y. Снова при их делении оператор / будет воспринят как «делить нацело» и результат (int на int) будет нулевым. Так что лучше просто сделайте числа и переменные, которые участвуют в дробных выражениях типом double заранее:

double x = 1; 
double y = 3;
double z = 15000000 * x / y; // деление x на y будет одна треть, потому что x и y - дробные.

Операции с типом int выполняются быстрее, чем с double, но в небольших приложениях этот выигрыш очень мал. Если всё-таки оптимизируете программу по скорости и используете int, то прошу, будьте с ним очень аккуратны!

Самое главное мы уже освоили. Теперь время решать задачи. Ответьте на несколько вопросов, чтобы лучше усвоился новый материал:
Задача 1. Ниже дан код. В нем распечатывается сначала 'a' как символ, затем распечатается код (по сути просто номер буквы в интернациональном алфавите) этого символа. Как бы вы распечатали символ с кодом на 3 больше? Непосредственно этого не было описано в материале темы, но вы можете догадаться - только не тратьте больше 10 минут на задачу, просто посмотрите решение, это тоже часть процесса обучения. Вот в задачнике даны задачи без решений, там можно засеть надолго :)

char symbol = 'a';
        
System.out.println (symbol);
        
int code = (int) symbol;
        
System.out.println (code);

Решайте больше задач по этому разделу здесь.
В следующей теме расскажем
о процентах и делении с остатком в Java