Зачем вообще нужны переменные

Программы обрабатывают данные. Это их основная работа, если честно. А чтобы что-то обработать, надо где-то это хранить. Вот для этого и придумали переменные — такие контейнеры для данных, которые живут, пока программа работает.Присваивание и инициализация в Java - 1

Какие бывают переменные в Java

В Java есть три основных типа переменных, и они отличаются тем, где объявлены и как долго живут. Поля класса (их ещё называют fields) — это переменные, которые ты пишешь прямо в классе, но не внутри методов. Они описывают состояние твоего объекта. Локальные переменные — те, что объявляются внутри метода или какого-то блока кода. Как только метод завершается — они исчезают. Параметры — это переменные в скобках при объявлении метода. Они получают значения, когда кто-то вызывает твой метод. Все переменные должны иметь тип переменной и название переменной.
  • Тип переменной показывает, какие данные представляет данная переменная (т.е. какие данные может хранить). Как мы знаем, тип переменной может быть примитивным (primitives primitives) или объектным, не примитивными (Non-primitive). При объектных переменных их тип описывается определённым классом.
  • Название переменной должно быть с маленькой буквы, в camel case. Подробнее про именование можно прочитать в "Variables:Naming".
Так же если переменная уровня класса, т.е. является полем класса, то для неё может указываться модификатор доступа.

Объявление переменной

Объявить переменную — это как сказать компилятору "эй, у меня тут будет переменная с таким-то именем и типом". Смотри, что получается:

public class Example {
    public static void main(String[] args) {
        int number;
        System.out.println(number);
    }
}
Запускаешь — а тебе сразу ошибка в лицо:

error: variable number might not have been initialized
Мы объявили переменную number, но забыли дать ей значение. И компилятор не даёт тебе даже запустить это. Почему? Потому что локальные переменные не получают никаких значений автоматически. Вообще никаких. И это проверяется ещё до запуска программы, на этапе компиляции. Поэтому, из этого следует следующие утверждения:
  • Обращение к локальным переменным должно быть выполнено только после того, как они будут инициализированы;
  • Локальные переменные не имеют значений по умолчанию;
  • Проверка значений локальных переменных выполняется в момент компиляции.
Итак, нам говорят, что переменная должна быть проинициализирована. Инициализация переменной – присвоение переменной значения. Давайте тогда разбираться, что это и почему.

Инициализация — присваиваем значение

Инициализация — это когда ты даёшь переменной начальное значение. Чтобы код заработал, нужен оператор присваивания =:

public class Example {
    public static void main(String[] args) {
        int number = 2;
        System.out.println(number);
    }
}
Теперь всё окей, код работает. Но что там внутри происходит? Давай копнём глубже.

Что творится в памяти

Когда инициализируешь переменную, JVM (это виртуальная машина Java) выделяет под неё место в памяти. Память в Java-процессе разбита на несколько зон — есть Heap, Stack, Metaspace. Начнём со Stack. Представь стопку тарелок — положил одну, потом вторую сверху, потом третью. Когда берёшь — снимаешь верхнюю. Это называется LIFO (Last In, First Out). Запускается программа, JVM видит метод main и создаёт поток (Thread). У каждого потока свой стек. Вызывается метод — создаётся фрейм в стеке, там хранятся все локальные переменные этого метода. Примитивы типа int, double, boolean живут прямо в стеке. Вот почему компилятор требует их инициализировать — JVM должна точно знать, какое значение записать в стек. Метод завершился — фрейм удаляется, переменные пропадают. Всё просто. (!) Для наглядности советую супер-пупер видео:

А если переменная — это объект?

С объектами чуть хитрее. Смотри пример:

public class Example {
    private int number = 2;

    public static void main(String[] args) {
        Example object = new Example();
        System.out.println(object.number);
    }
}
Что тут происходит? Во-первых, JVM создаёт фрейм для метода main в стеке. Потом видит new Example() — и создаёт объект. Но не в стеке, а в Heap (куче). В стеке хранится только ссылка на этот объект. Грубо говоря, адрес, где его найти. А сам объект со всеми его полями живёт в куче. Метод main завершается, фрейм из стека удаляется. Ссылка пропадает, но объект-то остался в куче. На него теперь никто не ссылается — и тут в дело вступает сборщик мусора. Он находит такие "потерянные" объекты и удаляет их. Автоматически, без твоего участия.

Поля класса — отдельная история

Поля класса работают не так, как локальные переменные. Посмотри:

public class Example {
    private int number;
    private static int count;

    public static void main(String[] args) {
        Example object = new Example();
        System.out.println(object.number); // Выведет 0
        System.out.println(count); // Выведет 0
    }
}
Заметил? Мы не присвоили никаких значений, а код работает. Выводит нули. Это потому что поля класса получают значения по умолчанию: - Числа — это 0 или 0.0 - boolean — это false - Объекты — это null Есть важное различие между обычными и статическими полями. Обычное поле number создаётся, когда создаёшь объект (new Example()). Статическое поле count создаётся раньше — когда JVM загружает сам класс в память. Это происходит до создания любых объектов.

Где живут статические переменные

Статические переменные — часть самого класса, его метаданные. С версии Java 8 они хранятся в Metaspace. Раньше была область PermGen, но её убрали. Metaspace — это Non-Heap память, кстати.

Порядок имеет значение

Нельзя использовать переменную до её объявления. Вот такой код не скомпилируется:

public class Example {
    private static int b = a; // Ошибка!
    private static int a = 1;

    public static void main(String[] args) {
        System.out.println(b);
    }
}
Ошибка, потому что пытаемся взять значение a, которая объявлена позже.

Что за зверь этот null

Для объектных переменных значение по умолчанию — null. Это не объект и не число. Это вообще специальное значение, которое означает "тут ничего нет".

public class Example {
    private String text; // По умолчанию null

    public static void main(String[] args) {
        Example object = new Example();
        System.out.println(object.text); // Выведет: null
    }
}
null — это литерал, который показывает отсутствие ссылки. Попробуешь вызвать метод на null — получишь NullPointerException:

String text = null;
System.out.println(text.length()); // Бах! NullPointerException
Эта ошибка встречается постоянно, особенно на первых порах. Привыкай проверять переменные на null там, где это нужно.

Кстати, про var

С Java 10 появилось ключевое слово var. Оно позволяет не писать тип явно — компилятор сам разберётся:

var number = 10; // Это int
var text = "Hello"; // Это String
var list = new ArrayList(); // Компилятор видит тип
Удобно, правда? Но есть ограничения. var работает только для локальных переменных, и только если ты сразу даёшь значение. Нельзя так:

var x; // Ошибка: а какой тип-то?
var y = null; // Ошибка: компилятор не может понять тип

Что важно запомнить

Локальные переменные надо инициализировать. Обязательно. Иначе компилятор не пропустит код. Поля класса инициализируются автоматически — числа получают 0, boolean получает false, объекты получают null. Примитивы живут в стеке (если это локальные переменные) или внутри объекта в куче (если это поля). Объекты всегда создаются в куче. В стеке хранятся только ссылки на них. Статические переменные создаются при загрузке класса и хранятся в Metaspace. Сборщик мусора сам удаляет объекты, на которые больше никто не ссылается. Не нужно делать это вручную. С Java 10 можешь использовать var для локальных переменных — компилятор сам определит тип. Вообще, понимание работы с памятью в Java — это база. Сначала кажется сложным, но когда разберёшься — многие вещи станут логичными. Ты начнёшь понимать, почему код ведёт себя так, а не иначе, и где искать проблемы.