Изучите роль final в Java

Java

Буквальное значение ключевого слова final является окончательным и неизменяемым.Похоже, это синтаксис, который вы, вероятно, можете знать, как использовать, увидев имя, но изучили ли вы конкретное использование final в различных сценариях, мерах предосторожности и предыстории. об идеях дизайна Java?

 

1. окончательная измененная переменная

1. Basics: final изменяет переменные базового типа данных и переменные ссылочного типа данных.

  • Я считаю, что у всех есть базовый здравый смысл: переменные, модифицированные final, не могут быть изменены, но «нельзя изменить» здесь имеет разные значения для разных типов данных.
  • Когда финал является модификацией основных типов данных данных, значение данных не может быть изменено после инициализации; когда окончательная модификация представляет собой данные опорных данных, что является изменением объекта, ссылка после инициализации всегда указывает Для адреса памяти не может быть изменено, но сохранение адреса памяти информации о объектной информации может быть изменена.
  • Предыдущий абзац может быть более абстрактным, я надеюсь, что следующая картинка поможет вам понять, вы обнаружите, что, несмотря на разные значения, суть остается той же.
  • Первая — это диаграмма памяти, когда базовый тип данных модифицируется конечными.

在这里插入图片描述

  • Как показано на рисунке выше, переменная a после инициализации всегда будет указывать на память 003, и в этой памяти после инициализации всегда будет храниться значение 100.
  • Ниже приведена схема окончательного модифицированного эталонного типа данных.

在这里插入图片描述

  • На приведенном выше рисунке переменная p указывает на память 0003, а в памяти 0003 хранится дескриптор объекта p (адрес памяти, где хранятся данные объекта p), и значение этого дескриптора не может быть изменено, то есть переменная p всегда указывает на объект p. Но данные объекта p можно изменить.
// 代码示例
public static void main(String[] args) {
    final Person p = new Person(20, "炭烧生蚝");
    p.setAge(18);   //可以修改p对象的数据
    System.out.println(p.getAge()); //输出18

    Person pp = new Person(30, "蚝生烧炭");
    p = pp; //这行代码会报错, 不能通过编译, 因为p经final修饰永远指向上面定义的p对象, 不能指向pp对象. 
}
  • Нетрудно видеть, что последняя модифицированная переменная равнаПрирода: Окончательная измененная переменная будет указывать на фиксированную память, и значение в этой памяти не может быть изменено.
  • Причина, по которой объект, на который указывает переменная ссылочного типа, может быть изменена, заключается в том, что ссылочная переменная не указывает непосредственно на данные объекта, а указывает на ссылку объекта Таким образом, переменная ссылочного типа, измененная final, будет всегда указывают на фиксированный объект и не могут быть изменены; значение данных объекта может быть изменено.

 

2. Расширенный: константы, измененные final, будут помещены в пул констант на этапе компиляции.

  • Final используется для определения констант. Преимущество определения констант заключается в том, что одни и те же переменные не нужно создавать повторно. Пул констант является важной технологией в Java, и переменные, измененные с помощью final, будут помещены в константы вызывающего класса. на этапе компиляции в пуле.
  • См. следующий демонстрационный код.Этот пример специально разработан для демонстрации, я надеюсь, что он поможет вам понять эту точку знаний.
public static void main(String[] args) {
    int n1 = 2019;          //普通变量
    final int n2 = 2019;    //final修饰的变量

    String s = "20190522";  
    String s1 = n1 + "0522";	//拼接字符串"20190512"
    String s2 = n2 + "0522";	

    System.out.println(s == s1);	//false
    System.out.println(s == s2);	//true
}

Первое, что нужно ввести, это то, что целое число -127-128 загружается в пул констант по умолчанию, то есть, если задействована целочисленная операция -127-128, значение целого числа может быть определено во время компиляции. по умолчанию, поэтому здесь я сознательно выбираю числа 2019 (больше 128), избегаю чисел, которые по умолчанию хранятся в постоянном пуле.

  • Приведенный выше код работает следующим образом:
  • Во-первых, в соответствии с принципом, что константы, измененные final, будут помещены в пул констант во время компиляции, n2 будет помещено в пул констант во время компиляции.
  • Затем строка «20190522», соответствующая переменной s, будет помещена в пул строковых констант, и будет предоставлена ​​ссылка для возврата к переменной s.
  • В это время выполняется сплайсинг строки s1.Поскольку данные, соответствующие n1, не помещаются в пул констант, s1 не может быть сплайсирован временно, а значение, соответствующее s1, может быть определено только при загрузке и запуске программы.
  • Но при сплайсинге s2, поскольку n2 уже существует в пуле констант, его можно напрямую сплайсировать с «0522», а результат сплайсинга — «20190522».В это время система проверит пул констант строк и найти, что строка 20190522 уже существует, поэтому возвращается ссылка на 20190522. Таким образом, s2 и s указывают на одну и ту же ссылку, которая указывает на 20190522 в пуле строковых констант.

 

  • Когда программа выполняется, переменная n1 имеет определенный указатель.
  • При склейке s1 создается новый объект типа String, что означает, что 20 190 522 предоставит новый пул констант строки в кавычках.
  • Таким образом, когда s1 и s оцениваются как "==", поскольку соответствующие ссылки различны, он возвращает false. А s2 и s указывают на одну и ту же ссылку и возвращают true.

Резюме: этот пример должен проиллюстрировать следующее: поскольку константа, измененная final, войдет в пул констант во время компиляции, если есть операция, включающая константу, она, вероятно, будет завершена во время компиляции.

 

3. Исследуйте: почему локальные/анонимные внутренние классы могут использовать только финальные измененные переменные при использовании внешних локальных переменных?

Совет: После JDK1.8 при доступе к внешним локальным переменным через внутренние классы нет необходимости явно объявлять внешние локальные переменные как final, это не значит, что не нужно объявлять как final, но система делает это за нас во время компиляции.. Но нам все еще нужно понять, почему мы используем final для изменения внешних локальных переменных.

public class Outter {
    public static void main(String[] args) {
        final int a = 10;
        new Thread(){
            @Override
            public void run() {
                System.out.println(a);
            }
        }.start();
    }
}
  • В приведенном выше коде, если ключевое слово final не добавлено во внешнюю локальную переменную a, он не может быть скомпилирован.Подумайте об этом: когда основной метод будет выполнен, всплывет кадр стека основного метода, если на этот раз жизненный цикл объекта Thread не закончился, и если оператор печати не был выполнен, внешняя переменная a будет недоступна.
  • Так зачем же добавлять ключевое слово final для нормальной компиляции? Давайте посмотрим на декомпилированный код, чтобы увидеть, как внутренний класс вызывает внешние переменные-члены.
  • Мы можем получить файл .class (скомпилированный с помощью IDE) через Javac, затем ввести командную строкуjavap -c .class文件的绝对路径Вы можете просмотреть антикомпонентный код файла .Class. Вышеуказанный класс находки скомпилируется для генерации двух файлов .Class соответственноOutter.class和Outter$1.class, то есть внутренний класс будет отдельно скомпилирован в файл .class.Дано следующееOutter$1.classдекомпилированный код.
Compiled from "Outter.java"
final class forTest.Outter$1 extends java.lang.Thread {
  forTest.Outter$1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Thread."<init>":()V
       4: return

  public void run();
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: bipush        10
       5: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
       8: return
}
  • Таргетингrun()Строка 3 в декомпилированном коде метода:
  • 3: bipush 10
  • Мы видим, что значение a находится во внутреннем классеrun()Во время выполнения метода он сохраняется в таблице локальных переменных в виде проталкивания стека, то есть, когда внутренний класс печатает значение переменной а, переменная а не является внешней локальной переменной а , потому что если это внешняя локальная переменная, она должна быть использованаloadИнструкция загружает значение переменной, то есть система копирует копию внешней локальной переменной a во внутренний класс в виде копии, а во внутреннем классе есть переменная, указывающая на значение, на которое указывает по внешней переменной а.

 

  • Однако, кажется, что связь с final не очень велика, кажется, что копию переменной можно скопировать без final, но значение переменной не может быть известно во время компиляции, а в это время пора подумать о новом вопросе: теперь мы знаем, что переменная a внутреннего класса и внешняя локальная переменная a являются двумя совершенно разными переменными, поэтому, если значение, на которое указывает переменная a, изменяется во внутреннем классе во время выполнения run( ), возникнет проблема несогласованности данных.
  • Просто потому, что наше первоначальное намерение состоит в том, чтобы внутренний класс и внешний класс обращались к одной и той же переменной a, поэтому при использовании внешней локальной переменной во внутреннем классе локальная переменная должна быть изменена с помощью final, чтобы значение локальной переменной a никогда не изменится. Это также позволяет избежать возникновения проблем с несогласованностью данных.

 

2. метод окончательной модификации

  • Использование окончательного модифицированного метода имеет два эффекта, первый - заблокировать метод и не допустить унаследованного класса для его изменения.
  • Еще одна функция - встраивание метода в компилятор для повышения эффективности.Но он сейчас редко используется, и современная версия Java хорошо справилась с этой частью оптимизации.Но чтобы удовлетворить ваше любопытство, давайте кое-что узнаем Это интроверт подход.
  • Интровертный метод: при вызове метода система должна сохранять информацию на месте, создавать кадры стека, восстанавливать потоки и т. д. Эти операции занимают относительно много времени. Если вы используете final для изменения метода a, вызовите метод a в другом Когда класс метода а компилируется, код метода а встраивается непосредственно в блок кода, который вызывает а.
//原代码
public static void test(){
    String s1 = "包夹方法a";
    a();
    String s2 = "包夹方法a";
}

public static final void a(){
    System.out.println("我是方法a中的代码");
    System.out.println("我是方法a中的代码");
}

//经过编译后
public static void test(){
    String s1 = "包夹方法a";
    System.out.println("我是方法a中的代码");
    System.out.println("我是方法a中的代码");
    String s2 = "包夹方法a";
}
  • Когда метод очень большой, такие встроенные методы вряд ли могут улучшить производительность. В последних версиях Java нет необходимости использовать окончательные методы для этих оптимизаций. --《Java Programming Ideas》

 

3. окончательный модифицированный класс

  • Цель использования final для украшения класса проста и ясна: указать, что класс не может быть унаследован.
  • Когда в программе есть классы, которые никогда не будут унаследованы, вы можете использовать ключевое слово final для изменения
  • Все члены измененного Final будут неявно изменены как метод Final.