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
27
28
29
30
|
public class Test { public void testAlternatives() { // Alternative 1 System.out.println( new Inner().field); // Alternative 2 System.out.println( new Inner().getField()); // Alternative 3 System.out.println( new Inner2().field); // Alternative 4 System.out.println( new Inner2().getField()); } class Inner { private int field; public int getField() { return field; } } class Inner2 { int field; public int getField() { return field; } } } |
Интуитивно понятный ответ заключается в том, что альтернативы 1 и 3 одинаково бывают быстрыми, потому что поле всегда видно классу включения, и оба используют доступ к полю, который в целом немного быстрее, чем доступ к методу, использованный в альтернативах 2 и 4. Однако есть детали реализации, которые приводит к тому, что это не соответствует действительности. Сама JVM не имеет концепции под названием «внутренние классы». Вся концепция реализуется компилятором Java, а на уровне байт-кода все состоит из обычных классов.
Проблема здесь в том, что если у внутреннего класса есть приватное поле, и компилятор в конечном итоге скомпилирует внутренний класс как обычный класс. Закрытое поле в обычном классе не может быть доступно другим классам, поэтому включающий класс Test не может «увидеть» поле без некоторых хитростей. Вот вышеприведенный код, «рассуждающий» к тому, что компилятор фактически компилирует в байт-код:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
public class Test { public void testAlternatives() { // Alternative 1 System.out.println(Test$Inner.access$ 000 ( new Test$Inner( this ))); // Alternative 2 System.out.println( new Test$Inner( this ).getField()); // Alternative 3 System.out.println( new Test$Inner2( this ).field); // Alternative 4 System.out.println( new Test$Inner2( this ).getField()); } } class Test$Inner { final Test this $ 0 ; private int field; Test$Inner(Test test) { this $ 0 = test; } public int getField() { return field; } static int access$ 000 (Test$Inner inner) { return inner.field; } } class Test$Inner2 { final Test this $ 0 ; int field; Test$Inner2(Test test) { this $ 0 = test; } public int getField() { return field; } } |
Как видите, статический метод доступа уровня пакета, называемый access $ 000, генерируется для предоставления доступа к приватному полю. Теперь легче понять, что вариант 3, скорее всего, будет самым быстрым, поскольку он единственный, который использует прямой доступ к полю. Использование доступа к пакетам в полях — это микрооптимизация, но все это определенно является деталью, которую должны знать разработчики Java. В критически важных для кода частях кода это может иметь значение, и в руководстве по производительности Android фактически упоминается эта деталь реализации.
Эта деталь реализации может также вызвать небольшую путаницу при попытке доступа к полю по нулевой ссылке на внутренний класс. Рассмотрим следующий код:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public class NullTest { class Inner { private int field; } public void test() { Inner inner = null ; System.out.println(inner.field); } public static void main(String[] args) { new NullTest().test(); } } |
Переменная «inner» имеет значение null, поэтому исключение NullPointerException явно выбрасывается. Однако, что не очевидно из исходного кода, так это то, что исключение выдается внутри статического метода доступа, созданного компилятором!
1
2
3
4
5
|
$ java NullTest Exception in thread 'main' java.lang.NullPointerException at NullTest$Inner.access$ 000 (NullTest.java: 2 ) at NullTest.test(NullTest.java: 8 ) at NullTest.main(NullTest.java: 12 ) |
Трассировка стека содержит интуитивно понятный источник исключений (строка 8), но реальный источник сбивает с толку разработчиков, которые не знают о методах доступа, созданных компилятором.
Ссылка: Подводные камни Java: Доступ к полям во внутренних классах от нашего партнера по JCG Joonas Javanainen в техническом блоге Jawsy Solutions .