final
final у переменной запрещает переприсваивание (для поля — это blank final, которое надо инициализировать ровно один раз в объявлении или конструкторе), у метода запрещает переопределение, у класса — наследование.
Важно: final ≠ immutable. final List<String> нельзя переприсвоить, но сам список можно менять. Неизменяемость объекта — это отдельное свойство, обеспечиваемое инкапсуляцией.
Безопасная публикация и final-поля
Публикация объекта — это передача ссылки на него туда, где его увидит другой код или поток: запись в общее поле, добавление в коллекцию, возврат из метода. В многопоточной программе тут есть тонкость: из-за кэшей процессора и переупорядочивания записей другой поток может увидеть ССЫЛКУ на объект раньше, чем ему станут видны записи полей этого объекта в конструкторе. Тогда он наблюдает «полупостроенный» объект с полями по умолчанию (null/0) — это и есть небезопасная публикация.
Для final-полей JMM даёт специальную гарантию (final field freeze): значения final-полей, записанные в конструкторе, гарантированно видны любому потоку, который получит ссылку на полностью построенный объект, — без всякой синхронизации. Поэтому неизменяемый объект (все поля final, и ссылка this не утекла наружу из конструктора) можно безопасно публиковать и делить между потоками просто так. Это и есть фундамент тезиса «immutable-объекты потокобезопасны».
Для НЕ-final полей такой гарантии нет: их безопасно публикуют через volatile-поле, запись под synchronized, потокобезопасную коллекцию или иное happens-before-ребро. Подробно про happens-before и видимость — в уроке «Java Memory Model».
static
static-члены принадлежат классу, а не экземпляру: одно поле на все объекты, метод вызывается без экземпляра. static-блок выполняется при инициализации класса.
Оправданное применение — утилиты без состояния и константы. Изменяемое static-поле — это глобальное изменяемое состояние со всеми его бедами (гонки, скрытые связи, сложность тестирования), поэтому его избегают.
Порядок инициализации
При первой активной загрузке класса один раз выполняются static-поля и static-блоки — в порядке их объявления в коде.
При создании объекта порядок такой: сначала отрабатывает конструктор суперкласса (super(...)), затем инициализаторы полей и instance-блоки в порядке объявления, и только потом — тело конструктора. Понимание этого порядка объясняет многие тонкие баги инициализации.
class A {
static int s = init("static A"); // 1) при загрузке класса
int i = init("field A"); // 3) при new, после super
A() { log("ctor A"); } // 4) тело конструктора
}
Константы времени компиляции
static final поле примитивного типа или String, инициализированное константным выражением, — это compile-time constant: компилятор подставляет его ЗНАЧЕНИЕ прямо в место использования (inlining).
Практическое следствие: если изменить такую константу в библиотеке, но не перекомпилировать клиентов, они продолжат использовать старое зашитое значение. Это нюанс версионирования API, о котором забывают.
Ловушки инициализации
Вызов переопределяемого метода из конструктора опасен: к моменту вызова поля подкласса ещё не инициализированы (конструктор подкласса не дошёл до своего тела), и метод увидит значения по умолчанию (null/0).
Другие грабли: циклические зависимости при инициализации классов (forward reference к ещё не готовому static) и публикация this из конструктора (передача недостроенного объекта наружу/в другой поток).
Как отвечать на собеседовании
Различите final для переменной/метода/класса и подчеркните final ≠ immutable (+ safe publication). Точно изложите порядок инициализации (static один раз при загрузке; для объекта: super → поля/блоки → конструктор), назовите инлайнинг compile-time констант и ловушку вызова переопределяемого метода из конструктора.