Я пытался понять механизм вложенных классов в 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 {};
}
Здесь компилятор не сгенерировал никаких методов доступа, но код работал нормально.
Так как же внутренний класс получил доступ к закрытым членам внешнего класса?




Единственное, что мешает классу получить доступ к private членам другого класса, — это JVM (или, точнее, ее верификатор), отклоняющий доступ. Поэтому все, что нужно для того, чтобы это стало возможным, — это сотрудничество с JVM.
В то время как Java 1.1 представила внутренние классы таким образом, что не потребовалось изменений в JVM, JVM претерпела так много изменений за это время, что довольно удивительно, что это изменилось только до Java 11.
В Java 11 появились атрибуты байт-кода NestHost и NestMembers, позволяющие файлам классов обозначать, что они принадлежат так называемому «гнезду». Всем классам, принадлежащим к одному гнезду, разрешен доступ друг к другу private членам. Как уже говорилось, единственное, что нужно было изменить, — это верификатор JVM, чтобы разрешить такой доступ. И, конечно же, компилятор для использования этой возможности. См. также JEP 181.
Таким образом, можно сказать, что JVM по-прежнему ничего не знает о внутренних классах, потому что принадлежность классов к гнезду определяется тем инструментом, который сгенерировал файлы классов (например, компилятор исходного кода Java). Таким образом, можно создавать файлы классов с помощью других инструментов, используя гнезда, не следуя семантике внутреннего класса.
Для завершения следует упомянуть, что файлы классов также содержат информацию о внутренних отношениях классов с использованием атрибута InnerClasses. Но это используется только компиляторами и Reflection, тогда как JVM не использует эту информацию при принятии решения о том, является ли доступ законным или нет.