Хотя наследование очень интересный и эффективный механизм, но в некоторых ситуациях его применение может быть нежелательным. И в этом случае можно запретить наследование с помощью ключевого слова final. Например:
final class Person { }
Если бы класс Person был бы определен таким образом, то следующий код был бы ошибочным и не сработал, так как мы тем самым запретили наследование:
class Employee extends Person{ } // ! Ошибка - унаследовать от класса Person нельзя
Кроме запрета наследования можно также запретить переопределение отдельных методов. Например, в примере выше переопределен метод print(),
запретим его переопределение:
class Person {
private String name;
Person(String name){ this.name = name; }
// запрет переопределения
final void print(){
System.out.println("Name: " + name);
}
}
В этом случае класс-наследник не сможет переопределить метод print:
class Employee extends Person{
private String company;
Employee(String name, String company){
super(name);
this.company = company;
}
// ! Ошибка - переопределить метод нельзя
void print(){
super.print();
System.out.println("Company: " + company);
}
}
Запрет наследования класса или переопределения метода с помощью final может быть полезным, если мы хотим гарантировать, что семантика класса или метода не будет изменена в производном
классе, то есть класс или метод будет действовать ровно так, как он был изначально определен. Поскольку если внутри базового класса вызываются его методы, то переопределение этих методов в производном классе может нарушить стандартную и ожидаемую работу этого класса.
Рассмотрим небольшой пример:
public class Program{
public static void main (String args[]){
Employee bob = new Employee("Bob", "Google");
}
}
class Person {
private String name;
Person(String name){
this.name = name;
print();
}
void print(){
System.out.println("Name: " + name);
}
}
class Employee extends Person{
private String company;
Employee(String name, String company){
super(name);
this.company = company;
}
@Override
void print(){
super.print();
System.out.println("Company: " + company);
}
}
В конструкторе класса Person вызывается метод print() для вывода информации о созданном объекте. Но в классе Employee переопределяем этот метод, чтобы также вывести значение поля company.
Поскольку в конструкторе Employee мы уже вызываем конструктор базового класса Person, нам не нужно второй раз вызывать метод print:
Employee(String name, String company){
super(name); // здесь вызывается метод print
this.company = company;
}
Однако посмотрим на консольный вывод программы:
Name: Bob Company: null
Конструктор базового класса Person, где вызывается метод print, отработал ДО установки поля company. В итоге при выводе на консоль выводится некорректная информация.
Вызов метода базового класса в конструкторе - это одна из распространенных причин запрета переопределения метода. И в этом случае метод print в классе Person мы можем определить как final:
public class Program{
public static void main (String args[]){
Employee bob = new Employee("Bob", "Google");
}
}
class Person {
private String name;
Person(String name){
this.name = name;
printPerson();
}
// запрет переопределения
final void printPerson(){
System.out.println("Name: " + name);
}
}
class Employee extends Person{
private String company;
Employee(String name, String company){
super(name); // в этой точке в базовом классе вызывается метод printPerson
this.company = company;
printEmployee();
}
void printEmployee(){
System.out.println("Company: " + company);
}
}
В этом случае мы получим корректный вывод:
Name: Bob Company: Google
Стоит отметить, что проблема выше была связана прежде всего с тем, что вызов конструктора базового класса выполнялся до остального кода (установки поля company). И в до версии Java 25 мы могли вызывать конструктор базового класса только в самом начале конструктоора производного класса до всего остального кода:
Employee(String name, String company){
super(name);
this.company = company;
}
Однако начиная с Java 25 можно вызывать конструктор базового класса в любом месте конструктора производного класса:
Employee(String name, String company){
this.company = company;
super(name);
}
Конкретно эта деталь в данном случае бы решила проблему, тем не менее в более сложных программах нам надо учитывать все возможные зависимости, которые могут повлять на создание объекта базового класса.
Поэтому в конструкторе лучше вызывать только методы с модификаторами final или private.