«1. Обзор

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

В этом уроке мы рассмотрим, что означает ключевое слово final для классов, методов и переменных.

2. Финальные классы

Классы, отмеченные как финальные, не могут быть продлены. Если мы посмотрим на код базовых библиотек Java, то обнаружим там множество финальных классов. Одним из примеров является класс String.

Рассмотрим ситуацию, когда мы можем расширить класс String, переопределить любой из его методов и заменить все экземпляры String экземплярами нашего конкретного подкласса String.

Тогда результат операций над объектами String станет непредсказуемым. А учитывая, что класс String используется повсеместно, это неприемлемо. Вот почему класс String помечен как final.

Любая попытка наследования от конечного класса вызовет ошибку компилятора. Чтобы продемонстрировать это, давайте создадим окончательный класс Cat:

public final class Cat {

    private int weight;

    // standard getter and setter
}

И попробуем его расширить:

public class BlackCat extends Cat {
}

Мы увидим ошибку компилятора:

The type BlackCat cannot subclass the final class Cat

~~ ~ Обратите внимание, что ключевое слово final в объявлении класса не означает, что объекты этого класса неизменны. Мы можем свободно изменять поля объекта Cat:

Cat cat = new Cat();
cat.setWeight(1);

assertEquals(1, cat.getWeight());

Мы просто не можем расширять его.

Если мы строго следуем правилам хорошего дизайна, мы должны тщательно создать и задокументировать класс или объявить его окончательным из соображений безопасности. Однако мы должны соблюдать осторожность при создании финальных классов.

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

Если класс окончательный, мы не можем расширить его, чтобы переопределить метод и решить проблему. Другими словами, мы теряем расширяемость — одно из преимуществ объектно-ориентированного программирования.

3. Окончательные методы

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

Иногда нам не нужно полностью запрещать расширение класса, а нужно лишь предотвратить переопределение некоторых методов. Хорошим примером этого является класс Thread. Его можно расширить и, таким образом, создать собственный класс потока. Но его методы isAlive() являются окончательными.

Этот метод проверяет, жив ли поток. Правильно переопределить метод isAlive() невозможно по многим причинам. Одним из них является то, что этот метод является нативным. Собственный код реализован на другом языке программирования и часто специфичен для операционной системы и аппаратного обеспечения, на котором он работает.

Давайте создадим класс Dog и сделаем его метод sound() окончательным:

public class Dog {
    public final void sound() {
        // ...
    }
}

Теперь давайте расширим класс Dog и попробуем переопределить его метод sound():

public class BlackDog extends Dog {
    public void sound() {
    }
}

~~ ~ Мы увидим ошибку компилятора:

- overrides
com.baeldung.finalkeyword.Dog.sound
- Cannot override the final method from Dog
sound() method is final and can’t be overridden

Если некоторые методы нашего класса вызываются другими методами, нам следует подумать о том, чтобы сделать вызываемые методы окончательными. В противном случае их переопределение может повлиять на работу вызывающих абонентов и привести к неожиданным результатам.

Если наш конструктор вызывает другие методы, мы обычно должны объявить эти методы окончательными по вышеуказанной причине.

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

Во втором случае мы не можем этого сделать.

4. Окончательные переменные

Переменные, помеченные как окончательные, не могут быть переназначены. Как только конечная переменная инициализирована, ее нельзя изменить.

4.1. Final Primitive Variables

Давайте объявим примитивную final переменную i, затем присвоим ей 1.

И попробуем присвоить ему значение 2:

public void whenFinalVariableAssign_thenOnlyOnce() {
    final int i = 1;
    //...
    i=2;
}

Компилятор говорит:

The final local variable i may already have been assigned

4.2. Окончательные ссылочные переменные

Если у нас есть конечная ссылочная переменная, мы также не можем переназначить ее. Но это не означает, что объект, на который он ссылается, неизменен. Мы можем свободно изменять свойства этого объекта.

«Чтобы продемонстрировать это, давайте объявим конечную ссылочную переменную cat и инициализируем ее:

final Cat cat = new Cat();

Если мы попытаемся переназначить ее, мы увидим ошибку компилятора:

The final local variable cat cannot be assigned. It must be blank and not using a compound assignment

Но мы можем изменить свойства экземпляра Cat:

cat.setWeight(5);

assertEquals(5, cat.getWeight());

4.3. Конечные поля

Конечные поля могут быть либо константами, либо однократно записываемыми полями. Чтобы различить их, мы должны задать вопрос — включили бы мы это поле, если бы нам пришлось сериализовать объект? Если нет, то это не часть объекта, а константа.

Обратите внимание, что согласно соглашению об именах, константы класса должны быть в верхнем регистре, а компоненты должны быть разделены символами подчеркивания (\»_\»):

static final int MAX_WIDTH = 999;

Обратите внимание, что любое финальное поле должно быть инициализировано до завершения конструктора.

Для статических полей final это означает, что мы можем их инициализировать:

    при объявлении, как показано в приведенном выше примере в блоке статического инициализатора

Например, для полей final это означает, что мы можем их инициализировать: ~~ ~ при объявлении в блоке инициализатора экземпляра в конструкторе

    В противном случае компилятор выдаст нам ошибку.

4.4. Заключительные аргументы

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

Приведенное выше присваивание вызывает ошибку компилятора:

public void methodWithFinalArguments(final int x) {
    x=1;
}

5. Заключение

The final local variable x cannot be assigned. It must be blank and not using a compound assignment

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

Как всегда, полный код для этой статьи можно найти в проекте GitHub.

«