Возникают ситуации, когда необходимо запретить вносить изменения состояния или поведения, показать завершенность.
В чем это проявляется в Java
? Бывают ситуации, когда необходимо запретить наследование от класса, переопределение некоторых методов в классе или изменение значения переменной.
Например, объявление константы.
Ключевое слово final
означает завершенность и применимо к классам, методам и переменным.
Применение final
по отношению к классу объявляет класс завершенным, т.е запрещает дальнейшее наследование от такого класса.
public final class FileUtils {
// some code
}
// compilation error
class FileUtilsExt extends FileUtils {
}
Это может быть полезным для написания классов, содержащих только статические методы, так называемых классов-утилит.
Пример из JDK
:
public final class Math {
// code
}
Объявление класса завершенным неявно делает завершенными и все его методы.
Вопрос:
Возможно ли одновременное объявление класса как abstract
и final
?
Ответ:
Нет, такое объявление недопустимо. Это логично, так как ключевое слово abstract
говорит о том, что класс не является завершенным, в то время как final
индификатор того, что класс полностью завершен.
Применение final
по отношению к методу объявляет метод завершенным, т.е запрещает дальнейшее переопределние такого метода.
public class Person {
final void hello() {
System.out.println("Hello!");
}
// some code
}
// compilation error
class Employee extends Person {
@Override
void hello() {
System.out.println("Hello Employee!");
}
}
Это полезно, когда вы допускаете использование класса в наследовании, но конкретное поведение переопределять хотите запретить.
Не имеет смысла объявлять метод private final
так как private
метод не виден в наследниках, соответственно не может быть переопределен.
Также конструктор не может быть объявлен как final
, что в принципе логично.
Переменная может быть объявлена как final
, что позволяет предотвратить ее изменение.
Для переменных примитивного типа это означает, что однажды присвоенное значение не может быть изменено.
Для ссылочных переменных это означает, что после присвоения объекта, нельзя изменить ссылку на данный объект. Но сам объект, его состояние, изменить можно!
Переменная класса, объявленная как final
, но не являющаяся static
, должна инициализироваться при объявлении или в теле конструктора, или блоке инициализации, иначе произойдет ошибка компиляции.
public class ArrayList {
private final int size;
private final String type = "ArrayList";
private final String[] data; // compilation error
public ArrayList(int size) {
this.size = size;
}
}
Применение final
к полям класса позволяет создавать неизменяемые(immutable) объекты. Т.е объекты, внутреннее состояние которых не изменяется.
Грубо говоря, это объекты с правами только на чтение. Это очень удобно, так как позвоялет использовать объекты в разных потоках исполнения, ведь они потокобезопасны.
Вопрос:
Пусть есть класс, где все поля объявлены как final
. Как вы напишите набор getter
-ов и setter
-ов дял такого класса?
Ответ:
Если все поля объявлены как final
, то никаких setter
-ов в таком классе быть не может, так как все поля будут проинициализированы один раз при объявлении или в теле конструктора, или блоке инициализации.
Переменные, являющиеся полями класса и объявленные как static
обязаны быть проинициализированы сразу или в статическом блоке инициализации:
public class ArrayList {
static final String TYPE;
static {
type = "ArrayList";
}
}
Обратите внимание на наименование переменной - это константа. Константами в Java
принято называть public static final
переменные класса.
Про константы и их оформление можно прочесть тут.
Константы часто используются для борьбы c магическими (или волшебными) числами, то есть непонятно что означающими числами или строками. Например, следующий код содержит магическое число 9.8
(фу, как грубо):
public final class PhysicUtil {
public static double getVelocity(double time) {
return time * 9.8;
}
}
Понятно, что это константа, обозначающая ускорение свободного падения на поверхности Земли. Но понятно становится только тем, кто понимает контекст задачи и удобнее было, если бы мы константное выражение использовали с именем:
public final class PhysicUtil {
public final static double EARTH_ACCELERATION = 9.8;
public static double getVelocity(double time) {
return time * EARTH_ACCELERATION;
}
}
Подобный подход также облегчил жизнь разработчику и пользователю, если бы в классе выше было несколько методов, использующих это число.
Локальная переменная, объявленная как final
, должна быть проинициализирована до момента ее использования, иначе возникнет ошибка компиляции:
void hello() {
final int salary;
final String greeting = "Hello";
System.out.println(salary); // compilation error
System.out.println(greeting);
}
Модификатор final
делает переменную константой в локальной области видимости объявления.
void hello() {
final int salary = 80;
System.out.println(salary);
salary = 90; // compilation error
}
Также, final
может быть применен и к аргументам методов:
void hello(final salary) {
System.out.println(salary);
salary = 90; // compilation error
}
Такой подход гарантирует, что ссылка не будет изменена в теле метода.
Рекомендация по использованию final
схожа с советом по использованию private
: все, что можно, делайте final
.
Если объект планируется использовать в разных потоках исполнения, то сделайте его неизменяемым, объявив все поля final
.
Если класс не должен участвовать в наследовании, то лишите его этой возможности. Контролируйте то, что можно переопределять, а что нельзя.
Мой совет: старайтесь и ссылки в методе, и переменные в теле метода также объявлять как final
. Это может спасти вас от случайного присваивания ссылке не того значения.
Хотя некоторые считают такое повсеместное использование неправильным, так как такое обилие final
делает код более многословным и трудночитаемым, но мне кажется, что плюсов от использования больше, чем минусов от многословности.
В таких языках как Scala
рекомендуемый способ объявления переменной выглядит как val
, который как раз таки работает как объявление переменной с помощью final
. Со временм я уверен подобный способ объявления будет и в Java
.
Главное, что надо помнить - это то, что, когда вы работаете с ссылочными переменными и объявляете такую переменную как final
, то после присвоения объекта, нельзя изменить ссылку на данный объект. Но сам объект, его состояние, изменить можно!
public class StringArrayList {
final String[] arr;
public StringArrayList() {
this.arr = new String[10];
arr[0] = "World";
}
}
public class Main {
public static void main() {
StringArrayList list = new StringArrayList();
System.out.println(StringArrayList.arr[0]); // print "World"
// modify array
list.arr[0] = "Hello"
System.out.println(StringArrayList.arr[0]); // print "Hello"
list.arr = new String[15]; // compilation error
}
}
Если вы разберете пример выше, то увидите, что если вы объявили ссылку на изменяемый объект как final
, то изменить значение ссылки нельзя - компилятор защищает эту ссылку, ведь вы объявили ее финальной. Но сам объект(если до него есть доступ) изменить можно!
Это как если вы прикрутили намертво полку к стене. Саму полку уже не изменить и не снять, но вещи оттуда взять или положить можно. Разумеется, если прав доступа до такой полки хватит!