Ключевое слово final в Java - Вопросы

Всего: 6 вопросов

1. 

Что такое ключевое слово final в Java и к чему его можно применять?

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

к переменной (локальной, параметру, полю экземпляра, статическому полю) — значение присваивается ровно один раз, повторное присваивание не компилируется;
к методу — метод нельзя переопределить в классе-наследнике;
к классу — от класса нельзя унаследоваться.

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

final int x = 5;                              // локальная переменная
void print(final double d) { }                // параметр метода
private final String name;                    // blank final: поле экземпляра
public static final int MAX_WEIGHT = 100;     // константа
public final void print() { }                 // final-метод
public final class Money { }                  // final-класс

Поле, объявленное как final, но не инициализированное при объявлении, называется blank final: оно должно быть присвоено ровно один раз в каждом конструкторе класса (или в блоке инициализации). Статическое static final поле инициализируется при объявлении или в статическом блоке инициализации. Нарушение правил даёт ошибку компиляции variable might not have been initialized или cannot assign a value to final variable.

2. 

Почему в коллекцию, объявленную как final, всё равно можно добавлять элементы?

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

Потому что для ссылочных типов 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 и так неизменяемый, любой его метод возвращает новую строку, а исходная остаётся прежней.

Вывод: final сам по себе не делает объект неизменяемым. Чтобы получить настоящий immutable-объект, нужны final-поля, отсутствие сеттеров, защитные копии изменяемых полей и запрет наследования (final-класс или record). А для по-настоящему неизменяемой коллекции используют List.of(), Set.of(), Map.of() (Java 9+) или Collections.unmodifiableList().

3. 

Что такое константа в Java и почему её объявляют как public static final?

Константа в Java — это поле класса, объявленное как public static final. Каждое слово в этой тройке отвечает за своё:

public — значение доступно из любого места;
static — одно значение на весь класс, объект создавать не нужно;
final — значение задаётся один раз и не может быть изменено.

Имена констант пишут заглавными буквами, слова разделяют подчёркиванием: MAX_WEIGHT, DEFAULT_TIMEOUT. Главная польза констант — избавление от магических чисел, смысл которых непонятен без комментария:

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;
    }
}

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

4. 

Что запрещает final у метода и что — у класса?

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

public class Report {
    public final void print() {
        System.out.println("Это завершенный метод");
    }
}

public class SalesReport extends Report {
    /* @Override
    public void print() { }   // ошибка компиляции */
}

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

Что ещё стоит помнить:

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

5. 

Что такое effectively final переменная и почему лямбда-выражение не компилируется, если переменную менять?

Effectively final (фактически финальная) — это локальная переменная или параметр, которым после инициализации ни разу не присваивают новое значение, даже если слово final не написано. Понятие появилось в Java 8.

Лямбда-выражения и анонимные классы могут захватывать только 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 вроде count() и reduce(), которые возвращают результат, а не меняют переменную.

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

6. 

В чём разница между final, finally и finalize()?

Три похожих слова относятся к совершенно разным темам языка:

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

final int x = 5;            // модификатор: x больше не изменить

try {
    readFile();
} finally {
    closeFile();            // блок: выполнится в любом случае
}

Вместо finalize() сегодня применяют try-with-resources и java.lang.ref.Cleaner. Вопрос об отличиях этих трёх слов — классика собеседований по Java.

Страница 1 из 1