Проблема с конструкторами вложенного класса
Этот вопрос касается интересного поведения Java: в некоторых ситуациях он создает дополнительный конструктор (не по умолчанию) для вложенных классов.
Этот вопрос также касается странного анонимного класса, который Java создает с помощью этого странного конструктора.
Рассмотрим следующий код:
package a;
import java.lang.reflect.Constructor;
public class TestNested {
class A {
A() {
}
A(int a) {
}
}
public static void main(String[] args) {
Class<A> aClass = A.class;
for (Constructor c : aClass.getDeclaredConstructors()) {
System.out.println(c);
}
}
}
Это будет печатать:
a.TestNested$A(a.TestNested)
a.TestNested$A(a.TestNested,int)
Хорошо. Далее, давайте сделаем конструктор A(int a)
частный:
private A(int a) {
}
Запустите программу снова. Получать:
a.TestNested$A(a.TestNested)
private a.TestNested$A(a.TestNested,int)
Это тоже нормально. Но теперь давайте изменим main()
метод таким образом (добавление нового экземпляра класса A
создание):
public static void main(String[] args) {
Class<A> aClass = A.class;
for (Constructor c : aClass.getDeclaredConstructors()) {
System.out.println(c);
}
A a = new TestNested().new A(123); // new line of code
}
Тогда ввод становится:
a.TestNested$A(a.TestNested)
private a.TestNested$A(a.TestNested,int)
a.TestNested$A(a.TestNested,int,a.TestNested$1)
Что это: a.TestNested $ A (a.TestNested, int, a.TestNested $ 1) <<< ---??
Хорошо, давайте снова сделаем конструктор A(int a)
пакет местный:
A(int a) {
}
Снова запустите программу (мы не удаляем строку с экземпляром A
создание!), вывод как в первый раз:
a.TestNested$A(a.TestNested)
a.TestNested$A(a.TestNested,int)
Вопросы:
1) Как это можно объяснить?
2) Что это за третий странный конструктор?
ОБНОВЛЕНИЕ: Исследование показало следующее.
1) Попробуем вызвать этот странный конструктор, используя отражение от другого класса. Мы не сможем сделать это, потому что нет никакого способа создать экземпляр этого странного TestNested$1
учебный класс.
2) Хорошо. Давайте сделаем свое дело. Добавим в класс TestNested
такое статическое поле:
public static Object object = new Object() {
public void print() {
System.out.println("sss");
}
};
Что ж? Хорошо, теперь мы можем вызвать этот третий странный конструктор из другого класса:
TestNested tn = new TestNested();
TestNested.A a = (TestNested.A)TestNested.A.class.getDeclaredConstructors()[2].newInstance(tn, 123, TestNested.object);
Извините, но я абсолютно не понимаю этого.
ОБНОВЛЕНИЕ-2: Дополнительные вопросы:
3) Почему Java использует специальный анонимный внутренний класс для типа аргумента для этого третьего синтетического конструктора? Почему не просто Object
тип конструктора со специальным именем?
4) Какая Java могла бы использовать уже определенный анонимный внутренний класс для этих целей? Разве это не какое-то нарушение безопасности?
2 ответа
Прежде всего, спасибо за этот интересный вопрос. Я был так заинтригован, что не удержался, посмотрев на байт-код. Это байт-код TestNested
:
Compiled from "TestNested.java"
public class a.TestNested {
public a.TestNested();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc_w #2 // class a/TestNested$A
3: astore_1
4: aload_1
5: invokevirtual #3 // Method java/lang/Class.getDeclaredConstructors:()[Ljava/lang/reflect/Constructor;
8: astore_2
9: aload_2
10: arraylength
11: istore_3
12: iconst_0
13: istore 4
15: iload 4
17: iload_3
18: if_icmpge 41
21: aload_2
22: iload 4
24: aaload
25: astore 5
27: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
30: aload 5
32: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
35: iinc 4, 1
38: goto 15
41: new #2 // class a/TestNested$A
44: dup
45: new #6 // class a/TestNested
48: dup
49: invokespecial #7 // Method "<init>":()V
52: dup
53: invokevirtual #8 // Method java/lang/Object.getClass:()Ljava/lang/Class;
56: pop
57: bipush 123
59: aconst_null
60: invokespecial #9 // Method a/TestNested$A."<init>":(La/TestNested;ILa/TestNested$1;)V
63: astore_2
64: return
}
Как видите, конструктор a.TestNested$A(a.TestNested,int,a.TestNested$1)
вызывается из вашего main
метод. Более того, null
передается как значение a.TestNested$1
параметр.
Итак, давайте посмотрим на таинственный анонимный класс a.TestNested$1
:
Compiled from "TestNested.java"
class a.TestNested$1 {
}
Странно - я бы ожидал, что этот класс действительно что-то сделает. Чтобы понять это, давайте посмотрим на конструкторы в a.TestNested$A
:
class a.TestNested$A {
final a.TestNested this$0;
a.TestNested$A(a.TestNested);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field this$0:La/TestNested;
5: aload_0
6: invokespecial #3 // Method java/lang/Object."<init>":()V
9: return
private a.TestNested$A(a.TestNested, int);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field this$0:La/TestNested;
5: aload_0
6: invokespecial #3 // Method java/lang/Object."<init>":()V
9: return
a.TestNested$A(a.TestNested, int, a.TestNested$1);
Code:
0: aload_0
1: aload_1
2: iload_2
3: invokespecial #1 // Method "<init>":(La/TestNested;I)V
6: return
}
Глядя на пакет-видимый конструктор a.TestNested$A(a.TestNested, int, a.TestNested$1)
Мы видим, что третий аргумент игнорируется.
Теперь мы можем объяснить конструктор и анонимный внутренний класс. Дополнительный конструктор необходим для того, чтобы обойти ограничение видимости частного конструктора. Этот дополнительный конструктор просто делегирует приватному конструктору. Однако он не может иметь точно такую же подпись, как частный конструктор. Из-за этого добавляется анонимный внутренний класс, чтобы обеспечить уникальную подпись, не вступая в конфликт с другими возможными перегруженными конструкторами, такими как конструктор с подписью (int,int)
или же (int,Object)
, Поскольку этот анонимный внутренний класс необходим только для создания уникальной подписи, его не нужно создавать и не нужно иметь содержимое.
Третий конструктор - это синтетический конструктор, сгенерированный компилятором, чтобы разрешить доступ к приватному конструктору из внешнего класса. Это связано с тем, что внутренние классы (и доступ их классов к своим закрытым членам) существуют только для языка Java, а не для JVM, поэтому компилятор должен преодолеть разрыв за кулисами.
Отражение скажет вам, если член является синтетическим:
for (Constructor c : aClass.getDeclaredConstructors()) {
System.out.println(c + " " + c.isSynthetic());
}
Это печатает:
a.TestNested$A(a.TestNested) false
private a.TestNested$A(a.TestNested,int) false
a.TestNested$A(a.TestNested,int,a.TestNested$1) true
См. Этот пост для дальнейшего обсуждения: Eclipse предупреждение о синтетическом аксессоре для частных статических вложенных классов в Java?
РЕДАКТИРОВАТЬ: интересно, компилятор eclipse делает это не так, как javac. При использовании eclipse он добавляет аргумент типа самого внутреннего класса:
a.TestNested$A(a.TestNested) false
private a.TestNested$A(a.TestNested,int) false
a.TestNested$A(a.TestNested,int,a.TestNested$A) true
Я попытался сбить его с толку, выставив этого конструктора заранее:
class A {
A() {
}
private A(int a) {
}
A(int a, A another) { }
}
Для этого нужно просто добавить еще один аргумент в синтетический конструктор:
a.TestNested$A(a.TestNested) false
private a.TestNested$A(a.TestNested,int) false
a.TestNested$A(a.TestNested,int,a.TestNested$A) false
a.TestNested$A(a.TestNested,int,a.TestNested$A,a.TestNested$A) true