Статьи

Еще несколько предложений на Java, частные переменные

В статье  Некоторые предложения о Java  я описал, что приватные переменные видны из внутренних и вложенных классов, а также наоборот. Это делается на уровне Java, это особенность языка. Однако, зная, что JVM ничего не знает о внутренних и вложенных классах, можно задаться вопросом, как это реализовано.

Нет внутренних классов в JVM

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

Когда есть класс внутри другого класса или интерфейса, компилятор генерирует дополнительные файлы .class для этих классов. Имя этих классов обычно содержит имя внешнего класса, знак $ и имя внутреннего или вложенного класса. Поскольку знак $ является допустимым символом в идентификаторе в Java, JVM не может сказать и не пытается определить, является ли класс внутренним или классом верхнего уровня на уровне языка Java.

Частный не частный тогда?

Частные методы и поля таким образом доступны между классами. Поскольку JVM не знает вложенности классов, для JVM это классы «верхнего уровня».

В качестве примера приведем код Java:

package com.javax0;
public class Inner {
    private static class InnerInner {
        private static Object b;
    }
    public void m() {
        InnerInner.b = null;
    }
}

Если я скомпилирую код с командной строкой

$ javac  Inner.java
$ ls -1
Inner$InnerInner.class
Inner.class
Inner.java

Я получаю два файла классов. Разборка внутреннего класса:

$ javap -v Inner\$InnerInner.class 
Classfile /Users/verhasp/github/JavaBeanTester/src/test/java/com/javax0/Inner$InnerInner.class
  Last modified 2014.12.27.; size 413 bytes
  MD5 checksum 79f4ea55abe8211fec751d9a4dec9ae1
  Compiled from "Inner.java"
class com.javax0.Inner$InnerInner
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Fieldref           #3.#15         // com/javax0/Inner$InnerInner.b:Ljava/lang/Object;
   #2 = Methodref          #4.#16         // java/lang/Object."<init>":()V
   #3 = Class              #18            // com/javax0/Inner$InnerInner
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               b
   #6 = Utf8               Ljava/lang/Object;
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               access$002
  #12 = Utf8               (Ljava/lang/Object;)Ljava/lang/Object;
  #13 = Utf8               SourceFile
  #14 = Utf8               Inner.java
  #15 = NameAndType        #5:#6          // b:Ljava/lang/Object;
  #16 = NameAndType        #7:#8          // "<init>":()V
  #17 = Class              #22            // com/javax0/Inner
  #18 = Utf8               com/javax0/Inner$InnerInner
  #19 = Utf8               InnerInner
  #20 = Utf8               InnerClasses
  #21 = Utf8               java/lang/Object
  #22 = Utf8               com/javax0/Inner
{
  static java.lang.Object access$002(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: dup
         2: putstatic     #1                  // Field b:Ljava/lang/Object;
         5: areturn
      LineNumberTable:
        line 4: 0
}
SourceFile: "Inner.java"
$ 

Вы можете заметить, что был создан синтетический метод для установки значения приватного поля b. Этот метод необходим для доступа к полю из внешнего класса. JVM запрещает прямой доступ, но компилятор позволяет вам получить доступ к полю с помощью синтетических методов, которые он генерирует. Однако вы не можете вызывать синтетический метод из любого другого класса, потому что компилятор Java откажется компилировать код, который пытается получить прямой доступ к синтетическому методу. Эта защита работает на уровне компилятора. Если вам случится создать код JVM напрямую с помощью специального инструмента, который обращается к синтетическому методу, JVM не остановит вас.

Eclipse предупреждения

Есть еще один признак, с которым вы можете столкнуться при создании классов, который заставляет компилятор создавать синтетические методы. Это когда Eclipse предупреждает вас об этом:

Fullscreen_2014__12__27__19_00

Этот снимок экрана показывает строку # 7 предыдущего исходного кода Java, где Eclipse сообщает вам, что хотя код «InnerInner.b = null;» выглядит как простое присваивание, оно будет выполнено как вызов метода.

К счастью, вы можете настроить Eclipse так, чтобы это предупреждение не отображалось.

Синтетические методы

Если вас больше интересуют синтетические и бридж-методы, прочитайте  предыдущую статью  о Java Deep.

Должны ли мы избегать частных переменных внутри внутренних классов?

Последний вопрос после того, как мы рассмотрели эту деталь языка Java, заключается в том, насколько серьезно мы должны беспокоиться о предупреждении, которое нам дает Eclipse. Должны ли мы использовать частные вложенные классы или мы должны избегать их?

Если мы их используем, то сгенерированный код JVM будет засорен синтетическими методами, и, следовательно, выполнение будет более сложным. Если мы их не используем, то сгенерированный код JVM будет проще. Должен ли я беспокоиться о простоте и крутости сгенерированной JVM? Я сомневаюсь.

Я хотел бы сосредоточиться на удобочитаемости кода, который мы поддерживаем, а не на коде, который генерирует javac.