Понимание инициализации классов, перегрузки методов и перезаписи с точки зрения JVM.

Java задняя часть JVM переводчик

инициализация класса

Прежде чем говорить об инициализации класса, давайте кратко рассмотрим цикл объявления класса. Как показано ниже

Цикл объявления класса можно разделить на 7 этапов, но сегодня мы поговорим только оинициализациясцена. мы чувствуем себя не в своей тарелкеиспользоватьа такжеудалитьвне сцены,инициализацияЭтап наиболее близок к тому, что мы обычно изучаем, а также с ним проще всего столкнуться в процессе выполнения вопросов в письменном тесте.Если вы хотите понять каждый этап, вы можете проверить его.Глубокое понимание виртуальной машины JavaЭта книга.

Давайте начнем объяснять процесс инициализации.

Уведомление:

Здесь следует указать, что до того, как будет выполнена инициализация класса, фактическиПодготовитьэтап ужепеременная классапамять была выделена и также была установленапеременная классаначальное значение . Например, начальное значение целого числа равно 0, начальное значение объекта равно нулю и так далее. Начальные значения примитивных типов данных следующие:

тип данных Первоначальный значение тип данных Первоначальный значение
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char '\u0000' reference null
byte (byte)0

Все сначала задумаются над вопросом: когда мы запускаем java-программу, будет ли инициализирован каждый класс? Если не каждый класс выполняет инициализацию, то когда класс выполняет инициализацию?

Ответ в том, что не каждый класс выполнит процесс инициализации, вы думаете, если этот класс вообще не используется, зачем его инициализировать, занимать место.

Что касается того, когда выполнять процесс инициализации, спецификация виртуальной машины строго оговариваетесть и только5 кейсов сразу будут проводиться на занятииинициализация.

  1. когда используешьnewЭто ключевое слово создает экземпляры объектов, считывает или устанавливает значение класса.статическое поле, и когда вызывается статический метод класса, инициализация класса будет запущена (обратите внимание, что статические поля, измененные с помощью final, исключаются).
  2. При использовании метода пакета java.lang.reflect для вызова отражения класса, если класс не был инициализирован, будет запущена инициализация класса.
  3. При инициализации класса, если его родительский класс не был инициализирован, его родительский класс будет запущен первым.
  4. Когда виртуальная машина запускается, пользователю необходимо указать основной класс для выполнения (тот, который содержит метод main()), и виртуальная машина сначала инициализирует этот основной класс.
  5. При использовании динамической языковой поддержки JDK 1.7, если... (опущено, я не понимаю, ха-ха).

Обратите внимание, чтоесть и только. Эти 5 вариантов поведения мы называем поведением класса.Активное цитирование.

процесс инициализации

Что делает процесс инициализации класса?

В процессе инициализации класса, грубо говоря,метод конструктора класса()обработать. Обратите внимание, что этот clinit не является конструктором класса (init()).

Что касается содержимого метода clinit()?

На самом деле метод clinit() автоматически собирает компилятором всепеременная классаДействие присваивания истатический блок(блок static{}), порядок, в котором операторы собираются компилятором, определяется порядком, в котором операторы появляются в исходном файле. а такжеВ блоке статических операторов доступны только переменные, определенные до блока статических операторов, а переменным, определенным после него, могут быть присвоены значения в предыдущем блоке статических операторов, но доступ к ним недоступен.. как следующая процедура.

public class Test1 {
    static {
        t = 10;//编译可以正常通过
        System.out.println(t);//提示illegal forward reference错误
    }
    static int t = 0;
}

Дайте вам практику

public class Father {
    public static int t1 = 10;
    static {
        t1 = 20;
    }
}
class Son extends Father{
    public static int t2 = t1;
}
//测试调用
class Test2{
    public static void main(String[] args){
        System.out.println(Son.t2);
    }
}

Каков результат?

Ответ 20. Думаю, все знают, почему. Потому что родительский класс будет инициализирован первым.

Однако здесь следует отметить, что для класса при выполнении метода clinit() класса сначала будет выполнен метод clinit() родительского класса, а для интерфейса метод clinit() класса Интерфейс не будет выполнен.Метод clinit() родительского интерфейса. Только когда используются переменные, определенные в родительском интерфейсе, будет выполнен метод clinit() родительского интерфейса.

пассивное цитирование

Вышеупомянутые пять случаев инициализации класса, которые мы называемАктивное цитирование. На самом деле есть инициатива, а значит, есть и т.н.пассивныйЦитировать. Здесь необходимо упомянуть, что пассивные ссылки не запускают инициализацию класса. Вот несколько примеров пассивного цитирования:

  1. Ссылка на статические поля родительского класса через подкласс не приведет к инициализации подкласса.
/**
 * 1.通过子类引用父类的静态字段,不会触发子类的初始化
 */
public class FatherClass {
    //静态块
    static {
        System.out.println("FatherClass init");
    }
    public static int value = 10;
}

class SonClass extends FatherClass {
    static {
        System.out.println("SonClass init");
    }
}
 class Test3{
    public static void main(String[] args){
        System.out.println(SonClass.value);
    }
}

выходной результат

FatherClass init

Описание не запускает инициализацию подкласса

  1. Ссылка на класс через определение массива не вызывает инициализацию этого класса.
 class Test3{
    public static void main(String[] args){
        SonClass[] sonClass = new SonClass[10];//引用上面的SonClass类。
    }      
 }

Выход - ничего.

  1. ссылки на другие классыпостоянныйне запускает инициализацию этого класса
public class FatherClass {
    //静态块
    static {
        System.out.println("FatherClass init");
    }
    public static final String value = "hello";//常量
}

class Test3{
    public static void main(String[] args){
        System.out.println(FatherClass.value);
    }
}

Результат вывода: привет

На самом деле причина, по которой "FatherClass init" не выводится, заключается в том, что на этапе компиляции над этой константой были выполнены некоторые оптимизации. "Эта константа хранится в пуле констант класса Test3, и последующие ссылки на FatherClass.value фактически преобразуются в ссылки класса Test3 на собственный пул констант. Другими словами, после компиляции в файл класса два класса не имеют ничего общего друг с другом.


перегрузка

Что касается перегрузки, я думаю, это понимают все, кто изучал java, но сегодня мы рассмотрим, что такое перегрузка с точки зрения виртуальной машины.

Сначала давайте посмотрим на кусок кода:

//定义几个类
public abstract class Animal {
}
class Dog extends Animal{
}
class Lion extends Animal{
}

class Test4{
    public void run(Animal animal){
        System.out.println("动物跑啊跑");
    }
    public void run(Dog dog){
        System.out.println("小狗跑啊跑");
    }
    public void run(Lion lion){
        System.out.println("狮子跑啊跑");
    }
    //测试
    public static void main(String[] args){
        Animal dog = new Dog();
        Animal lion = new Lion();;
        Test4 test4 = new Test4();
        test4.run(dog);
        test4.run(lion);
    }
}

результат операции:

животное бег бег

животное бег бег

Я считаю, что каждый, кто научился перегружаться, может догадаться, что это результат. Но почему для перегрузки был выбран именно этот метод? Как выбирается виртуальная машина?

Перед этим давайте сначала разберемся с двумя понятиями.

Сначала посмотрите на строку кода:

Animal dog = new Dog();

Для этой строки кода мы называем Animal переменной dog.статический тип, а последний Dog называется переменной dogфактический тип.

Так называемый статический тип означает, что о нем можно судить во время компиляции кода, что означает, что о статическом типе собаки можно судить во время компиляции. Но переменная dog не может быть известна во время компиляции.фактический типчто.

Теперь давайте посмотрим, на чем основана виртуальная машина.перегрузкакакой способ выбрать.

Для переменных с одинаковым статическим типом, но разными фактическими типами, виртуальная машина выбирает параметры в соответствии со статическим типом, а не фактическим типом при перегрузке. А статический тип известен компилятору, а значит, на этапе компиляции он уже решил, какой перегруженный метод выбрать.

Поскольку статические типы собаки и льва относятся к типу Animal, выбирается метод run(Animal animal).

Однако следует отметить, что иногда перегруженных версий может быть несколько, то есть перегруженная версия не является единственной. Давайте посмотрим на код ниже.

public class Test {
    public static void sayHello(Object arg){
        System.out.println("hello Object");
    }
    public static void sayHello(int arg){
        System.out.println("hello int");
    }
    public static void sayHello(long arg){
        System.out.println("hello long");
    }
    public static void sayHello(Character arg){
        System.out.println("hello Character");
    }
    public static void sayHello(char arg){
        System.out.println("hello char");
    }
    public static void sayHello(char... arg){
        System.out.println("hello char...");
    }
    public static void sayHello(Serializable arg){
        System.out.println("hello Serializable");
    }

    //测试
    public static void main(String[] args){
        char a = 'a';
        sayHello('a');
    }
}

Запустите код ниже. Я считаю, что все знают, что выход

hello char

Поскольку статический тип a — char, он будет соответствовать sayHello(char arg);

Однако, если мы закомментируем метод sayHello(char arg) и запустим его снова.

Вывод результата:

hello int

Фактически, в настоящее время, поскольку в методе нет статического метода типа char, он будет автоматически выполнять преобразование типов. Помимо символа, «а» также может представлять число 97. Поэтому для перегрузки будет выбран тип int.

Продолжаем комментировать метод sayHello(int arg). Результат выведет:

привет долго.

В это время 'a' выполняет два преобразования типов, а именно 'a' -> 97 -> 97L. Таким образом, он соответствует методу sayHell(long arg).

На самом деле 'a' будет преобразовано в следующем порядке: char -> int -> long -> float -> double. Но он не будет конвертировать в байт или шорт, потому что преобразование из char в байт или шорт небезопасно. (Почему не безопасно? Оставьте это на ваше усмотрение)

Продолжайте комментировать метод типа long. Результат:

hello Character

В этот момент происходит автоупаковка, и 'a' инкапсулируется как тип Character.

Идите вперед и закомментируйте методы типа Character. выход

hello Serializable

Почему?

Какое отношение имеет символ или число к сериализации? На самом деле это потому, что Serializable — это интерфейс, реализованный классом Character.После автоматической упаковки обнаруживается, что упакованный класс не может быть найден, но тип интерфейса, реализованный упакованным классом, найден, поэтому автоматическое преобразование происходит один раз.

Мы продолжаем комментировать Serialiable, и на данный момент выводим:

hello Object

В это время 'a' преобразуется в родительский класс после упаковки. Если есть несколько родительских классов, поиск начнется снизу вверх в отношении наследования, то есть чем ближе к верхнему слою, тем ниже приоритет.

Продолжайте комментировать метод Object, на этот раз вывод:

hello char...

На этот раз 'a' преобразуется в элемент массива.

Из приведенного выше примера видно, что статический тип элемента не обязательно является фиксированным,Он преобразуется во время компиляции в соответствии с принципом приоритета. По сути, в этом суть перегрузки в языке java.

переписать

Давайте сначала посмотрим на кусок кода

//定义几个类
public abstract class Animal {
    public abstract void run();
}
class Dog extends Animal{
    @Override
    public void run() {
        System.out.println("小狗跑啊跑");
    }
}
class Lion extends Animal{
    @Override
    public void run() {
        System.out.println("狮子跑啊跑");
    }
}
class Test4{
    //测试
    public static void main(String[] args){
        Animal dog = new Dog();
        Animal lion = new Lion();;
        dog.run();
        lion.run();
    }
}

результат операции:

щенок бегает лев беги беги

Я думаю, что у всех нет сомнений в этом результате. Их статические типы одинаковы, как виртуальная машина узнает, какой метод выполнять?

Судя по всему, виртуальная машина основана нафактический типдля выполнения метода. Давайте взглянем на часть метода main()байт-код

//声明:我只是挑出了一部分关键的字节码
public static void (java.lang.String[]);
    Code:
    Stack=2, Locals=3, Args_size=1;//可以不用管这个
    //下面的是关键
    0:new #16;//即new Dog
    3: dup
    4: invokespecial #18; //调用初始化方法
    7: astore_1
    8: new #19 ;即new Lion
    11: dup
    12: invokespecial #21;//调用初始化方法
    15: astore_2
    
    16: aload_1; 压入栈顶
    17: invokevirtual #22;//调用run()方法
    20: aload_2 ;压入栈顶
    21: invokevirtual #22;//调用run()方法
    24: return

Объясните этот байт-код:

Роль строк 0-15 заключается в создании пространства памяти объектов Dog и Lion и вызове конструктора экземпляров типов Dog и Lion. Соответствующий код:

Animal dog = new Dog();

Animal lion = new Lion();

Ключевыми частями являются следующие предложения 16-21. Два предложения 16 и 20 соответственно перемещают ссылки двух только что созданных объектов на вершину стека. 17 и 21 — инструкции вызова метода run().

Как видно из инструкций, инструкции вызова этих двух методов совершенно одинаковы. Но окончательная реализация целевого метода не одинакова. Почему это?

Фактически:

Инструкция вызова метода invokevirtual выполняется следующим образом:

  1. Найдите фактический тип объекта, на который указывает первый элемент на вершине стека, обозначенный буквой C.
  2. Если метод run() найден в типе C, то проверяется право доступа, если доступ есть, то метод является прямой ссылкой на метод и поиск завершается, если доступ к методу невозможен, java.lang Выдается исключение .IllegalAccessEror.
  3. Если метод run() не найден в объекте, второй шаг поиска и проверки выполняется для каждого родительского класса C снизу вверх в соответствии с отношением наследования.
  4. Если ничего не найдено, создается исключение java.lang.AbstractMethodError.

Таким образом, несмотря на то, что вызов инструкции тот же, когда метод run вызывается в строке 17, ссылка на объект, хранящаяся на вершине стека, — это Dog, а в строке 21 — Lion.

В этом суть переопределения методов в языке java.

Это конец этого объяснения, я надеюсь, что это поможет вам.

Справочная литература:

  1. Глубокое понимание виртуальной машины Java

Обратите внимание на мой публичный номер: трудолюбивый кодовый фермер Возьмите вас, чтобы пробить leetcode каждый день. Публикуйте технические статьи нерегулярно каждую неделю. Обязательно найдутся полезные для вас статьи, если не верите, попробуйте