В статье Некоторые предложения о 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 предупреждает вас об этом:
Этот снимок экрана показывает строку # 7 предыдущего исходного кода Java, где Eclipse сообщает вам, что хотя код «InnerInner.b = null;» выглядит как простое присваивание, оно будет выполнено как вызов метода.
К счастью, вы можете настроить Eclipse так, чтобы это предупреждение не отображалось.
Синтетические методы
Если вас больше интересуют синтетические и бридж-методы, прочитайте предыдущую статью о Java Deep.
Должны ли мы избегать частных переменных внутри внутренних классов?
Последний вопрос после того, как мы рассмотрели эту деталь языка Java, заключается в том, насколько серьезно мы должны беспокоиться о предупреждении, которое нам дает Eclipse. Должны ли мы использовать частные вложенные классы или мы должны избегать их?
Если мы их используем, то сгенерированный код JVM будет засорен синтетическими методами, и, следовательно, выполнение будет более сложным. Если мы их не используем, то сгенерированный код JVM будет проще. Должен ли я беспокоиться о простоте и крутости сгенерированной JVM? Я сомневаюсь.
Я хотел бы сосредоточиться на удобочитаемости кода, который мы поддерживаем, а не на коде, который генерирует javac.