Объявление, инициализация и присваивание это три разных шага работы с переменной. Объявление сообщает компилятору имя и тип:
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 |
| Если не инициализировать |
Ошибка компиляции, программа даже не запустится |
Код работает, поле отдает значение по умолчанию |
| Когда появляется |
Когда выполнение доходит до объявления в методе |
Обычное вместе с объектом, статическое при загрузке класса |
| Когда исчезает |
Когда метод завершился и его фрейм удален из стека |
Обычное вместе с объектом, когда до него доберется сборщик мусора |
| Где лежит значение |
В кадре стека потока |
Внутри объекта, то есть в куче |
Есть важное различие между обычными и статическими полями.
Обычное поле number создается, когда создаешь объект (
new Example()).
Статическое поле count создается раньше, когда JVM загружает сам класс в память. Это происходит до создания любых объектов.
Где живут статические переменные
Статические переменные принадлежат самому классу, а не объектам. А вот лежат они не там, где обычно думают. До 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; // Ошибка: компилятор не может понять тип
Что важно запомнить
Локальные переменные надо инициализировать. Обязательно. Иначе компилятор не пропустит код.
Поля класса инициализируются автоматически: числа получают 0, boolean получает false, объекты получают null.
Примитивы живут в стеке (если это локальные переменные) или внутри объекта в куче (если это поля).
Объекты всегда создаются в куче. В стеке хранятся только ссылки на них.
Статические переменные создаются при загрузке класса, а их значения хранятся в куче, внутри объекта
java.lang.Class. В Metaspace лежат метаданные класса, но не сами значения.
Сборщик мусора сам удаляет объекты, на которые больше никто не ссылается. Не нужно делать это вручную.
С Java 10 можешь использовать
var для локальных переменных, компилятор сам определит тип.
Вообще, понимание работы с памятью в Java это база. Сначала кажется сложным, но когда разберешься, многие вещи станут логичными. Ты начнешь понимать, почему код ведет себя так, а не иначе, и где искать проблемы.
Вопросы и ответы
Чем инициализация отличается от присваивания?
Инициализация это первое значение переменной, то самое, с которым она начинает жизнь. Обычно его дают прямо в объявлении:
int number = 2. Присваивание это любая запись значения оператором =, в том числе и много раз подряд по ходу программы. То есть первое присваивание и есть инициализация, а все следующие это уже просто присваивания.
Почему локальные переменные надо инициализировать, а поля класса нет?
Так устроен язык. Спецификация Java требует, чтобы каждая локальная переменная была определенно присвоена к моменту, когда к ее значению обращаются, иначе компилятор выдает ошибку. Правило касается не только примитивов, но и ссылок. У полей класса такого требования нет: им значения проставляет сама виртуальная машина при создании объекта или при загрузке класса.
Какие значения по умолчанию получают поля класса?
Целые числа получают 0, дробные 0.0, тип
char получает символ с кодом ноль,
boolean получает
false, а любые ссылочные поля, включая
String и массивы, получают
null. Локальных переменных это не касается вообще: у них значений по умолчанию нет.
Почему нельзя использовать переменную до ее объявления?
Потому что компилятор читает класс сверху вниз и на момент, когда встречает обращение, о переменной еще ничего не знает. Строка
private static int b = a; при объявленной ниже a не скомпилируется. Лечится перестановкой: сначала объявляем то, что читаем, потом то, что читает.
Где можно и где нельзя использовать var?
Можно только для локальных переменных, у которых значение задано прямо в объявлении, а также для переменных цикла. Нельзя для полей класса, параметров метода и типа возвращаемого значения. Не получится и объявить переменную без значения (
var x;) или присвоить ей
null (
var y = null): в обоих случаях компилятору неоткуда взять тип.
Что такое null и откуда берется NullPointerException?
Null это литерал, который означает отсутствие ссылки: переменная объявлена, но ни на какой объект не указывает. Ссылочные поля класса получают
null по умолчанию. Как только у такой переменной пытаются вызвать метод или прочитать поле, виртуальная машина бросает
NullPointerException. Это самая частая ошибка у новичков, и лечится она проверкой на
null там, где значение действительно может отсутствовать.
Читайте также
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ