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
, не могут быть переопределены. Когда мы разрабатываем класс и чувствуем, что метод не следует переопределять, мы можем сделать этот метод final
. Мы также можем найти много методов final в основных библиотеках Java.
Иногда нам не нужно полностью запрещать расширение класса, а нужно только предотвратить переопределение некоторых методов. Хорошим примером этого является класс Thread .
Его можно расширить и, таким образом, создать собственный класс потока. Но его методы isAlive()
являются окончательными
.
Этот метод проверяет, жив ли поток. Правильно переопределить метод isAlive()
невозможно по многим причинам. Одним из них является то, что этот метод является нативным. Собственный код реализован на другом языке программирования и часто специфичен для операционной системы и аппаратного обеспечения, на котором он работает.
Давайте создадим класс Dog
и сделаем его метод sound()
final
:
public class Dog {
public final void sound() {
// ...
}
}
Теперь давайте расширим класс Dog
и попробуем переопределить его метод sound() :
public class BlackDog extends Dog {
public void sound() {
}
}
Мы увидим ошибку компилятора:
- overrides
com.foreach.finalkeyword.Dog.sound
- Cannot override the final method from Dog
sound() method is final and can’t be overridden
Если некоторые методы нашего класса вызываются другими методами, нам следует подумать о том, чтобы сделать вызываемые методы окончательными
. В противном случае их переопределение может повлиять на работу вызывающих абонентов и привести к неожиданным результатам.
Если наш конструктор вызывает другие методы, мы обычно должны объявлять эти методы окончательными
по указанной выше причине.
В чем разница между созданием всех методов класса final
и маркировкой самого класса final
? В первом случае мы можем расширить класс и добавить в него новые методы.
Во втором случае мы не можем этого сделать.
4. Окончательные
переменные
Переменные, помеченные как окончательные
, не могут быть переназначены. После инициализации final
переменная не может быть изменена.
4.1. Окончательные
примитивные переменные
Давайте объявим примитивную конечную
переменную 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 , это означает, что мы можем их инициализировать:
- по заявлению
- в блоке инициализатора экземпляра
- в конструкторе
В противном случае компилятор выдаст нам ошибку.
4.4. Заключительные
аргументы
Ключевое слово 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
5. Вывод
В этой статье мы узнали, что означает ключевое слово final
для классов, методов и переменных. Хотя мы не можем часто использовать ключевое слово final
в нашем внутреннем коде, это может быть хорошим дизайнерским решением.
Как всегда, полный код для этой статьи можно найти в проекте GitHub .