В Java есть ключевое слово — final. Оно может применяться к классам, методам, переменным (включая параметры методов). Для класса ключевое слово final означает, что класс не может иметь подклассов, то есть наследование запрещено... Это полезно при создании неизменяемых (immutable) объектов. Например, класс String объявлен как final.

    public final class String {
    }
    
    class SubString extends String { // Ошибка компиляции
    }
Также следует отметить, что модификатор final нельзя применять к абстрактным классам (тем, которые имеют ключевое слово abstract), поскольку это взаимоисключающие концепции.Java Final - 1Для 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). Она применяется только к переменным (включая параметры методов). Суть в том, что, несмотря на явное отсутствие ключевого слова 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)); // Ошибка компиляции
        }
Теперь давайте проведем небольшое собеседование. В конце концов, цель прохождения курса CodeGym — стать Java-разработчиком и найти интересную и высокооплачиваемую работу. Итак, начнем.
  1. Что можно сказать о массиве, который объявлен как final?

  2. Мы знаем, что класс String неизменяем: класс объявлен как final. Строковое значение хранится в массиве char, который помечен ключевым словом final.


public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** Значение используется для хранения символов. */
    private final char value[];

Можем ли мы заменить значение объекта String (не изменяя ссылку на объект)? Это реальные вопросы собеседования. И практика показывает, что многие кандидаты не отвечают на них правильно. Понимание того, как используется ключевое слово final, особенно для ссылочных переменных, очень важно. Пока вы размышляете над этим, я сделаю небольшую просьбу к команде CodeGym. Пожалуйста, дайте текстовому редактору возможность добавлять блок, содержимое которого можно показывать/скрывать при нажатии на него. Ответы:
  1. Массив является объектом, поэтому ключевое слово final означает, что как только ссылка на массив присвоена, ссылку нельзя изменить. Тем не менее, вы можете изменить состояние объекта.

    
            final int[] array = {1, 2, 3, 4, 5};
            array[0] = 9;	 // OK, потому что мы изменяем содержимое массива: {9, 2, 3, 4, 5}
            array = new int[5]; // Ошибка компиляции
    
  2. Да, можем. Главное — понять, что означает коварное ключевое слово final при использовании с объектами. Для замены значений можно использовать Reflection API.


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
         */
    }
}
Обратите внимание, что если бы мы попытались таким образом изменить 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

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

  • Неизменяемые данные: Объявление чувствительных данных как final предотвращает их переназначение. Никаких подозрительных действий!
  • Безопасное наследование: Помечайте критические классы или методы как final, чтобы заблокировать подклассам возможность изменения поведения. Никто не может перехватить вашу логику!
Например, уместно применить final в объявлении метода, который проверяет пароль пользователя. Это гарантирует, что метод будет выполнять именно то, что в него заложено изначально. Если такой метод не будет final, злоумышленник сможет создать свой класс-наследник и переопределить логику проверки, "подсунув" программе свою версию, которая всегда возвращает true, независимо от введенного пароля. Важное замечание: чтобы final-метод был по-настоящему безопасным, поля, которые он использует, должны быть final или private. Иначе дочерний класс сможет изменить значение поля и таким образом повлиять на поведение 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 fruits = new ArrayList<>();
fruits.add("Яблоко"); // Разрешено
fruits = new ArrayList<>(); // Ошибка!

Видите? Сам список fruits заблокирован на месте, но его содержимое может быть изменено. Это называется нетранзитивностью. Хотите действительно неизменяемый объект? Используйте неизменяемые классы, такие как String или Collections.unmodifiableList().

Обратная сторона final: Ограничения и компромиссы

Употребление final в объявлении метода или класса накладывает серьезные ограничения на возможность дальнейшего использования и развития кода. Завершенная реализация: Применение final в объявлении метода — это верный показатель того, что его реализация самодостаточна и полностью завершена. Ограничение для других разработчиков: Программисты, которые захотят использовать ваш класс и расширить его для своих нужд, будут стеснены в возможностях. Пометив класс как final, вы полностью запрещаете наследование и можете существенно снизить его практическую ценность для других. Прежде чем использовать final, всегда стоит подумать, оправданы ли такие ограничения. Зачастую для достижения безопасности достаточно сделать final только "критические" методы, а не весь класс, сохранив таким образом возможность его расширения. Удачи всем!