Объявление, инициализация и присваивание это три разных шага работы с переменной. Объявление сообщает компилятору имя и тип: int number;. Инициализация дает переменной первое значение, обычно прямо в той же строке: int number = 2;. Присваивание это любая последующая запись нового значения оператором =. Локальную переменную обязательно нужно инициализировать до первого обращения, иначе код не скомпилируется, а полям класса компилятор сам проставляет значения по умолчанию.![Схема: объявление переменной, инициализация значением и последующее присваивание]()
Кратко
- Объявление это имя и тип, инициализация это первое значение, присваивание это любая запись значения оператором =.
- Локальные переменные значений по умолчанию не получают. Обращение к неинициализированной локальной переменной это ошибка компиляции, а не сбой во время работы программы.
- Поля класса, наоборот, инициализируются автоматически: числа нулем, boolean значением false, ссылки значением null.
- Локальные примитивы лежат в кадре стека, объекты всегда создаются в куче, а в стеке хранится только ссылка на объект.
- Обычное поле появляется вместе с объектом, статическое раньше, при загрузке класса.
- С Java 10 тип локальной переменной можно не писать: var выведет его из значения. Но только там, где значение задано сразу.
Зачем вообще нужны переменные
Программы обрабатывают данные. Это их основная работа, если честно. А чтобы что-то обработать, надо где-то это хранить. Вот для этого и придумали переменные, такие контейнеры для данных, которые живут, пока программа работает.
Какие бывают переменные в Java
В Java есть три основных типа переменных, и они отличаются тем, где объявлены и как долго живут. Поля класса (их еще называют fields) это переменные, которые ты пишешь прямо в классе, но не внутри методов. Они описывают состояние твоего объекта. Локальные переменные это те, что объявляются внутри метода или какого-то блока кода. Как только метод завершается, они исчезают. Параметры это переменные в скобках при объявлении метода. Они получают значения, когда кто-то вызывает твой метод. Все переменные должны иметь тип переменной и название переменной.- Тип переменной показывает, какие данные представляет данная переменная (т.е. какие данные может хранить). Как мы знаем, тип переменной может быть примитивным (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 живут прямо в стеке. А вот требовать инициализацию компилятор заставляет не поэтому. Причина в правилах самого языка: глава 16 спецификации Java говорит, что каждая локальная переменная должна быть определенно присвоена к моменту любого обращения к ее значению, иначе возникает ошибка компиляции. Правило одно и то же для примитивов и для ссылок, так что переменную типа String компилятор проверяет ровно так же строго. Метод завершился, фрейм удаляется, переменные пропадают. Все просто. (!) Для наглядности советую супер-пупер видео:А если переменная это объект?
С объектами чуть хитрее. Смотри пример: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
| Локальная переменная | Поле класса | |
|---|---|---|
| Где объявляется | Внутри метода или блока кода | В классе, но не внутри метода |
| Значение по умолчанию | Нет | Есть: 0, false, null |
| Если не инициализировать | Ошибка компиляции, программа даже не запустится | Код работает, поле отдает значение по умолчанию |
| Когда появляется | Когда выполнение доходит до объявления в методе | Обычное вместе с объектом, статическое при загрузке класса |
| Когда исчезает | Когда метод завершился и его фрейм удален из стека | Обычное вместе с объектом, когда до него доберется сборщик мусора |
| Где лежит значение | В кадре стека потока | Внутри объекта, то есть в куче |
Где живут статические переменные
Статические переменные принадлежат самому классу, а не объектам. А вот лежат они не там, где обычно думают. До Java 8 в области PermGen хранилось все сразу: и метаданные классов, и статические поля, и интернированные строки. В Java 8 PermGen убрали (JEP 122), и содержимое разъехалось в разные стороны. Метаданные классов ушли в Metaspace, это Non-Heap память. А значения статических полей и интернированные строки переехали в кучу. В самом JEP это сказано дословно: «The proposed implementation will allocate class meta-data in native memory and move interned Strings and class statics to the Java heap». В HotSpot статические поля лежат внутри объекта-зеркала java.lang.Class, а он такой же обычный объект в куче, как и все остальные.Порядок имеет значение
Нельзя использовать переменную до ее объявления. Вот такой код не скомпилируется: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. Строго говоря, это не ключевое слово: JEP 286, который его и принес, формулирует так, «the identifier var is not a keyword; instead it is a reserved type name». На практике разница одна: переменную или метод можно и дальше называть var, старый код не сломается. Оно позволяет не писать тип явно, компилятор сам разберется:var number = 10; // Это int
var text = "Hello"; // Это String
var list = new ArrayList<String>(); // Компилятор видит тип
Удобно, правда? Но есть ограничения. var работает только для локальных переменных, и только если ты сразу даешь значение. Нельзя так:
var x; // Ошибка: а какой тип-то?
var y = null; // Ошибка: компилятор не может понять тип
