Проблема с конструкторами вложенного класса


Этот вопрос касается интересного поведения 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
Другие вопросы по тегам