Как внутренний класс получает доступ к закрытым членам класса в более высоких версиях Java?

Я пытался понять механизм вложенных классов в java.

Рассмотрим следующий код:

public class OuterClass {

private String privateOuterField = "Private Outer Field";

public static String publicStaticOuterField = "Public static outer field";

private static String privateStaticOuterField = "private static outer field";


class InnerClass{
    private String privateInnerField = "Private Inner Field";
    
    //non-final static data members not allowed in java 1.8 but allowed in java 17.0
    //private static String innerClassStaticField = "Private Inner Class Static Field";   
    
    public void accessMembers() {
        System.out.println(privateOuterField);
        System.out.println(publicStaticOuterField);
    }
}

static class StaticInnerClass{
    
    private String privateStaticInnerField = "Private Inner Field of static class";
    
    public void accessMembers(OuterClass outer) {
        //System.out.println(privateOuterField);  //error
        System.out.println(outer.privateOuterField);
        System.out.println(publicStaticOuterField);
        System.out.println(privateStaticOuterField);
    }
}
    
public static void main(String[] args) {
    
    OuterClass outerObj = new OuterClass();
    OuterClass.InnerClass innerObj = outerObj.new InnerClass();
    
    StaticInnerClass staticInnerObj = new StaticInnerClass();
    
    innerObj.accessMembers();
    staticInnerObj.accessMembers(outerObj);
    

}

}

Я знаю, что внутренние классы — это феномен компилятора, виртуальная машина о них не знает. Внутренние классы транслируются в обычные файлы классов с $, разделяющими внешнее и внутреннее имя класса.

Чтобы понять этот механизм более подробно, я попытался разобрать файл класса, скомпилированный в java версии 1.8, с помощью команды javap -p.

Я получил следующие результаты: Внешний класс:

public class staticNestedClasses.OuterClass {
  private java.lang.String privateOuterField;
  public static java.lang.String publicStaticOuterField;
  private static java.lang.String privateStaticOuterField;
  public staticNestedClasses.OuterClass();
  public static void main(java.lang.String[]);
  static java.lang.String access$000(staticNestedClasses.OuterClass);
  static java.lang.String access$100();
  static {};
}

Внутренний класс:

class staticNestedClasses.OuterClass$InnerClass {
  private java.lang.String privateInnerField;
  final staticNestedClasses.OuterClass this$0;
  staticNestedClasses.OuterClass$InnerClass(staticNestedClasses.OuterClass);
  public void accessMembers();
}

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

staticNestedClasses.OuterClass$InnerClass(staticNestedClasses.OuterClass);

эта ссылка на внешний класс хранится в final staticNestedClasses.OuterClass this$0

Но класс OuterClass$InnerClass не может напрямую обращаться к закрытым членам через ссылку на внешний класс, поэтому всякий раз, когда компилятор обнаруживает доступ к закрытым членам из внутреннего класса, он генерирует метод доступа (или методы получения) во внешнем классе.

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

static java.lang.String access$000(staticNestedClasses.OuterClass);
static java.lang.String access$100();

Но когда я скомпилировал тот же код в java 17.0 и разобрал файл класса, я получил следующий результат.

Внешний класс:

public class staticNestedClasses.OuterClass {
  private java.lang.String privateOuterField;
  public static java.lang.String publicStaticOuterField;
  private static java.lang.String privateStaticOuterField;
  public staticNestedClasses.OuterClass();
  public static void main(java.lang.String[]);
  static {};
}

Внешний класс $ внутренний класс:

class staticNestedClasses.OuterClass$InnerClass {
  private java.lang.String privateInnerField;
  private static java.lang.String innerClassStaticField;
  final staticNestedClasses.OuterClass this$0;
  staticNestedClasses.OuterClass$InnerClass(staticNestedClasses.OuterClass);
  public void accessMembers();
  static {};
}

Здесь компилятор не сгенерировал никаких методов доступа, но код работал нормально.

Так как же внутренний класс получил доступ к закрытым членам внешнего класса?

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
0
44
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Единственное, что мешает классу получить доступ к private членам другого класса, — это JVM (или, точнее, ее верификатор), отклоняющий доступ. Поэтому все, что нужно для того, чтобы это стало возможным, — это сотрудничество с JVM.

В то время как Java 1.1 представила внутренние классы таким образом, что не потребовалось изменений в JVM, JVM претерпела так много изменений за это время, что довольно удивительно, что это изменилось только до Java 11.

В Java 11 появились атрибуты байт-кода NestHost и NestMembers, позволяющие файлам классов обозначать, что они принадлежат так называемому «гнезду». Всем классам, принадлежащим к одному гнезду, разрешен доступ друг к другу private членам. Как уже говорилось, единственное, что нужно было изменить, — это верификатор JVM, чтобы разрешить такой доступ. И, конечно же, компилятор для использования этой возможности. См. также JEP 181.

Таким образом, можно сказать, что JVM по-прежнему ничего не знает о внутренних классах, потому что принадлежность классов к гнезду определяется тем инструментом, который сгенерировал файлы классов (например, компилятор исходного кода Java). Таким образом, можно создавать файлы классов с помощью других инструментов, используя гнезда, не следуя семантике внутреннего класса.

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

Другие вопросы по теме