Зачем вообще нужны переменные
Программы обрабатывают данные. Это их основная работа, если честно. А чтобы что-то обработать, надо где-то это хранить. Вот для этого и придумали переменные — такие контейнеры для данных, которые живут, пока программа работает.
![Присваивание и инициализация в 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 — это база. Сначала кажется сложным, но когда разберёшься — многие вещи станут логичными. Ты начнёшь понимать, почему код ведёт себя так, а не иначе, и где искать проблемы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ