final нельзя наследовать, метод с final нельзя переопределить, а переменной с final нельзя присвоить второе значение. Тонкость ровно одна: у ссылочной переменной запрет касается самой ссылки, а не состояния объекта, на который она указывает.Кратко
finalу класса запрещает наследование: отнаследоваться от такого класса нельзя, как отStringилиInteger.finalу метода запрещает переопределение в подклассе, но не мешает ни перегрузке, ни обычному вызову.finalу переменной разрешает ровно одно присваивание: примитив после него не изменится никогда.- У ссылки
finalфиксирует только саму ссылку:finalсписок нельзя переназначить, а добавлять в него элементы можно. Это и называется нетранзитивностью. - С Java 8 есть effectively final: переменная без ключевого слова, которой присвоили значение один раз, тоже годится для лямбд и анонимных классов.
finalиabstractвместе не ставятся, а если наследование нужно разрешить только избранным классам, с Java 17 для этого естьsealed.
Final классы в Java
Для класса ключевое слово final означает, что класс не может иметь подклассов, то есть наследование запрещено... Это полезно при создании неизменяемых (immutable) объектов. Например, класс String объявлен как final.public final class String {
}
class SubString extends String { // Ошибка компиляции
}
Также следует отметить, что модификатор final нельзя применять к абстрактным классам (тем, которые имеют ключевое слово abstract), поскольку это взаимоисключающие концепции: спецификация языка запрещает такое объявление напрямую (JLS 8.1.1.2). Смежный вопрос, можно ли в final-классе объявить абстрактный метод, разобран в отдельной статье.Final методы в Java
Для final метода модификатор означает, что метод не может быть переопределен в подклассах. Это полезно, когда мы хотим предотвратить изменение исходной реализации.public class SuperClass {
public final void printReport() {
System.out.println("Отчет");
}
}
class SubClass extends SuperClass {
public void printReport() { //Ошибка компиляции
System.out.println("МойОтчет");
}
}Final переменные в Java
Для переменных примитивного типа ключевое слово final означает, что значение, однажды присвоенное, не может быть изменено. Для ссылочных переменных это означает, что после присвоения объекта вы не можете изменить ссылку на этот объект. Это важно! Ссылку нельзя изменить, но состояние объекта может быть изменено. Java 8 представила новую концепцию: effectively final (эффективно final), и она описана в спецификации языка (JLS 4.12.4). Она применяется только к переменным (включая параметры методов). Суть в том, что, несмотря на явное отсутствие ключевого слова final, значение переменной не изменяется после инициализации. Другими словами, ключевое слово final может быть применено к такой переменной без ошибки компиляции. Effectively final переменные могут использоваться внутри локальных классов (local inner classes), анонимных классов (anonymous inner classes) и потоков (Stream API).public void someMethod() {
// В примере ниже и a, и b являются effectively final, поскольку им значения присваиваются только один раз:
int a = 1;
int b;
if (a == 2) b = 3;
else b = 4;
// c НЕ является effectively final, поскольку ее значение изменяется
int c = 10;
c++;
Stream.of(1, 2).forEach(s-> System.out.println(s + a)); // OK
Stream.of(1, 2).forEach(s-> System.out.println(s + c)); // Ошибка компиляции
}
Соберем все случаи в одну таблицу, чтобы было куда подглядывать:
| Где стоит final | Что запрещено | Что по-прежнему можно |
|---|---|---|
| Класс | наследовать его (extends) |
создавать объекты и менять их состояние |
| Метод | переопределять в подклассе | перегружать и вызывать как обычно |
| Переменная примитивного типа | присвоить второе значение | читать сколько угодно раз |
| Ссылочная переменная | переназначить на другой объект | менять состояние самого объекта |
| Параметр метода | присваивать ему значение внутри метода | использовать в лямбдах и анонимных классах |
Вопрос 1: что можно сказать о массиве, который объявлен как final?
Массив является объектом, поэтому ключевое слово final означает, что как только ссылка на массив присвоена, ссылку нельзя изменить. Тем не менее, вы можете изменить состояние объекта.final int[] array = {1, 2, 3, 4, 5};
array[0] = 9; // OK, потому что мы изменяем содержимое массива: {9, 2, 3, 4, 5}
array = new int[5]; // Ошибка компиляцииВопрос 2: можно ли заменить значение объекта String, не меняя ссылку на него?
Мы знаем, что класс String неизменяем: класс объявлен как final. Строковое значение хранится в массиве, который тоже помечен ключевым словом final. Правда, тип массива со временем поменялся: до Java 9 это былchar[], а с Java 9 это byte[] плюс поле coder с кодировкой (JEP 254, Compact Strings).
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** Значение используется для хранения символов строки. */
private final byte[] value;
/** Кодировка: Latin-1 или UTF-16. */
private final byte coder;Да, можем, но с поправкой на версию Java. Главное понять, что означает коварное ключевое слово final при использовании с объектами. Значение подменяют через Reflection API, и во времена Java 8 прием выглядел так:
import java.lang.reflect.Field;
class B {
public static void main(String[] args) throws Exception {
String value = "Старое значение";
System.out.println(value);
// Получаем поле value класса String
Field field = value.getClass().getDeclaredField("value");
// Делаем его изменяемым
field.setAccessible(true);
// Устанавливаем новое значение
field.set(value, "CodeGym".toCharArray());
System.out.println(value);
/* Вывод:
* Старое значение
* CodeGym
*/
}
}Сегодня этот код не запустится, и сразу по двум причинам. Первая: поле value теперь имеет тип byte[], и массив символов ему не подходит. Вторая: с Java 16 сильная инкапсуляция модулей включена по умолчанию, а в Java 17 ключ --illegal-access убрали совсем (JEP 403), поэтому setAccessible(true) для поля из java.lang бросает InaccessibleObjectException. Добраться до внутренностей String можно только явным ключом запуска: --add-opens java.base/java.lang=ALL-UNNAMED.
А вот на собственном классе прием работает и сегодня. Javadoc Field.set() разрешает запись в final-поле, если выполнены все условия: setAccessible(true) отработал, поле нестатическое, а его класс не record и не скрытый класс.
import java.lang.reflect.Field;
class Config {
private final String url;
Config(String url) {
this.url = url;
}
public String getUrl() {
return url;
}
}
public class Main {
public static void main(String[] args) throws Exception {
Config config = new Config("https://old.example");
System.out.println(config.getUrl());
Field field = Config.class.getDeclaredField("url");
field.setAccessible(true);
field.set(config, "https://new.example");
System.out.println(config.getUrl());
/* Вывод:
* https://old.example
* https://new.example
*/
}
}Важная оговорка: так меняются только поля, которым значение присваивают в конструкторе. Если поле инициализировано литералом (private final int x = 5;), компилятор подставляет константу прямо в места чтения (JLS 13.1), и рефлексия на результат уже не влияет. Тот же javadoc честно предупреждает: менять final-поля осмысленно разве что при десериализации, в остальных случаях последствия непредсказуемы.
Обратите внимание, что если бы мы попытались таким образом изменить final переменную примитивного типа, то ничего бы не произошло. Предлагаю вам убедиться в этом: создайте Java-класс, например, с final int полем и попытайтесь изменить его значение, используя Reflection API.Повышение производительности с final
Знали ли вы, что использование final может сделать ваши программы быстрее? Да! Когда вы помечаете переменную, метод или класс как final, компилятор Java и Just-In-Time (JIT) компилятор могут принимать более умные решения.
- Встраивание методов: Если метод является
final, JIT компилятор знает, что он не будет переопределен. Это позволяет ему заменять вызовы методов на фактический код, ускоряя выполнение!
Этот механизм называется встраиванием кода (inlining). В простом случае компилятор может просто заменить вызов метода кодом его тела. Например, при использовании final-метода getName() следующие два выражения могут выполняться совершенно одинаково:System.out.println("id = " + user.name); System.out.println("id = " + user.getName());
Хотя выражения становятся равнозначны по скорости, второй вариант обладает преимуществом, так как метод getName() позволяет сделать поле name доступным только для чтения. Такую же схему оптимизации компилятор может применить и к private, и к static методам, так как они тоже не подлежат переопределению. - Лучшие оптимизации: Неизменяемые переменные помогают компилятору пропускать избыточные проверки и улучшать работу с памятью.
Довольно круто, правда? Больше скорости без дополнительных усилий!
Только ставить final ради скорости не стоит: современный JIT встраивает и обычные методы. Он опирается на анализ загруженной иерархии классов, а если позже появится класс с переопределением, просто деоптимизирует скомпилированный код и соберет его заново. В документации Oracle по оптимизациям HotSpot инлайнинг разбирается как раз на примере обычного геттера. Так что final это прежде всего про дизайн и безопасность, а выигрыш в скорости идет бонусом и ничем не гарантирован.
Повышение безопасности с final
Поговорим о безопасности. Волнует ли вас то, что хитрый код может изменить ваши переменные или переопределить критические методы? final поможет!
- Неизменяемые данные: Объявление чувствительных данных как
finalпредотвращает их переназначение. Никаких подозрительных действий! - Безопасное наследование: Помечайте критические классы или методы как
final, чтобы заблокировать подклассам возможность изменения поведения. Никто не может перехватить вашу логику!
Блокируя изменения, final добавляет слой защиты от вредоносных атак. Уже чувствуется безопаснее!
Правильная инициализация final переменных
Итак, final означает отсутствие изменений после присвоения, но когда и как вы присваиваете значение?
- Немедленное присвоение:
final int age = 30;Очень просто. - Присвоение в конструкторе: Для нестатических
finalпеременных вы можете устанавливать их в каждом конструкторе. Ни один конструктор не должен оставлять переменную неприсвоенной!class Person { final String name; Person(String name) { this.name = name; } } - Статический инициализатор: Статические
finalпеременные могут быть инициализированы в статическом блоке.static final int ID; static { ID = 1001; }
Пока значение присвоено один раз, все в порядке!
Нетранзитивность final ссылок
А вот тонкость! Объявление ссылки как final означает, что вы не можете ее переназначить, но объект, на который она указывает, все еще может изменяться. Запутались? Давайте разберем:
final List<String> fruits = new ArrayList<>();
fruits.add("Яблоко"); // Разрешено
fruits = new ArrayList<>(); // Ошибка!
Видите? Сам список fruits заблокирован на месте, но его содержимое может быть изменено. Это называется нетранзитивностью. Хотите действительно неизменяемый объект? Используйте неизменяемые классы, такие как String, или обертки вроде Collections.unmodifiableList(). Только учтите, что обертка возвращает представление, а не копию: если у кого-то остался исходный список, менять его он по-прежнему может. Для настоящей неизменяемой копии с Java 9 есть List.copyOf() и List.of().
Обратная сторона final: Ограничения и компромиссы
Употребление final в объявлении метода или класса накладывает серьезные ограничения на возможность дальнейшего использования и развития кода. Завершенная реализация: Применение final в объявлении метода это верный показатель того, что его реализация самодостаточна и полностью завершена. Ограничение для других разработчиков: Программисты, которые захотят использовать ваш класс и расширить его для своих нужд, будут стеснены в возможностях. Пометив класс как final, вы полностью запрещаете наследование и можете существенно снизить его практическую ценность для других. Прежде чем использовать final, всегда стоит подумать, оправданы ли такие ограничения. Зачастую для достижения безопасности достаточно сделать final только "критические" методы, а не весь класс, сохранив таким образом возможность его расширения. С Java 17 у этой развилки появился и третий вариант: запечатанные (sealed) классы (JEP 409). Такой класс перечисляет вpermits тех, кому наследование разрешено, поэтому иерархия остается под контролем автора, но не закрывается наглухо.
Удачи всем!
Вопросы и ответы
Что делает ключевое слово final в Java?
Оно запрещает изменение того, к чему применено. У классаfinal запрещает наследование, у метода переопределение в подклассе, у переменной повторное присваивание. Смысл везде один: автор кода фиксирует решение и не дает его переиграть. Компилятор проверяет это на этапе сборки, поэтому нарушение превращается в ошибку компиляции, а не в сюрприз во время работы.
Можно ли изменить объект, на который ссылается final-переменная?
Да, и это самая частая ловушка.final защищает ссылку, а не объект: список, объявленный как final, нельзя переназначить на новый, но добавлять и удалять его элементы никто не мешает. Если нужна настоящая неизменяемость, берут неизменяемые типы вроде String или неизменяемые копии List.of() и List.copyOf().
Чем final отличается от effectively final?
final вы пишете сами, а effectively final компилятор определяет за вас: так называют переменную, которой значение присвоили один раз, хотя ключевого слова рядом нет. Практический смысл в том, что лямбды, анонимные и локальные классы умеют захватывать только такие переменные. Понятие появилось в Java 8 и описано в спецификации языка.
Когда нужно инициализировать final-поле?
До конца создания объекта, и ровно один раз. Есть три места: прямо при объявлении, в блоке инициализации и в конструкторе, причем значение должен присвоить каждый конструктор класса. Дляstatic final поля годятся объявление и статический блок. Если хотя бы один путь оставляет поле без значения, программа не скомпилируется.
Можно ли объявить класс одновременно final и abstract?
Нет, это ошибка компиляции. Абстрактный класс существует ради наследников, аfinal наследников запрещает, поэтому спецификация языка объявляет такое сочетание недопустимым. Если нужно разрешить наследование не всем, а нескольким конкретным классам, с Java 17 для этого есть запечатанные классы: sealed со списком permits.
Почему класс String объявлен как final?
Чтобы его неизменяемость нельзя было сломать. БудьString открыт для наследования, кто угодно мог бы написать наследника с изменяемым состоянием и подсунуть его туда, где ожидается обычная строка: в имя файла, в проверку прав, в ключ словаря. Заодно final позволяет кешировать хеш-код и безопасно переиспользовать строки из пула литералов.
Читайте также: