Skip to content

Latest commit

 

History

History
239 lines (161 loc) · 12.4 KB

final.md

File metadata and controls

239 lines (161 loc) · 12.4 KB

Ключевое слово final

Введение

Возникают ситуации, когда необходимо запретить вносить изменения состояния или поведения, показать завершенность.

В чем это проявляется в 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, то изменить значение ссылки нельзя - компилятор защищает эту ссылку, ведь вы объявили ее финальной. Но сам объект(если до него есть доступ) изменить можно!

Это как если вы прикрутили намертво полку к стене. Саму полку уже не изменить и не снять, но вещи оттуда взять или положить можно. Разумеется, если прав доступа до такой полки хватит!