final, static и когда их использовать
Три слова, три совершенно разных смысла в зависимости от того, к чему их приставить, — и почти каждое собеседование проверяет, не путаете ли вы их между собой.
Вопрос с собеседования: «Что значит final для переменной, метода и класса — это одно и то же? И почему static-поля называют частой причиной утечек памяти, если Java вроде бы сама убирает мусор?»
Слово одно, а смысл каждый раз новый — это и есть первая ловушка вопроса. Разберём по порядку: сначала final, потом static, а в конце — почему их сочетание требует особой осторожности.
Что выведет этот код?
import java.util.ArrayList;
import java.util.List;
final class Config { // final класс — от него нельзя унаследоваться
final int maxUsers = 100; // final поле — переприсвоить нельзя
final List<String> roles = new ArrayList<>(); // final ссылка, но не final содержимое!
final void printInfo() { // final метод — переопределить нельзя
System.out.println("Лимит: " + maxUsers + ", ролей: " + roles.size());
}
}
public class Main {
public static void main(String[] args) {
Config config = new Config();
config.roles.add("admin"); // разрешено — меняем содержимое списка
config.roles.add("editor");
config.printInfo();
}
}Вывод:
Лимит: 100, ролей: 2Казалось бы, всё в классе Config помечено final — но список roles прекрасно пополняется. Здесь и кроется главная путаница вокруг final: оно запрещает не «менять данные», а именно переприсваивание.
Как это работает под капотом: final для переменной, метода, класса
final-переменная (поле или локальная переменная). Запрещает переприсвоить саму переменную после инициализации — то есть поменять, на что она указывает (для ссылочных типов) или какое значение хранит (для примитивов). Если переменная ссылочная, как roles в примере выше, final фиксирует именно ссылку на объект в памяти, а не содержимое этого объекта — сам список можно менять сколько угодно, добавлять и удалять элементы. Присвоить roles новый список (roles = new ArrayList<>()) уже не получится — это ошибка компиляции.
final-метод. Запрещает подклассам переопределять этот метод. Используется, когда логика метода критична для корректности класса и переопределение в наследнике может её сломать — например, метод внутри конструктора, вызывающий другие методы объекта.
final-класс. Запрещает наследоваться от этого класса вообще — ни один подкласс создать нельзя. Самый известный пример из стандартной библиотеки — класс String: он final, поэтому вы физически не можете написать class MyString extends String. Это гарантия неизменности и предсказуемости поведения строк во всей Java.
static: одна копия на весь класс, а не на каждый объект
Если обычное (не static) поле принадлежит каждому конкретному объекту — у каждого своя копия, — то static-поле принадлежит самому классу и существует в единственном экземпляре, общем для всех объектов этого класса.
class Counter {
static int totalCreated = 0; // одно поле на ВСЕ объекты класса
int id; // а это поле — у каждого объекта своё
Counter() {
totalCreated++; // общий счётчик растёт при каждом создании
id = totalCreated;
}
}
public class Main {
public static void main(String[] args) {
Counter a = new Counter();
Counter b = new Counter();
Counter c = new Counter();
System.out.println("id объекта a: " + a.id);
System.out.println("id объекта c: " + c.id);
System.out.println("Всего создано: " + Counter.totalCreated); // обращаемся через имя класса
}
}Вывод:
id объекта a: 1
id объекта c: 3
Всего создано: 3Поле id у каждого объекта своё (1, 2, 3), а totalCreated — общий счётчик, который видят и меняют все три объекта одновременно. Обратите внимание: обращаться к static-полю принято через имя класса (Counter.totalCreated), а не через конкретный объект — хотя технически a.totalCreated тоже сработает, это сбивает с толку и считается плохим стилем.
Почему static — частая причина утечек памяти
Сборщик мусора Java удаляет из памяти только те объекты, на которые больше никто не ссылается. Проблема static-полей в том, что они живут столько же, сколько живёт сам класс, — то есть практически всё время работы программы. Если static-поле — это коллекция, в которую вы добавляете объекты и забываете их удалять, эта коллекция будет держать ссылки на объекты вечно, даже если больше нигде в программе они не используются.
import java.util.ArrayList;
import java.util.List;
class Session {
// ОПАСНО: static-список растёт всю жизнь программы, если не чистить
static List<byte[]> cachedData = new ArrayList<>();
static void addSession(byte[] data) {
cachedData.add(data); // объекты никогда не удаляются отсюда
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
byte[] data = new byte[1024]; // локальная переменная должна была бы "умереть"
Session.addSession(data); // но её держит static-список — сборщик мусора не тронет
}
System.out.println("Сессий в static-кеше: " + Session.cachedData.size());
}
}Вывод:
Сессий в static-кеше: 3Локальные переменные data внутри цикла обычно живут только до конца итерации, а затем сборщик мусора должен был бы их удалить. Но раз на них ссылается static-поле cachedData, живущее до конца работы программы, — сборщик мусора не может их тронуть. Это классическая утечка памяти: в долго работающем сервере (веб-приложение, которое не перезапускается неделями) такой список будет расти без остановки, пока не закончится память.
Частые ошибки на собеседовании
- Думать, что final делает объект неизменяемым целиком. Как показал первый пример,
finalна ссылке не мешает менять содержимое объекта, на который она указывает — фиксируется только сама ссылка. - Забывать, что static-методы не видят this и обычные поля напрямую. Раз static-метод принадлежит классу, а не объекту, у него просто нет доступа к нестатическим (принадлежащим конкретному объекту) полям без явного указания объекта.
- Не приводить пример утечки памяти через static. Мало сказать «static-поля живут долго» — хороший ответ объясняет механизм: static-коллекция держит ссылки, из-за чего сборщик мусора не может освободить память под объекты, которые давно никому не нужны.
- Путать final-переменную класса с константой в других языках. В Java «настоящая» константа — это обычно комбинация
static final(одно значение на класс, и оно неизменяемо), простоfinalбезstatic— это лишь неизменяемое поле каждого отдельного объекта.
Итоги-шпаргалка
final-переменная — нельзя переприсвоить (для ссылки — саму ссылку, а не содержимое объекта).final-метод — нельзя переопределить в подклассе.final-класс — нельзя унаследовать вообще (пример:String).static-поле — одна копия на весь класс, общая для всех объектов; живёт, пока жив класс — то есть почти всю работу программы.- Static-коллекции, в которые добавляют объекты и не чистят их, — классический источник утечек памяти: сборщик мусора не может освободить то, на что всё ещё есть ссылка.