Статьи

Подводные камни Java: доступ к полям во внутренних классах

Это не «подводный камень» как таковой, но деталь реализации, которую стоит знать. Допустим, у меня есть внутренний класс с полем. Такое поле видно классу включения, но какой из следующих способов является самым быстрым способом доступа к нему? Запись! Я смотрю здесь только на сгенерированный байт-код, а не на оптимизацию JIT, поэтому этот «анализ производительности» очень наивен.

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 .