Статьи

Странности отражения Java с параметрами конструктора внутреннего класса

О внутренних классах Java

Java допускает классы-члены (классы, которые определены внутри других классов), локальные классы (классы, которые определены внутри блоков операторов) и анонимные классы (классы без имен):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Outer {
    Object anonymous = new Object(){}; // this is an anonymous class
  
    // anonymous initialisation block
    {
        // this is a local class
        class Local{}
        Local l = new Local();
    }
  
    Outer() {
        // this is a local named class in a constructor
        class Local{}
        Local l = new Local();
    }
  
    void method() {
        // this is a local named class in a method
        class Local{}
        Local l = new Local();
    }
  
    // this is a member class
    class Inner{}
    Inner i = new Inner();
}

Спецификация языка Java классифицирует членские, локальные и анонимные классы как внутренние классы .

Реализация «деталей»

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

Еще одна полезная деталь реализации внутренних классов — это то, что конструкторы внутренних классов принимают дополнительные синтетические параметры. Относительно хорошо известно, что первым синтетическим параметром конструктора внутреннего класса будет его включающий экземпляр, который он будет хранить в this$X синтетическом поле this$X Это справедливо для всех трех типов внутренних классов: членских, локальных и анонимных.

Но, как правило, не известно, что локальные классы, которые захватывают непостоянные конечные переменные, потребуют передачи всех этих переменных в качестве дополнительных параметров синтетического конструктора (захваченные постоянные конечные переменные будут встроены и не будут генерировать дополнительные параметры синтетического конструктора):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
class Outer {
    void method() {
        final String constant = "foo";
        final String nonConstant = "foo".toUpperCase();
        class Local{
            /* synthetic fields and constructor:
  
            Outer this$0;
            String nonConstant;
  
            Local(Outer this$0, String nonConstant){
                this.this$0 = this$0;
                this.nonConstant = nonConstant;
            }
            */
        }
        Local l = new Local();
    }
}

Хорошо, но почему я должен заботиться?

В большинстве случаев вам все равно, кроме вашего собственного любопытства. Но если вы делаете отражение Java с внутренними классами, есть несколько вещей, которые вы должны знать, и, поскольку я не нашел их в списке или в сети, я подумал, что было бы важно составить список вещей, чтобы помочь другим понять это потому, что разные компиляторы будут давать разные результаты в API отражения Java.

Вопрос в том, что происходит, когда вы используете отражение Java для получения экземпляра java.lang.reflect.Constructor для конструкторов внутреннего класса? В частности, что происходит с методами, которые позволяют получить доступ к типам параметров (pre-generics: getParameterTypes() ), универсальным типам параметров (post-generics: getGenericParameterTypes() ) и аннотациям ( getParameterAnnotations() ), а также к ответу это: это зависит .

Предположим, следующий класс Java:

1
2
3
4
5
6
7
class Outer {
    class Inner {
        Inner(){}
        Inner(String param){}
        Inner(@Deprecated Integer param){}
    }
}

Ниже приведены размеры массивов, возвращаемых этими тремя методами отражения для каждого нашего конструктора, и их различия в зависимости от используемого компилятора Java:

Outer.Inner.class
.getDeclaredConstructor ()
Outer.Inner.class
.getDeclaredConstructor (
String.class)
Outer.Inner.class
.getDeclaredConstructor (
Integer.class)
getParameterTypes ()
.length
1 2 2
getGenericParameterTypes ()
длина, скомпилированная с Eclipse
1 2 2
getGenericParameterTypes ()
длина скомпилирована с Javac
0 1 1
getParameterAnnotations ()
.length
1 2 1

Как видите, синтетические параметры всегда включаются в getParameterTypes() , но включаются только в getGenericParameterTypes() при компиляции с Eclipse.

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

С помощью этой информации вы теперь понимаете различия между результатами этих методов, но до сих пор я не нашел способа определить, какой параметр является синтетическим или нет, потому что, хотя вы можете сделать правильное предположение для this$X синтетического this$X Параметр, который требуется для каждого внутреннего класса, у вас нет возможности узнать количество непостоянных захваченных переменных, которые в конечном итоге станут синтетическими параметрами для конструкторов локальных классов.