W3docs

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

Делайте переменные константами, запрещайте переопределение методов и наследование классов с помощью ключевого слова final в Java.

final означает «задай один раз, затем не меняй». Оно применяется к четырём вещам, и эффект зависит от того, к чему именно применяется:

  • К переменной — связь не может быть переназначена.
  • К методу — метод не может быть переопределён подклассом.
  • К классу — класс не может быть расширен.
  • К параметру — параметр не может быть переназначен внутри метода.

Общий смысл — «никаких изменений после первоначального связывания». Что именно запрещается, зависит от того, что помечается.

Локальные переменные final

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

void demo() {
  final int max = 100;
  max = 50;          // ERROR: cannot assign a value to final variable max
}

Инициализировать её при объявлении необязательно — первое присваивание можно отложить, главное, чтобы оно произошло ровно один раз до любого чтения:

final int x;
if (condition) x = 1;
else           x = 2;
System.out.println(x);   // ok — x was definitely assigned

final у локальной переменной — в основном инструмент документирования: он сообщает будущему читателю (и компилятору) «это больше не изменится». Захваты в лямбдах и внутренних классах требуют его: любая локальная переменная, используемая внутри, должна быть final или effectively final (ни разу не переназначенной).

Параметры final

Параметр final не может быть переназначен внутри тела метода:

void greet(final String name) {
  name = name.toUpperCase();    // ERROR
}

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

Поля final

Поле final присваивается ровно один раз — либо при объявлении, в инициализаторе экземпляра или в конструкторе — и больше никогда:

public class Point {
  private final int x, y;

  public Point(int x, int y) {
    this.x = x;        // assigned once
    this.y = y;
  }

  // no setX or setY — there's no way to change the values
}

Это основа неизменяемого класса. Каждое поле final; ничто не может изменить объект после конструирования.

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

static final — константы

Стандартная форма константы — public static final плюс имя в UPPER_SNAKE:

public static final double PI       = 3.141592653589793;
public static final int    MAX_RETRY = 3;

Статическая — потому что нет смысла держать по одной копии на экземпляр; final — потому что это константа. Для примитивов и String-констант компилятор встраивает значение на каждом месте использования. Одно следствие: если изменить значение такой константы, классы, ссылавшиеся на неё, сохраняют старое значение до перекомпиляции — литерал был встроен в их байткод в момент их собственной компиляции.

final и ссылки

Распространённое заблуждение: final замораживает ссылку, а не объект, на который она указывает. У final-массива или списка можно по-прежнему изменять содержимое:

final int[] nums = {1, 2, 3};
nums[0] = 99;        // ok — mutating the array, not reassigning the reference
nums = new int[5];   // ERROR — reassigning the reference

final List<String> names = new ArrayList<>();
names.add("Rex");    // ok — mutates the list
names = List.of();   // ERROR

Если нужен список с замороженным содержимым, оберните его в List.copyOf(...) или Collections.unmodifiableList(...).

Методы final

final у метода означает, что ни один подкласс не может его переопределить:

public class Shape {
  public final String describe() {       // can never be overridden
    return getClass().getSimpleName() + " area=" + area();
  }
  public double area() { return 0; }
}

public class Circle extends Shape {
  public String describe() { return "circle"; }   // ERROR
}

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

Классы final

final у класса означает, что ни один класс не может его расширить:

public final class Money { ... }

public class CryptoMoney extends Money { }    // ERROR

Используйте это, когда расширение разрушает дизайн. Стандартная библиотека делает это регулярно: String, Integer, Long, Double и остальные обёртки примитивов — все final. Расширение их позволило бы тайно добавить изменяемое поведение в тип, который язык считает неизменяемым.

final — это не иммутабельность

Поле final само по себе не делает объект неизменяемым; оно лишь предотвращает изменение ссылки. Настоящая иммутабельность требует:

  1. Каждое поле final.
  2. Сам класс final или построен с учётом предотвращения добавления изменяемого состояния подклассами.
  3. Никакого метода, открывающего внутренний изменяемый объект (защитные копии на выходе).
  4. Никакого метода, позволяющего вызывающей стороне изменять состояние через него.

Записи (рассматриваются в разделе records) объединяют большую часть этого автоматически.

Рабочий пример

java— editable, runs on the server

Что дальше

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

Практика

Практика
Поле объявлено как final int[] xs = {1, 2, 3}; — какое присваивание будет отклонено компилятором?
Поле объявлено как final int[] xs = {1, 2, 3}; — какое присваивание будет отклонено компилятором?
Was this page helpful?