🎓 УРОК 2: Переменные. Примитивные типы данных


📌 ЦЕЛИ УРОКА

К концу этого урока ты сможешь:

  1. Объяснить, что такое переменная в Java и как работает статическая типизация
  2. Перечислить все 8 примитивных типов данных Java и знать диапазоны их значений
  3. Правильно объявлять, инициализировать и использовать переменные разных типов
  4. Понимать разницу между объявлением и инициализацией переменной
  5. Объяснить, что происходит в памяти при создании примитивной переменной
  6. Применять правила именования переменных и понимать конвенции camelCase

🔗 СВЯЗЬ С ПРЕДЫДУЩИМИ ТЕМАМИ

В Уроке 1 мы уже мельком использовали числа в примерах (5 + 3, 10 > 5), но не объявляли их как переменные — писали "жёстко" внутри println. Сегодня разберём, как хранить данные в именованных ячейках памяти — переменных.

Также вспомним: Java — статически типизированный язык. Мы уже видели в структуре main, что параметр объявлен как String[] args — то есть тип указывается явно. Сегодня разберём эту концепцию досконально, потому что она пронизывает весь язык.


📖 ТЕОРЕТИЧЕСКАЯ ЧАСТЬ

1. Что такое переменная и статическая типизация

Переменная — это именованная область памяти, которая хранит значение определённого типа.

Ключевое слово здесь — "определённого типа". В Java тип переменной фиксируется в момент объявления и не может измениться в течение жизни переменной. Это называется статическая типизация (static typing).

int age = 25;      // переменная age навсегда останется типом int
age = "двадцать пять";  // ❌ ОШИБКА КОМПИЛЯЦИИ! Нельзя присвоить строку переменной типа int

Почему так строго? Компилятор Java проверяет типы ещё ДО запуска программы (на этапе компиляции). Это позволяет отловить огромное количество ошибок заранее, а не во время выполнения программы у пользователя. Это философия надёжности Java: лучше упасть с понятной ошибкой компиляции, чем сломаться в проде через полгода из-за неожиданного типа данных.

Компилятор всегда должен знать заранее, сколько памяти выделить и какие операции допустимы для переменной — а это возможно только если тип известен и неизменен.

2. Синтаксис объявления переменной

Общая форма:

тип имяПеременной = значение;

Например:

int count = 10;
double price = 99.99;
boolean isActive = true;

Три обязательных части:

  1. Тип — что за данные будут храниться (int, double, boolean и т.д.)
  2. Имя — идентификатор переменной, по которому мы к ней обращаемся
  3. Значение — то, что записывается в переменную (может отсутствовать при объявлении без инициализации)

3. Объявление vs инициализация — важное разграничение

Это два разных, хоть и часто совмещаемых действия:

Объявление (declaration) — сообщаем компилятору, что переменная с таким именем и типом существует:

int age;   // объявили переменную age типа int, но значения ещё нет

Инициализация (initialization) — присваиваем переменной первое значение:

age = 25;  // инициализировали ранее объявленную переменную

Их можно объединить в одну строку (и обычно так и делают):

int age = 25;  // объявление + инициализация одновременно

⚠️ Критически важный момент: в Java локальные переменные (объявленные внутри метода) не имеют значения по умолчанию. Если ты объявишь переменную, но попытаешься её использовать до присвоения значения — компилятор выдаст ошибку:

public static void main(String[] args) {
    int x;                      // объявили, но не инициализировали
    System.out.println(x);      // ❌ ОШИБКА: variable x might not have been initialized
}

Это защита от неопределённого поведения — компилятор не даст тебе случайно использовать "мусорное" значение.

(Забегая вперёд: поля класса (не локальные переменные) ведут себя иначе — они получают значения по умолчанию автоматически. Но об этом подробно — в уроке про классы, урок 14.)

4. Восемь примитивных типов данных

Java — язык с чёткой иерархией типов. Есть 8 примитивных типов (primitive types) — они хранят "сырые" значения напрямую, а не ссылки на объекты (про ссылочные типы — в Уроке 3).

Разделим их на 4 категории:

🔢 Целочисленные типы (integer types)

Тип Размер Диапазон значений Пример
byte 8 бит (1 байт) от -128 до 127 byte b = 100;
short 16 бит (2 байта) от -32,768 до 32,767 short s = 30000;
int 32 бита (4 байта) от -2,147,483,648 до 2,147,483,647 (≈ ±2.1 млрд) int i = 100000;
long 64 бита (8 байт) от -9.2×10¹⁸ до 9.2×10¹⁸ long l = 10000000000L;

По умолчанию любое целое число в коде — это int. Если нужно long, обязательно добавляй суффикс L (заглавная или строчная, но заглавную предпочитают — легче отличить от единицы):

long bigNumber = 3000000000L;  // без L будет ошибка компиляции — 
                                 // число не помещается в int

Почему четыре разных типа для целых чисел? Это про экономию памяти. Если ты храмишь возраст человека (максимум ~150), незачем выделять 4 байта под int, если хватит 1 байта byte. На современных компьютерах разница в памяти для одной переменной несущественна, но при работе с массивами из миллионов элементов выбор правильного типа критически важен для производительности.

🔢 Типы с плавающей точкой (floating-point types)

Тип Размер Точность Пример
float 32 бита ~6-7 значащих цифр float f = 3.14f;
double 64 бита ~15-16 значащих цифр double d = 3.14159265;

По умолчанию любое дробное число в коде — это double. Если нужен float, обязателен суффикс f:

float price = 19.99f;   // без f — ошибка компиляции
double price2 = 19.99;  // а тут суффикс не нужен, double по умолчанию

⚠️ Важнейший подводный камень: типы с плавающей точкой хранят числа неточно из-за особенностей двоичного представления дробей. Классический пример:

System.out.println(0.1 + 0.2);   // выведет 0.30000000000000004, а не 0.3!

Это не баг Java, а фундаментальная особенность стандарта IEEE 754, по которому работают float/double во всех языках программирования. Для точных денежных расчётов используют специальный класс BigDecimal (разберём в продвинутых темах), а не double.

Когда использовать double, а когда float? В подавляющем большинстве случаев — double. float используют редко, в основном там, где критична экономия памяти (например, в графике, машинном обучении с большими массивами данных).

🔤 Символьный тип

Тип Размер Диапазон Пример
char 16 бит от 0 до 65,535 (символы Unicode) char letter = 'A';

char хранит один символ в одинарных кавычках (не путать со строками в двойных кавычках — про это подробно в Уроке 3):

char grade = 'A';
char digit = '5';       // это символ '5', а не число 5!
char unicodeSymbol = '\u0041';  // тоже 'A', через Unicode-код

Интересный факт: char в Java — это фактически 16-битное число (беззнаковое), поэтому с ним можно выполнять арифметику:

char letter = 'A';
System.out.println((int) letter);   // выведет 65 (код символа 'A' в Unicode/ASCII)

✅ Логический тип

Тип Размер Значения Пример
boolean не определён формально (JVM-зависимо, условно 1 бит) true / false boolean isJavaFun = true;

Только два возможных значения — true или false. Никаких 0 и 1 вместо них, как в некоторых языках!

boolean isReady = true;
boolean hasErrors = 0;    // ❌ ОШИБКА! boolean не совместим с int в Java

5. Таблица-памятка всех примитивов

┌───────────┬──────────┬─────────────────────────────────────┐
│   Тип     │  Размер  │              Что хранит              │
├───────────┼──────────┼─────────────────────────────────────┤
│  byte     │  1 байт  │  Целые числа (-128...127)            │
│  short    │  2 байта │  Целые числа (-32768...32767)        │
│  int      │  4 байта │  Целые числа (±2.1 млрд) — по умолч. │
│  long     │  8 байт  │  Большие целые числа (нужен суфф. L) │
│  float    │  4 байта │  Дробные числа (нужен суфф. f)       │
│  double   │  8 байт  │  Дробные числа — по умолчанию        │
│  char     │  2 байта │  Один символ (в одинарных кавычках)  │
│  boolean  │   —      │  true / false                        │
└───────────┴──────────┴─────────────────────────────────────┘

6. Что происходит в памяти при создании примитивной переменной

Когда JVM выполняет метод main, для него выделяется область в стеке (Stack). Локальные примитивные переменные хранятся непосредственно в стеке, со своим значением напрямую (не через ссылку, в отличие от объектов, которые лежат в куче — про это подробно в Уроке 3).

public static void main(String[] args) {
    int age = 25;       // в стеке создаётся ячейка "age" со значением 25
    double price = 9.99; // в стеке создаётся ячейка "price" со значением 9.99
}

Схематично:

СТЕК (Stack)
┌─────────────────┐
│ main() frame     │
│  ┌─────────────┐ │
│  │ age = 25     │ │
│  │ price = 9.99 │ │
│  └─────────────┘ │
└─────────────────┘

Когда метод main завершается, весь его "фрейм" в стеке уничтожается автоматически — переменные age и price перестают существовать. Это очень быстрый и эффективный механизм работы с памятью для простых значений.

7. Правила именования переменных (identifiers)

Технические правила (обязательные, иначе ошибка компиляции):

Соглашения (best practice, не обязательны технически, но крайне важны для читаемости кода):

int studentAge = 20;          // ✅ правильно
int student_age = 20;         // ⚠️ работает, но не Java-стиль (это стиль Python/C)
int StudentAge = 20;          // ⚠️ работает, но заглавная первая буква — конвенция для классов, не переменных
int a = 25;           // ❌ плохо — непонятно, что это
int userAge = 25;     // ✅ хорошо — сразу понятно назначение

8. Константы: ключевое слово final

Если значение переменной никогда не должно меняться после инициализации, используют модификатор final:

final double PI = 3.14159;
PI = 3.15;   // ❌ ОШИБКА компиляции: cannot assign a value to final variable PI

По конвенции константы называют ЗАГЛАВНЫМИ_БУКВАМИ с подчёркиванием:

final int MAX_USERS = 100;
final String APP_NAME = "MyApp";  // забегая вперёд, String разберём в Уроке 3

(Подробно про final — отдельный Урок 22, сегодня только знакомство с концепцией.)

9. Множественное объявление переменных одного типа

Java позволяет объявить несколько переменных одного типа в одной строке:

int x = 1, y = 2, z = 3;

Технически работает, но на практике избегай такого стиля — читаемость кода важнее компактности. Предпочитай:

int x = 1;
int y = 2;
int z = 3;

💻 ПРИМЕРЫ КОДА

Пример 1: Базовое объявление разных типов

public class Main {
    public static void main(String[] args) {
        int age = 25;                  // целое число
        double salary = 55000.50;      // дробное число
        char grade = 'A';              // символ
        boolean isEmployed = true;     // логическое значение

        System.out.println("Возраст: " + age);
        System.out.println("Зарплата: " + salary);
        System.out.println("Оценка: " + grade);
        System.out.println("Трудоустроен: " + isEmployed);
    }
}

Вывод в консоли:

Возраст: 25
Зарплата: 55000.5
Оценка: A
Трудоустроен: true

Разбор построчно:


Пример 2: Диапазоны типов и переполнение (overflow)

public class Main {
    public static void main(String[] args) {
        byte maxByte = 127;
        System.out.println("Максимум byte: " + maxByte);
        
        maxByte = (byte) (maxByte + 1);   // выходим за границу диапазона!
        System.out.println("После переполнения: " + maxByte);
    }
}

Вывод в консоли:

Максимум byte: 127
После переполнения: -128

Что произошло: byte вмещает значения от -128 до 127. Когда мы прибавляем 1 к максимальному значению 127, происходит переполнение (overflow) — значение "перескакивает" на противоположный край диапазона, на -128. Это классическая проблема всех языков с фиксированной битностью типов, не только Java. Важно об этом знать, чтобы не получить неожиданный баг в реальном проекте.

(Про приведение типов (byte) подробно поговорим в Уроке 30, сегодня просто демонстрация проблемы.)


Пример 3: Ошибка неинициализированной переменной

public class Main {
    public static void main(String[] args) {
        int result;                    // объявили, не инициализировали
        System.out.println(result);    // ❌ ОШИБКА компиляции
    }
}

Сообщение компилятора:

error: variable result might not have been initialized
        System.out.println(result);
                            ^
1 error

Как исправить:

int result = 0;   // всегда инициализируй переменную перед использованием
System.out.println(result);   // теперь работает

Пример 4: Разница float и double + необходимость суффиксов

public class Main {
    public static void main(String[] args) {
        float price = 19.99f;      // суффикс f обязателен!
        double weight = 72.5;      // суффикс не нужен, double — тип по умолчанию

        System.out.println("Цена: " + price);
        System.out.println("Вес: " + weight);

        // long bigNum = 5000000000;    // ❌ закомментировано намеренно — 
                                          // без суффикса L будет ошибка компиляции,
                                          // т.к. число превышает диапазон int
        long bigNum = 5000000000L;      // ✅ с суффиксом L — работает
        System.out.println("Большое число: " + bigNum);
    }
}

Вывод в консоли:

Цена: 19.99
Вес: 72.5
Большое число: 5000000000

⚠️ Если убрать f у float:

float price = 19.99;   // ❌ ошибка: incompatible types: possible lossy conversion from double to float

Компилятор скажет, что по умолчанию 19.99 — это double, а double нельзя автоматически "втиснуть" в float без явного приведения (double занимает больше памяти и точнее, чем float) — это защита от потери точности.


Пример 5: Демонстрация неточности double (классическая ловушка)

public class Main {
    public static void main(String[] args) {
        double a = 0.1;
        double b = 0.2;
        double sum = a + b;

        System.out.println("0.1 + 0.2 = " + sum);
        System.out.println("Равно ли 0.3? " + (sum == 0.3));
    }
}

Вывод в консоли:

0.1 + 0.2 = 0.30000000000000004
Равно ли 0.3? false

Почему так происходит: дробные числа в двоичной системе не всегда представимы точно (аналогично тому, как 1/3 нельзя точно записать конечной десятичной дробью). Это стандартная особенность IEEE 754, по которому работают float/double во всех современных языках. Для сравнения дробных чисел на "почти равенство" используют небольшую погрешность (эпсилон), а для денежных расчётов — специальный класс BigDecimal.


⌨️ ГОРЯЧИЕ КЛАВИШИ INTELLIJ IDEA

Действие Windows/Linux macOS Когда использовать
Переименовать переменную везде в коде Shift + F6 Shift + F6 Изменить имя переменной сразу во всех местах использования — безопаснее, чем ручная замена
Быстрое исправление (Quick Fix) Alt + Enter Option + Enter Когда IDE подчёркивает ошибку/предупреждение — покажет варианты исправления (например, добавить суффикс L)
Извлечь переменную (Extract Variable) Ctrl + Alt + V Cmd + Alt + V Выделить кусок выражения и превратить его в отдельную переменную автоматически
Показать тип переменной / информацию о выражении Ctrl + Shift + P Cmd + Shift + P Узнать выведенный тип выражения, не считая вручную
Дублировать строку Ctrl + D Cmd + D Быстро скопировать строку объявления переменной для создания похожей
Перейти к объявлению переменной Ctrl + B (или Ctrl+клик) Cmd + B Найти, где переменная была объявлена, в большом файле

💡 Особенно полезно сегодня: Alt+Enter — когда напишешь float price = 19.99; без суффикса, IDE подчеркнёт ошибку, и через Alt+Enter можно выбрать автоматическое исправление "Add 'f' suffix".


🎯 ПРАКТИЧЕСКИЕ ЗАДАНИЯ

Задание 1 (5 мин) — Все типы разом

Объяви по одной переменной каждого из 8 примитивных типов, инициализируй их произвольными подходящими значениями и выведи все в консоль с подписями.

Критерий проверки: используются все 8 типов (byte, short, int, long, float, double, char, boolean), нет ошибок компиляции.


Задание 2 (10 мин) — Профиль пользователя

Создай набор переменных, описывающих профиль пользователя: имя (пока просто отдельные буквы через char, или пропусти — String будет в след. уроке), возраст (int), рост в метрах (double), является ли студентом (boolean), первая буква имени (char). Выведи всё в читаемом формате.

Пример вывода:

Возраст: 22
Рост: 1.78
Студент: true
Первая буква имени: А

Задание 3 (10 мин) — Найди и исправь ошибку

Дан код с 3 ошибками. Найди их все, объясни причину и исправь:

public class Main {
    public static void main(String[] args) {
        int count;
        System.out.println(count);
        
        float weight = 65.5;
        
        long population = 8000000000;
    }
}

Критерий проверки: все 3 ошибки найдены (использование неинициализированной переменной, отсутствие суффикса f, отсутствие суффикса L), код компилируется после исправления.


Задание 4 (10 мин) — Демонстрация переполнения

Напиши код, который демонстрирует переполнение для типа short (диапазон -32768...32767). Выведи значение до и после переполнения, объясни результат комментарием в коде.


Задание 5 (15 мин) — Работа с константами

Создай программу-калькулятор площади круга, используя константу PI (объявленную через final), и переменную радиуса. Выведи результат.

Пример:

final double PI = 3.14159;
double radius = 5.0;
double area = PI * radius * radius;
System.out.println("Площадь круга: " + area);

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


Задание 6 (15 мин) — Ловушка double

Воспроизведи пример с 0.1 + 0.2 из теории. Затем попробуй сложить 0.1f + 0.2f (используя float вместо double) — сравни результаты. Объясни своими словами (в комментарии в коде), почему это происходит.


Задание 7 (15 мин) — char как число

Напиши код, который:

  1. Объявляет char letter = 'Z';
  2. Выводит саму букву
  3. Выводит её числовой Unicode-код (используя приведение (int) letter)
  4. Прибавляет к букве число 1 и выводит результат — что получилось? (Подсказка: результат сам по себе будет числом типа int, если явно не привести обратно к char)

Критерий проверки: понимание того, что char внутри — это число, и можно к нему применять арифметику.


❓ ПРОВЕРОЧНЫЕ ВОПРОСЫ

1. Что такое статическая типизация?

Ответ Тип переменной определяется на этапе компиляции и фиксируется — его нельзя изменить в течение "жизни" переменной. Компилятор проверяет корректность типов ещё до запуска программы.

2. В чём разница между объявлением и инициализацией переменной?

Ответ Объявление — это создание переменной определённого типа без присвоения значения (`int x;`). Инициализация — присвоение первого значения переменной (`x = 5;`). Можно объединить в одну строку: `int x = 5;`.

3. Что выведет этот код?

int x;
System.out.println(x);
Ответ Программа не скомпилируется — ошибка "variable x might not have been initialized". Локальные переменные не имеют значения по умолчанию.

4. Какой суффикс нужен для литерала типа long?

Ответ `L` (заглавная предпочтительнее строчной `l`, чтобы не путать с единицей `1`). Пример: `long x = 5000000000L;`

5. Что выведет код?

double result = 0.1 + 0.2;
System.out.println(result);
Ответ `0.30000000000000004` — из-за особенностей представления дробных чисел в двоичной системе (IEEE 754), а не ровно `0.3`.

6. Сколько байт занимает тип int?

Ответ 4 байта (32 бита), диапазон примерно от -2.1 до +2.1 миллиарда.

7. Что произойдёт при выполнении этого кода?

byte b = 127;
b = (byte)(b + 1);
System.out.println(b);
Ответ Выведет `-128` — переполнение (overflow): при выходе за верхнюю границу диапазона `byte` значение "перескакивает" на нижнюю границу.

8. Какой тип данных используется по умолчанию для дробных чисел, если не указан суффикс?

Ответ `double`. Поэтому `float price = 19.99;` без суффикса `f` вызовет ошибку компиляции (несовместимость типов).

9. Верно ли, что boolean isReady = 1; — корректный код в Java?

Ответ Неверно. В Java `boolean` НЕ совместим с числами — нельзя присвоить `0`/`1` вместо `true`/`false`. Это вызовет ошибку компиляции.

10. Что такое final в контексте переменной?

Ответ Модификатор, делающий переменную константой — значение можно присвоить один раз, повторное присвоение вызовет ошибку компиляции.

11. Как по конвенции называются константы в Java?

Ответ ЗАГЛАВНЫМИ_БУКВАМИ с подчёркиванием между словами, например `MAX_USERS`, `PI_VALUE`.

12. Что выведет код?

char letter = 'A';
System.out.println((int) letter);
Ответ `65` — числовой Unicode/ASCII-код символа `'A'`. `char` внутренне представлен как число.

13. Какой стиль именования переменных принят в Java (camelCase, snake_case или PascalCase)?

Ответ camelCase — первое слово с маленькой буквы, последующие слова с заглавной: `studentAge`, `totalPrice`.

14. Почему в Java существует 4 разных целочисленных типа (byte, short, int, long), а не один универсальный?

Ответ Для оптимизации использования памяти — выбор типа под конкретный диапазон значений экономит память, особенно критично при работе с большими массивами данных.

15. Что произойдёт при компиляции этого кода?

int x = 10, y = 20;
final int MAX = 100;
MAX = 200;
Ответ Ошибка компиляции на строке `MAX = 200;` — "cannot assign a value to final variable MAX", т.к. `MAX` объявлена как константа (`final`) и уже была инициализирована значением `100`.

🔄 ДОМАШНЕЕ ЗАДАНИЕ

Задача 1: "Паспорт товара"

Напиши программу Product.java, моделирующую характеристики товара в интернет-магазине:

Выведи все характеристики в консоль в читаемом формате, а также рассчитай итоговую цену со скидкой.

Пример вывода:

=== Информация о товаре ===
Цена: 1500.0
Количество на складе: 42
В наличии: true
Рейтинг: 5
Скидка: 10.0%
Цена со скидкой: 1350.0

Обязательные требования:


Задача 2: "Демонстрация всех подводных камней"

Напиши ОДНУ программу PitfallsDemo.java, которая последовательно (с комментариями-пояснениями перед каждым блоком) демонстрирует:

  1. Переполнение byte при увеличении сверх максимума
  2. Ошибку неточности double при сложении 0.1 + 0.2
  3. Разницу между float (с суффиксом f) и double для одного и того же числа 123.456789 — сравни, сколько значащих цифр сохраняется у каждого
  4. Приведение char к числу и обратно

Критерии оценки: все 4 пункта продемонстрированы с понятным выводом в консоль, каждый блок кода предваряется комментарием, объясняющим, что демонстрируется.


Задача 3: "Мини-калькулятор возраста в разных единицах" (комплексная, объединяет с Уроком 1)

Напиши программу, которая:

  1. Объявляет переменную int ageInYears = 25;
  2. Вычисляет и выводит: возраст в месяцах (int), возраст в днях (приблизительно, long, т.к. число может быть большим), возраст в часах (long)
  3. Использует константу final int DAYS_IN_YEAR = 365;
  4. Форматирует вывод так, чтобы каждая единица измерения была на отдельной строке с использованием println

Пример вывода:

Возраст в годах: 25
Возраст в месяцах: 300
Возраст в днях: 9125
Возраст в часах: 219000

Критерии оценки: корректные расчёты, использование минимум типов int и long, использование final-константы, применение техник вывода из Урока 1.


📚 ДОПОЛНИТЕЛЬНЫЕ МАТЕРИАЛЫ

Официальная документация:

На что обратить особое внимание:

Частые ошибки новичков:

  1. Забывают суффикс L для больших чисел (long) или f для float
  2. Путают одинарные и двойные кавычки (char vs String — про это подробно в следующем уроке)
  3. Используют double для денежных расчётов там, где нужна точность (правильно — BigDecimal, разберём позже)
  4. Пытаются сравнивать boolean с числами 0/1 (что не работает в Java, в отличие от некоторых других языков)
  5. Забывают, что при делении двух int результат всегда int — дробная часть отбрасывается (например, 7 / 2 даст 3, а не 3.5) — подробно разберём при изучении операторов в следующих уроках

✅ ЧЕК-ЛИСТ ГОТОВНОСТИ

После этого урока ты должен уметь без подсказок:

Если что-то из списка вызывает сомнение — вернись к соответствующему разделу теории перед переходом к Уроку 3 (String и ссылочные типы)! 🚀