ООП ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-08-08

Ключевое слово final в Java

Этот код компилируется и работает, хотя список объявлен как final:

final List<String> names = new ArrayList<>();
names.add("Anna");
names.add("Boris");
System.out.println(names); // [Anna, Boris]

Именно здесь чаще всего спотыкаются: final запрещает присвоить переменной другой объект, но не запрещает менять сам объект. Разберём, что модификатор final делает с переменными, методами и классами — и чего он не делает.

1. Что такое final в Java

final — это модификатор в Java, который запрещает дальнейшее изменение того, к чему он применён: переменной нельзя присвоить новое значение, метод нельзя переопределить, а от класса нельзя унаследоваться. Слово final переводится как «завершённый», поэтому такие переменные, методы и классы называют завершёнными.

Модификатор final не относится к модификаторам доступа: он свободно сочетается с public, private, static и ставится перед типом.

Где применяется final Что запрещает Пример
Локальная переменная Повторное присваивание final int x = 5;
Параметр метода Изменение параметра внутри метода void print(final double d)
Поле экземпляра Изменение после инициализации (объявление, конструктор или блок инициализации) private final String name;
Статическое поле (константа) Изменение после инициализации (объявление или статический блок) public static final int MAX_WEIGHT = 100;
Метод Переопределение в классе-наследнике public final void print()
Класс Наследование, то есть создание подклассов public final class Money

2. final-переменные: правила инициализации

Переменная, объявленная как final, получает значение ровно один раз. Попытка присвоить ей значение повторно — ошибка компиляции, а не ошибка времени выполнения.

Правила инициализации зависят от вида переменной:

  • Поле экземпляра (final без static) — инициализируется при объявлении, в блоке инициализации или в конструкторе. Если поле не инициализировано при объявлении, оно называется blank final и должно быть присвоено ровно один раз в каждом конструкторе класса.
  • Статическое поле (static final) — инициализируется при объявлении или в статическом блоке инициализации.
  • Локальная переменная — может быть объявлена без значения и присвоена позже, но только один раз.
  • Параметр метода — значение приходит при вызове, изменить его внутри метода нельзя.

Если правило нарушено, компилятор выдаёт ошибку: variable might not have been initialized или cannot assign a value to final variable.

Следующий пример показывает разные варианты объявления завершённых переменных — закомментированные строки не скомпилируются:

public class FinalVariablesExample {
    public static final int FILE_NEW = 1;          // static final: константа класса
    private final String someString = "something"; // final String: поле экземпляра

    public static void print(final double d) {     // final double: параметр метода
        //  FILE_NEW = 2;   // ошибка: cannot assign a value to final variable FILE_NEW
        final String str;   // локальная final-переменная без значения
        str = "someString"; // единственное присваивание - разрешено
        //  str = "";       // ошибка: variable str might already have been assigned
        //  d = 4;          // ошибка: final parameter d may not be assigned
        System.out.println("FILE_NEW = " + FILE_NEW);
        System.out.println("str = " + str);
        System.out.println("d = " + d);
    }

    public static void main(String[] args) {
        print(3);
    }
}

Типичное практическое применение blank final — неизменяемые поля объекта, которые задаются в конструкторе:

public class User {
    private final String name; // blank final
    private final int id;

    public User(String name, int id) {
        this.name = name; // присваиваем ровно один раз
        this.id = id;
    }

    public String getName() {
        return name;
    }
    // сеттера для name быть не может: повторное присваивание не скомпилируется
}

3. final и ссылка на объект

Для ссылочных типов final замораживает ссылку, а не состояние объекта. Переменная навсегда указывает на один и тот же объект, но сам объект может меняться, если он изменяемый:

final List<String> names = new ArrayList<>();
names.add("Anna");           // OK: меняем состояние объекта
names.clear();               // OK
// names = new ArrayList<>(); // ошибка: cannot assign a value to final variable names

final int[] numbers = {1, 2, 3};
numbers[0] = 42;             // OK: элементы массива не защищены
// numbers = new int[5];     // ошибка компиляции

С String путаницы обычно не возникает, потому что String и так неизменяемый (immutable): любой метод возвращает новую строку, не трогая исходную.

final String greeting = "Hello";
// greeting = "Hi";                      // ошибка компиляции
String upper = greeting.toUpperCase();   // OK: создаётся новая строка
System.out.println(greeting);            // Hello - исходная строка не изменилась

Важно

final сам по себе не делает объект неизменяемым. Чтобы получить immutable-класс, нужны final-поля, отсутствие сеттеров, защитные копии изменяемых полей и запрет наследования (final-класс или record). Бонус: значения final-полей, записанные в конструкторе, корректно видны другим потокам после публикации объекта — это гарантия модели памяти Java.

4. Константы Java: public static final

Константа в Java — это поле класса, объявленное как public static final: одно значение на весь класс, доступное без создания объекта и защищённое от изменения. Имена констант пишут заглавными буквами, а слова разделяют подчёркиванием: MAX_WEIGHT, DEFAULT_TIMEOUT.

Константы помогают избавиться от магических чисел — чисел или строк в коде, смысл которых непонятен без комментария. Например, здесь магическое число 9.81 повторяется трижды:

public class PhysicsMagicNumber {
    public static double potentialEnergy(double mass, double height) {
        return mass * height * 9.81;
    }

    public static double getVelocity(double time) {
        return time * 9.81;
    }

    public static double getDistance(double time) {
        return 9.81 * time * time / 2;
    }
}

Перепишем код, введя константу ACCELERATION типа final double. Что это даёт: во-первых, имя константы объясняет смысл числа; во-вторых, значение задано в одном месте, и изменить его достаточно один раз. После рефакторинга:

public class Physics {
    public static final double ACCELERATION = 9.81;

    public static double potentialEnergy(double mass, double height) {
        return mass * height * ACCELERATION;
    }

    public static double getVelocity(double time) {
        return time * ACCELERATION;
    }

    public static double getDistance(double time) {
        return ACCELERATION * time * time / 2;
    }
}

Неочевидный момент

Константы примитивных типов и String, инициализированные литералом, компилятор считает compile-time constants и подставляет их значение прямо в байт-код вызывающего класса. Поэтому если поменять значение такой константы в библиотеке и пересобрать только её, старый скомпилированный код продолжит использовать прежнее значение — зависимые классы нужно перекомпилировать.

5. final-методы: запрет переопределения

Чтобы запретить переопределение метода в классах-наследниках, перед его объявлением ставят final. Такие методы называют завершёнными. Их можно наследовать и вызывать, но нельзя заменить своей реализацией.

Класс Report содержит завершённый метод, который нельзя переопределить в классе-наследнике SalesReport — при попытке возникнет ошибка компиляции overridden method is final:

public class Report {
    public final void print() {
        System.out.println("Это завершенный метод");
    }
}
public class SalesReport extends Report {
    // Этот метод не может быть переопределен
    /* @Override
    public void print() {
        System.out.println("Недопустимо");
    }*/
}

Что стоит помнить о final-методах:

  • Объявлять метод private final бессмысленно: private-метод не виден наследникам и так не может быть переопределён.
  • static-методы не переопределяются, а скрываются, поэтому final у них лишь запрещает сокрытие в наследнике.
  • Конструктор нельзя объявить как final — он и так не наследуется.
  • Сочетание abstract final для метода невозможно: abstract требует реализации в наследнике, а final её запрещает.
  • Перегрузку (overload) final не запрещает — только переопределение (override).

6. final-классы: запрет наследования

Чтобы предотвратить наследование класса, перед объявлением класса ставят final. Все методы такого класса автоматически становятся завершёнными, ведь подклассов, где их можно было бы переопределить, просто не будет. Объявить класс одновременно abstract и final нельзя: абстрактный класс существует только ради наследников.

final class A {
    //...
}

class B extends A { // ОШИБКА! Класс A не может иметь подклассы
    //...
}

В стандартной библиотеке final-классов много: String, Integer и другие обёртки примитивов, LocalDate, Math. Их сделали завершёнными, чтобы никто не сломал неизменяемость или контракт equals()/hashCode() в подклассе. Компоненты record тоже неявно являются final, а сам record нельзя наследовать.

Совет

Раньше final-классы были головной болью при тестировании: обычный Mockito не умел их мокать. Начиная с Mockito 5 inline mock maker включён по умолчанию, поэтому final-классы и final-методы мокаются без дополнительной настройки. Если нужно только ограничить список наследников, а не запретить их полностью, посмотрите на sealed-классы (Java 17+).

7. Параметры, локальные переменные и effectively final

final у параметра метода или локальной переменной ничего не меняет для вызывающего кода — это защита от случайного переприсваивания внутри метода и подсказка читателю, что значение дальше не меняется.

Начиная с Java 8 появилось понятие effectively final (фактически финальная переменная): так называют локальную переменную или параметр, которым после инициализации ни разу не присваивают новое значение, даже если слово final не написано. Лямбда-выражения и анонимные классы могут захватывать только final или effectively final переменные:

public void printTotal(List<String> items) {
    int total = items.size();  // effectively final: больше не присваивается

    Runnable task = () -> System.out.println("Всего: " + total); // OK
    task.run();

    // total++; // если раскомментировать, лямбда перестанет компилироваться:
    //          // local variables referenced from a lambda expression
    //          // must be final or effectively final
}

Это одна из самых частых ошибок при переходе на лямбды: счётчик в цикле менять нельзя, вместо него используют массив из одного элемента, AtomicInteger или средства Stream API.

8. final, finally и finalize — не путать

Три похожих слова относятся к совершенно разным темам. Вопрос об их отличиях — классика собеседований:

Слово Что это Назначение
final Модификатор Запрещает изменение переменной, переопределение метода, наследование класса
finally Блок конструкции try-catch-finally Выполняется всегда: и когда исключение было, и когда его не было. Освобождение ресурсов
finalize() Метод класса Object Устаревший хук перед сборкой мусора. Объявлен deprecated в Java 9 и помечен к удалению в Java 18 — полагаться на него нельзя

9. Кратко: что запомнить

  • final — модификатор со значением «завершённый»: применяется к переменным, методам и классам.
  • final-переменной значение присваивается ровно один раз; нарушение — ошибка компиляции.
  • Для ссылочных типов замораживается ссылка, а не объект: элементы final-массива или final-коллекции менять можно.
  • Константа — это public static final поле с именем в стиле MAX_WEIGHT; константы вытесняют магические числа.
  • final-метод нельзя переопределить, final-класс нельзя унаследовать; сочетания abstract final и final-конструктор запрещены.
  • Лямбды и анонимные классы захватывают только final или effectively final переменные.

Часто задаваемые вопросы

Ускоряет ли final работу программы?

Заметного выигрыша ждать не стоит. Современная JVM и так определяет во время выполнения, что метод нигде не переопределён, и встраивает его. Реальный эффект даёт только подстановка compile-time констант (static final примитивов и строк). Ставьте final ради читаемости и безопасности проектирования, а не ради скорости.

Как сделать коллекцию по-настоящему неизменяемой, если final не помогает?

Используйте фабрики List.of(), Set.of(), Map.of() (Java 9+) или обёртки Collections.unmodifiableList(). Они возвращают коллекцию, которая бросает UnsupportedOperationException при попытке изменения. Комбинация private final List<String> items = List.of(...) защищает и ссылку, и содержимое.

Можно ли объявить метод одновременно abstract и final?

Нет, это ошибка компиляции. abstract означает, что реализацию обязан дать наследник, а final запрещает переопределение — требования взаимоисключающие. По той же причине нельзя объявить класс как abstract final.

Нужно ли писать final у параметров и локальных переменных?

Это вопрос стиля команды. Плюс: сразу видно, что значение не меняется, и случайное переприсваивание не пройдёт компиляцию. Минус: код становится многословнее. Компромисс, который используют чаще всего, — обязательный final для полей класса и необязательный для параметров, потому что для лямбд достаточно effectively final.

Почему в Java нет ключевого слова const?

Слово const зарезервировано в Java, но не используется: назвать так переменную нельзя, а объявить константу через него невозможно. Роль константы выполняет static final поле. Для локальных значений внутри метода применяют final.

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

unknown Aug 8, 2024
Конструктор может быть приватным. Как вариант - для реализации шаблона проектирования Singleton.
unknown Aug 8, 2024
Прошу прощения, перепутал final c private)

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.