Когда у вас есть класс внутри другого класса, они могут видеть private
методы друг друга. Это не очень хорошо известно среди разработчиков Java. Многие кандидаты во время интервью говорят, что private
— это видимость, которая позволяет коду видеть члена, если он находится в одном классе. Это действительно так, но было бы точнее сказать, что существует класс, в котором находится и код, и член. Когда у нас есть вложенные и внутренние классы, может случиться так, что private
член и код, использующий его, находятся в того же класса, и в то же время они также в разных классах.
Например, если у меня есть два вложенных класса в классе верхнего уровня, то код в одном из вложенных классов может видеть private
член другого вложенного класса.
Это становится интересным, когда мы смотрим на сгенерированный код. JVM не заботится о классах внутри других классов. Он имеет дело с классами высшего уровня JVM. Компилятор создаст .class
которые будут иметь имя, подобное A$B.class
когда у вас есть класс с именем B
внутри класса A
В B
есть private
метод, вызываемый из A
после чего JVM видит, что код в A.class
вызывает метод в A$B.class
. JVM проверяет контроль доступа. Когда мы обсуждали это с юниорами, кто-то предположил, что, вероятно, JVM не заботится о модификаторе. Это неправда. Попробуйте скомпилировать A.java
и B.java
, два класса верхнего уровня с некоторым кодом в A
вызывающем public
метод в B
Если у вас есть A.class
и B.class
измените метод в B.java
, чтобы он был public
чтобы он был private
и перекомпилируйте B
ta новый B.class
. Запустите приложение, и вы увидите, что JVM очень заботится о модификаторах доступа. Тем не менее, вы можете вызвать в примере выше из A.class
метод из A$B.class
.
Чтобы разрешить этот конфликт, Java генерирует дополнительные синтетические методы, которые по своей природе являются общедоступными, вызывают оригинальный закрытый метод внутри того же класса и могут вызываться, если рассматривать управление доступом JVM. С другой стороны, компилятор Java не будет компилировать код, если вы выясните имя сгенерированного метода и попытаетесь вызвать его непосредственно из исходного кода Java. Я писал о деталях более 4 лет назад.
Если вы опытный разработчик, то вы, вероятно, думаете, что это странный и отвратительный взлом. Ява такая чистая, элегантная, лаконичная и чистая, кроме этого хака. А также, возможно, взлом кеша Integer
который делает небольшие объекты Integer
(типичные тестовые значения) равными, используя ==
тогда как большие значения равны только equals()
но не ==
(типичные производственные значения). Но кроме синтетических классов и взлома кеша в Integer
Java чист, элегантен, лаконичен и чист. (Вы можете понять, что я фанат Монти Пайтона.)
Причина этого в том, что вложенные классы не были частью оригинальной Java, они были добавлены только к версии 1.1. Решение было взломано, но в то время были более важные вещи, такие как внедрение JIT-компилятора, JDBC, RMI, отражение и некоторые другие вещи, которые мы принимаем сегодня как должное. Тогда не было вопроса, является ли решение красивым и чистым. Скорее вопрос был в том, выживет ли Java вообще и станет ли он основным языком программирования или умрет, и остается хорошей попыткой. В то время я все еще работал торговым представителем, а кодирование было лишь хобби, потому что в Восточной Европе работа по кодированию была скудной, в основном это были скучные бухгалтерские приложения и низкая зарплата. Это были немного другие времена, поисковая система называлась AltaVista, мы пили воду из-под крана, а у Java были разные приоритеты.
Следствием этого является то, что в течение более 20 лет у нас есть несколько большие JAR-файлы, немного медленное выполнение Java (если JIT не оптимизирует цепочку вызовов) и неприятные предупреждения в IDE, свидетельствующие о том, что у нас лучше использовать методы защиты пакетов во вложенных классах, а не private
когда мы используем его из верхнего уровня или других вложенных классов.
Гнездо Хозяев
Теперь кажется, что этот 20-летний технический долг будет решен. Http://openjdk.java.net/jeps/181 попадает в Java 11, и он решит эту проблему, введя новое понятие: гнездо. В настоящее время байт-код Java содержит некоторую информацию об отношениях между классами. JVM имеет информацию о том, что определенный класс является вложенным классом другого класса, и это не только имя. Эта информация может помочь JVM принять решение о том, разрешен ли фрагмент кода в одном классе или нет доступа к private
члену другого класса, но разработка JEP-181 имеет нечто более общее. С течением времени JVM больше не является виртуальной машиной Java. Ну, да, это, по крайней мере, название, однако, это виртуальная машина, которая выполняет байт-код, скомпилированный из Java. Или, кстати, из некоторых других языков. Существует много языков, нацеленных на JVM, и помня, что JEP-181 не хочет связывать новую функцию контроля доступа в JVM с определенной функцией языка Java.
JEP-181 определяет понятие NestHost
и NestMembers
как атрибуты класса. Компилятор заполняет эти поля, и когда есть доступ к закрытому члену класса из другого класса, тогда контроль доступа JVM может проверить: два класса находятся в одном гнезде или нет? Если они находятся в одном и том же гнезде, то доступ разрешен, иначе нет. У нас будут методы, добавленные к отражающему доступу, чтобы мы могли получить список классов, которые находятся в гнезде.
Пример простого гнезда
С помощью
1
2
3
4
|
$ java -version java version "11-ea" 2018-09-25 Java(TM) SE Runtime Environment 18.9 (build 11-ea+25) Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11-ea+25, mixed mode) |
Версия Java сегодня мы можем сделать уже эксперименты. Мы можем создать простой класс:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
package nesttest; public class NestingHost { public static class NestedClass1 { private void privateMethod() { new NestedClass2().privateMethod(); } } public static class NestedClass2 { private void privateMethod() { new NestedClass1().privateMethod(); } } } |
Довольно просто и ничего не делает. Закрытые методы вызывают друг друга. Без этого компилятор видит, что они просто ничего не делают и не нужны, а байт-код их просто не содержит.
Класс для чтения информации о вложенности
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
package nesttest; import java.util.Arrays; import java.util.stream.Collectors; public class TestNest { public static void main(String[] args) { Class host = NestingHost.class.getNestHost(); Class[] nestlings = NestingHost.class.getNestMembers(); System.out.println( "Mother bird is: " + host); System.out.println( "Nest dwellers are :\n" + Arrays.stream(nestlings).map(Class::getName) .collect(Collectors.joining( "\n" ))); } } |
Распечатка, как и ожидалось:
1
2
3
4
5
|
Mother bird is: class nesttest.NestingHost Nest dwellers are : nesttest.NestingHost nesttest.NestingHost$NestedClass2 nesttest.NestingHost$NestedClass1 |
Обратите внимание, что узел вложенности также указан среди членов гнезда, хотя эта информация должна быть достаточно очевидной и избыточной. Однако такое использование может позволить некоторым языкам раскрывать из доступа частные члены самого узла размещения и разрешать доступ только для птенцов.
Байт код
Компиляция с использованием компилятора JDK11 генерирует файлы
-
NestingHost$NestedClass1.class
-
NestingHost$NestedClass2.class
-
NestingHost.class
-
TestNest.class
Там нет изменений. С другой стороны, если мы посмотрим на байт-код с javap
декомпилятора javap
то увидим следующее:
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
|
$ javap - v build /classes/java/main/nesttest/NestingHost \$NestedClass1.class Classfile ... /packt/Fundamentals-of-java-18 .9 /sources/ch08/bulkorders/build/classes/java/main/nesttest/NestingHost $NestedClass1.class Last modified Aug 6, 2018; size 557 bytes MD5 checksum 5ce1e0633850dd87bd2793844a102c52 Compiled from "NestingHost.java" public class nesttest.NestingHost$NestedClass1 minor version: 0 major version: 55 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #5 // nesttest/NestingHost$NestedClass1 super_class: #6 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 3 Constant pool: *** CONSTANT POOL DELETED FROM THE PRINTOUT *** { public nesttest.NestingHost$NestedClass1(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lnesttest /NestingHost $NestedClass1; } SourceFile: "NestingHost.java" NestHost: class nesttest /NestingHost InnerClasses: public static #13= #5 of #20; // NestedClass1=class nesttest/NestingHost$NestedClass1 of class nesttest/NestingHost public static #23= #2 of #20; // NestedClass2=class nesttest/NestingHost$NestedClass2 of class nesttest/NestingHost |
Если мы скомпилируем тот же класс, используя компилятор JDK10, то строки дизассемблирования будут следующими:
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
47
48
49
|
$ javap - v build /classes/java/main/nesttest/NestingHost \$NestedClass1.class Classfile /C : /Users/peter_verhas/Dropbox/packt/Fundamentals-of-java-18 .9 /sources/ch08/bulkorders/build/classes/java/main/nesttest/NestingHost $NestedClass1.class Last modified Aug 6, 2018; size 722 bytes MD5 checksum 8c46ede328a3f0ca265045a5241219e9 Compiled from "NestingHost.java" public class nesttest.NestingHost$NestedClass1 minor version: 0 major version: 54 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #6 // nesttest/NestingHost$NestedClass1 super_class: #7 // java/lang/Object interfaces: 0, fields: 0, methods: 3, attributes: 2 Constant pool: *** CONSTANT POOL DELETED FROM THE PRINTOUT *** { public nesttest.NestingHost$NestedClass1(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #2 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lnesttest /NestingHost $NestedClass1; static void access$100(nesttest.NestingHost$NestedClass1); descriptor: (Lnesttest /NestingHost $NestedClass1;)V flags: (0x1008) ACC_STATIC, ACC_SYNTHETIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method privateMethod:()V 4: return LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 x0 Lnesttest /NestingHost $NestedClass1; } SourceFile: "NestingHost.java" InnerClasses: public static #14= #6 of #25; // NestedClass1=class nesttest/NestingHost$NestedClass1 of class nesttest/NestingHost public static #27= #3 of #25; // NestedClass2=class nesttest/NestingHost$NestedClass2 of class nesttest/NestingHost |
Компилятор Java 10 генерирует метод access$100
. Компилятор Java 11 этого не делает. Вместо этого в файле классов есть поле размещения вложенности. Мы наконец избавились от тех синтетических методов, которые вызывали сюрпризы, когда перечислили все методы в некотором рефлексивном коде платформы.
Взломать гнездо
Давайте немного поиграем с кукушкой. Мы можем немного изменить код, чтобы он теперь что-то делал:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
package nesttest; public class NestingHost { // public class NestedClass1 { // public void publicMethod() { // new NestedClass2().privateMethod(); /* <-- this is line 8 */ // } // } public class NestedClass2 { private void privateMethod() { System.out.println( "hallo" ); } } } |
мы также создаем простой тестовый класс
1
2
3
4
5
6
7
8
9
|
package nesttest; public class HackNest { public static void main(String[] args) { // var nestling =new NestingHost().new NestedClass1(); // nestling.publicMethod(); } } |
Сначала удалите все //
из начала строк и скомпилируйте проект. Это работает как шарм и печатает hallo
. После этого скопируйте сгенерированные классы в безопасное место, например, в корень проекта.
1
2
|
$ cp build /classes/java/main/nesttest/NestingHost \$NestedClass1.class . $ cp build /classes/java/main/nesttest/HackNest .class . |
Давайте скомпилируем проект, на этот раз с комментариями, а затем скопируем два файла классов из предыдущей компиляции:
1
2
|
$ cp HackNest.class build /classes/java/main/nesttest/ $ cp NestingHost\$NestedClass1.class build /classes/java/main/nesttest/ |
Теперь у нас есть NestingHost
который знает, что у него есть только один птенец: NestedClass2
. Однако тестовый код считает, что есть еще один птенец NestedClass1
и он также имеет открытый метод, который можно вызывать. Таким образом, мы пытаемся проникнуть в гнездо дополнительного птенца. Если мы выполним код, то получим ошибку:
1
2
3
4
|
$ java - cp build /classes/java/main/ nesttest.HackNest Exception in thread "main" java.lang.IncompatibleClassChangeError: Type nesttest.NestingHost$NestedClass1 is not a nest member of nesttest.NestingHost: current type is not listed as a nest member at nesttest.NestingHost$NestedClass1.publicMethod(NestingHost.java:8) at nesttest.HackNest.main(HackNest.java:7) |
Из кода важно признать, что строка, которая вызывает ошибку, является той, где мы хотим вызвать приватный метод. Среда выполнения Java выполняет проверку только в этот момент и не раньше.
Нравится нам это или нет? Где принцип быстрого отказа? Почему среда выполнения Java начинает выполнять класс и проверять структуру гнезда только тогда, когда это очень необходимо? Причина, как много раз в случае с Java: обратная совместимость. JVM может проверить согласованность структуры гнезда, когда все классы загружены. Классы загружаются только тогда, когда они используются. Было бы возможно изменить загрузку классов в Java 11 и загрузить все вложенные классы вместе с хостом, но это нарушило бы обратную совместимость. Если бы не что иное, ленивый шаблон синглтона развалился бы, и мы этого не хотим. Мы любим синглтон, но только когда односолодовый (это так).
Вывод
JEP-181 — это небольшое изменение в Java. Большинство разработчиков даже не заметят. Это устраненный технический долг, и если основной Java-проект не устраняет технический долг, то чего нам ожидать от среднего разработчика?
Как гласит старая латинская поговорка: «Debitum technica necesse est deletur».
Опубликовано на Java Code Geeks с разрешения Питера Верхаса, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Вложенные классы и частные методы
Мнения, высказанные участниками Java Code Geeks, являются их собственными. |