Перейти к содержанию

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

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

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

Final классы

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

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

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

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

public final class Cat {

    private int weight;

    // стандартный геттер и сеттер
}

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

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 из соображений безопасности. Однако необходимо соблюдать осторожность при создании final классов.

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

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

Final методы

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

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

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

Создадим класс Dog и определим его метод sound() как final:

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

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

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

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

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

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

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

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

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

Final переменные

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

Final примитивные переменные

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

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

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

Компилятор выдаст следующее сообщение:

The final local variable i may already have been assigned

Final ссылочные переменные

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

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

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());

Final поля

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

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

static final int MAX_WIDTH = 999;

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

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

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

Например, поля c final означают, что можно их инициализировать:

  • при объявлении;
  • в блоке инициализатора экземпляра;
  • в конструкторе.

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

Final аргументы

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

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

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

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

Заключение

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

Полный код для этой статьи можно найти на GitHub.

Оригинал