1. Введение
В этом руководстве мы рассмотрим вложения
, новый контекст управления доступом, представленный в Java 11.
2. До Java 11
2.1. Вложенные типы
Java позволяет вкладывать классы и интерфейсы друг в друга . Эти вложенные типы имеют неограниченный доступ друг к другу, в том числе к закрытым полям, методам и конструкторам.
Рассмотрим следующий пример вложенного класса:
public class Outer {
public void outerPublic() {
}
private void outerPrivate() {
}
class Inner {
public void innerPublic() {
outerPrivate();
}
}
}
Здесь, хотя метод externalPrivate()
является закрытым
, он доступен из метода innerPublic()
.
Мы можем описать тип верхнего уровня, а также все типы, вложенные в него, как образующие гнездо. Два члена гнезда описываются как соседи по гнезду.
Таким образом, в приведенном выше примере Outer
и Inner
вместе образуют гнездо и являются соседями друг друга.
2.2. Мостовой метод
Правила доступа JVM не разрешают частный доступ между соседями. В идеале мы должны получить ошибку компиляции для приведенного выше примера. Однако компилятор исходного кода Java разрешает доступ, вводя уровень косвенности.
Например, вызов закрытого члена компилируется в вызов сгенерированного компилятором закрытого для пакета связующего метода в целевом классе, который, в свою очередь, вызывает предполагаемый закрытый метод .
Это происходит за кулисами. Этот метод соединения немного увеличивает размер развернутого приложения и может запутать пользователей и инструменты.
2.3. Использование отражения
Еще одним следствием этого является то, что основное отражение также запрещает доступ. Это удивительно, учитывая, что рефлексивные вызовы должны вести себя так же, как и вызовы исходного кода.
Например, если мы попытаемся вызвать externalPrivate()
рефлексивно из класса Inner :
public void innerPublicReflection(Outer ob) throws Exception {
Method method = ob.getClass().getDeclaredMethod("outerPrivate");
method.invoke(ob);
}
Мы получим исключение:
java.lang.IllegalAccessException:
Class com.foreach.Outer$Inner can not access a member of class com.foreach.Outer with modifiers "private"
Java 11 пытается решить эти проблемы.
3. Контроль доступа на основе гнезда
Java 11 привносит понятие соплеменников и связанных с ними правил доступа в JVM . Это упрощает работу компиляторов исходного кода Java.
Для этого формат файла класса теперь содержит два новых атрибута:
- Один член гнезда (обычно класс верхнего уровня) назначается хостом гнезда. Он содержит атрибут (NestMembers) для идентификации других статически известных членов гнезда.
- У каждого из других членов гнезда есть атрибут (NestHost), чтобы идентифицировать его узел гнезда.
Таким образом, чтобы типы C
и D
были соседями по гнезду, у них должен быть один и тот же хост гнезда. Тип C
утверждает, что является членом гнезда, размещенного D
, если он перечисляет D
в своем атрибуте NestHost. Членство подтверждается, если D
также перечисляет C
в своем атрибуте NestMembers. Кроме того, тип D
неявно является членом гнезда, которое он размещает.
Теперь компилятору не нужно генерировать мостовые методы .
Наконец, управление доступом на основе гнезда устраняет неожиданное поведение основного отражения. Поэтому метод innerPublicReflection()
, показанный в предыдущем разделе, будет выполняться без каких-либо исключений.
4. API отражения Nestmate
Java 11 предоставляет средства для запроса новых атрибутов файла класса с использованием базового отражения . Класс java.lang.Class
содержит следующие три новых метода.
4.1. получитьNestHost()
Это возвращает узел гнезда гнезда, к которому принадлежит этот объект класса :
@Test
public void whenGetNestHostFromOuter_thenGetNestHost() {
is(Outer.class.getNestHost().getName()).equals("com.foreach.Outer");
}
@Test
public void whenGetNestHostFromInner_thenGetNestHost() {
is(Outer.Inner.class.getNestHost().getName()).equals("com.foreach.Outer");
}
Оба класса Outer
и Inner
принадлежат вложенному хосту com.foreach.Outer
.
4.2. isNestmateOf()
Это определяет, является ли данный класс
родственным этому объекту класса
:
@Test
public void whenCheckNestmatesForNestedClasses_thenGetTrue() {
is(Outer.Inner.class.isNestmateOf(Outer.class)).equals(true);
}
4.3. получитьNestMembers()
Это возвращает массив, содержащий объекты класса
, представляющие все члены гнезда, к которому принадлежит этот объект класса :
@Test
public void whenGetNestMembersForNestedClasses_thenGetAllNestedClasses() {
Set<String> nestMembers = Arrays.stream(Outer.Inner.class.getNestMembers())
.map(Class::getName)
.collect(Collectors.toSet());
is(nestMembers.size()).equals(2);
assertTrue(nestMembers.contains("com.foreach.Outer"));
assertTrue(nestMembers.contains("com.foreach.Outer$Inner"));
}
5. Детали компиляции
5.1. Метод моста до Java 11
Давайте углубимся в детали метода соединения, сгенерированного компилятором. Мы можем увидеть это, разобрав получившийся файл класса:
$ javap -c Outer
Compiled from "Outer.java"
public class com.foreach.Outer {
public com.foreach.Outer();
Code:
0: aload_0
1: invokespecial #2 // Method java/lang/Object."<init>":()V
4: return
public void outerPublic();
Code:
0: return
static void access$000(com.foreach.Outer);
Code:
0: aload_0
1: invokespecial #1 // Method outerPrivate:()V
4: return
}
Здесь, кроме конструктора по умолчанию и общедоступного метода externalPublic()
, обратите внимание на метод access$000()
. Компилятор генерирует это как метод соединения.
innerPublic ()
проходит через этот метод для вызова externalPrivate()
:
$ javap -c Outer\$Inner
Compiled from "Outer.java"
class com.foreach.Outer$Inner {
final com.foreach.Outer this$0;
com.foreach.Outer$Inner(com.foreach.Outer);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/foreach/Outer;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return
public void innerPublic();
Code:
0: aload_0
1: getfield #1 // Field this$0:Lcom/foreach/Outer;
4: invokestatic #3 // Method com/foreach/Outer.access$000:(Lcom/foreach/Outer;)V
7: return
}
Обратите внимание на комментарий в строке №19. Здесь innerPublic()
вызывает метод моста access$000()
.
5.2. Соседи с Java 11
Компилятор Java 11 сгенерирует следующий дизассемблированный файл класса Outer :
$ javap -c Outer
Compiled from "Outer.java"
public class com.foreach.Outer {
public com.foreach.Outer();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void outerPublic();
Code:
0: return
}
Обратите внимание, что нет метода соединения, сгенерированного компилятором. Кроме того, класс Inner
теперь может напрямую вызывать метод externalPrivate()
:
$ javap -c Outer\$Inner.class
Compiled from "Outer.java"
class com.foreach.Outer$Inner {
final com.foreach.Outer this$0;
com.foreach.Outer$Inner(com.foreach.Outer);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/foreach/Outer;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return
public void innerPublic();
Code:
0: aload_0
1: getfield #1 // Field this$0:Lcom/foreach/Outer;
4: invokevirtual #3 // Method com/foreach/Outer.outerPrivate:()V
7: return
}
6. Заключение
В этой статье мы рассмотрели управление доступом на основе вложений, представленное в Java 11.
Как обычно, фрагменты кода можно найти на GitHub .