Статьи

JEP 181 несовместимость, классы вложенности / 2

JEP 181 — это основанный на гнезде контроль доступа https://openjdk.java.net/jeps/181 . Он был представлен в Java 11 и намеренно внес несовместимость с предыдущими версиями. Это хороший пример того, что совместимость с предыдущими версиями Java — это не правило, высеченное в камне, а скорее сохранение последовательности и устойчивого развития языка. В этой статье я расскажу об изменениях на примере, с которым я столкнулся несколько лет назад, и о том, как Java 11 делает жизнь проще и последовательнее в этом особом случае.

Обратная совместимость Java ограничена функциями, а не поведением

Исходная ситуация

Несколько лет назад, когда я написал интерпретатор ScriptBasic для Java, который можно расширить с помощью методов Java, чтобы они были доступны так же, как если бы они были написаны на BASIC, я создал несколько модульных тестов. Класс модульного теста содержал некоторый внутренний класс, в котором был какой-то метод, доступный для кода BASIC. Внутренний класс был статическим и закрытым, так как он не имел никакого отношения к каким-либо другим классам, кроме теста, однако класс и методы все еще были доступны для тестового кода, поскольку они находились в одном классе. К моему ужасу, методы не были доступны через программы BASIC. Когда я попытался вызвать методы через интерпретатор BASIC, который сам использовал рефлексивный доступ, я получил IllegalAccessException .

Чтобы исправить ситуацию, я создал следующий простой код после нескольких часов отладки и изучения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
package javax0;
 
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
 
public class ReflThrow {
    private class Nested {
        private void m(){
            System.out.println("m called");
        }
    }
    public static void main(String[] args)
            throws NoSuchMethodException,
            InvocationTargetException,
            IllegalAccessException {
        ReflThrow me = new ReflThrow();
        Nested n = me.new Nested();
        n.m();
        Method m = Nested.class.getDeclaredMethod("m");
        m.invoke(n);
    }
}

Если вы запустите этот код с Java N, где N <11, то вы получите нечто похожее на это:

1
2
3
4
5
m called
Exception in thread "main" java.lang.IllegalAccessException: class ReflThrow cannot access a member of class ReflThrow$Nested with modifiers "private"
    at java.base/jdk.internal.reflect.Reflection.throwIllegalAccessException(Reflection.java:423)
    at java.base/jdk.internal.reflect.Reflection.throwIllegalAccessException(Reflection.java:414)
...

Тем не менее, он прекрасно работает с Java 11 (и, вероятно, он также будет работать с более поздними версиями Java).

объяснение

До версии 11 Java JVM не обрабатывала внутренние и вложенные классы. Все классы в JVM являются классами верхнего уровня. Компилятор Java создает специально названный класс верхнего уровня из внутренних и вложенных классов. Например, один из компиляторов Java может создавать файлы классов ReflThrow.class и ReflThrow$Nested.class . Поскольку они являются классами верхнего уровня для JVM, код в классе ReflThrow не может вызывать закрытый метод m() объекта Nested когда они представляют собой два разных класса верхнего уровня.

На уровне Java, однако, когда эти классы создаются из вложенной структуры, это возможно. Чтобы это произошло, компилятор создает дополнительный синтетический метод внутри класса Nested который может вызывать код в ReflThrow и этот метод уже внутри Nested вызывает m() .

Синтетические методы имеют модификатор SYNTHETIC так что компилятор позже узнает, что другой код не должен «видеть» эти методы. Таким образом, вызов метода m() работает хорошо.
С другой стороны, когда мы пытаемся вызвать метод m() используя его имя и отражающий доступ, маршрут проходит непосредственно через границы класса, не вызывая никакого синтетического метода, и поскольку метод является закрытым для класса, в котором он находится, вызов бросает исключение.

Java 11 меняет это. JEP 181, включенный в уже выпущенную Java 11, вводит понятие гнездо. «Гнезда позволяют классам, которые логически являются частью одного и того же объекта кода, но скомпилированы в отдельные файлы классов, для доступа к закрытым членам друг друга без необходимости в компиляторах вставлять методы моста, расширяющие доступность». Это просто означает, что есть классы, которые являются гнездами, и есть классы, которые принадлежат гнезду. Когда код генерируется из Java, класс верхнего уровня является классом вложенности, а классы внутри являются вложенными. Эта структура на уровне JVM оставляет много места для различных языковых структур и не ставит октавы структуры Java в среду выполнения. JVM нацелена на то, чтобы стать полиглотом, и с появлением GraalVM в будущем она станет еще более полиглотной. JVM, использующая эту структуру, просто видит, что два класса находятся в одном и том же гнезде, поэтому они могут обращаться друг к другу в виде private методов, полей и других членов. Это также означает, что нет методов моста с различными ограничениями доступа, и поэтому отражение проходит через те же границы доступа, что и обычный вызов Java.

Резюме / Еда на вынос

Java не меняется в одночасье и в основном обратно совместима. Однако обратная совместимость ограничена функциями, а не поведением. JEP181 этого не сделал и никогда не намеревался воспроизвести не совсем совершенное поведение выброса IllegalAccessException отражающего доступа к вложенным классам. Такое поведение было скорее поведением / ошибкой реализации, нежели языковой особенностью, и было исправлено в Java 11.

Опубликовано на Java Code Geeks с разрешения Питера Верхаса, партнера нашей программы JCG . См. Оригинальную статью здесь: несовместимость JEP 181, классы вложенности / 2

Мнения, высказанные участниками Java Code Geeks, являются их собственными.