Статьи

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

Если вы когда-либо играли с отражением и выполняли getDeclaredMethods (), вы можете быть удивлены. Вы можете получить методы, которых нет в исходном коде. Или, возможно, вы взглянули на модификаторы некоторых методов и увидели, что некоторые из этих специальных методов являются изменчивыми. Кстати, это неприятный вопрос для интервью на Java: «Что это значит, когда метод изменчив?» Правильный ответ: метод не может быть изменчивым. В то же время среди методов, возвращаемых getDeclaredMethods () или evengetMethods (), может быть какой-то метод, для которого Modifier.isVolatile (method.getModifiers ()) имеет значение true.

Это случилось с одним из  пользователей имутатора проекта . Он понял, что имутатор (который сам по себе довольно глубоко разбирается во мрачных деталях Java) генерирует исходный код Java, который нельзя скомпилировать, используя ключевое слово volatile в качестве модификатора для метода. Как следствие, это тоже не сработало.

Что там произошло? Каковы мост и синтетические методы?

видимость

Когда вы создаете вложенный или встроенный класс, закрытые переменные и методы вложенного класса доступны из класса верхнего уровня. Используется  неизменным встроенным шаблоном компоновщика . Это хорошо определенное поведение Java, определенное в спецификации языка.

JLS7, 6.6.1 Определение доступности

… Если член или конструктор объявлен закрытым, тогда доступ
разрешается тогда и только тогда, когда он происходит в теле класса верхнего уровня (§7.6),
который включает в себя объявление члена или конструктора…

package synthetic;
public class SyntheticMethodTest1 {
private A aObj = new A();
public class A {
private int i;
}
private class B {
private int i = aObj.i;
}
public static void main(String[] args) {
SyntheticMethodTest1 me = new SyntheticMethodTest1();
me.aObj.i = 1;
B bObj = me.new B();
System.out.println(bObj.i);
}
}

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

$ ls -Fart
../  SyntheticMethodTest2$A.class  MyClass.java  SyntheticMethodTest4.java  SyntheticMethodTest2.java
SyntheticMethodTest2.class  SyntheticMethodTest3.java  ./  MyClassSon.java  SyntheticMethodTest1.java

Если вы создадите вложенный или внутренний класс, он будет скомпилирован в полноценный класс верхнего уровня.

Как приватные поля будут доступны из внешнего класса? Если они попадают в класс высшего уровня и являются частными, как они есть на самом деле, то как они будут доступны из внешнего класса?

Способ, которым javac решает эту проблему, заключается в том, что для любого поля, метода или конструктора, которые являются частными, но используются из класса верхнего уровня, он генерирует синтетический метод. Эти синтетические методы используются для достижения оригинального частного filed / method / constructor. Генерация этих методов производится умным способом: генерируются только те, которые действительно необходимы и используются извне.

package synthetic;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class SyntheticMethodTest2 {
public static class A {
private A(){}
private int x;
private void x(){};
}
public static void main(String[] args) {
A a = new A();
a.x = 2;
a.x();
System.out.println(a.x);
for (Method m : A.class.getDeclaredMethods()) {
System.out.println(String.format("%08X", m.getModifiers()) + " " + m.getName());
}
System.out.println("--------------------------");
for (Method m : A.class.getMethods()) {
System.out.println(String.format("%08X", m.getModifiers()) + " " + m.getReturnType().getSimpleName() + " " + m.getName());
}
System.out.println("--------------------------");
for( Constructor<?> c : A.class.getDeclaredConstructors() ){
System.out.println(String.format("%08X", c.getModifiers()) + " " + c.getName());
}
}
}

Поскольку имя сгенерированных методов зависит от реализации и не гарантируется, самое большее, что я могу сказать о выходных данных вышеупомянутой программы, это то, что на конкретной платформе, где я выполнял, она выдает следующий вывод:

2
00001008 access$1
00001008 access$2
00001008 access$3
00000002 x
--------------------------
00000111 void wait
00000011 void wait
00000011 void wait
00000001 boolean equals
00000001 String toString
00000101 int hashCode
00000111 Class getClass
00000111 void notify
00000111 void notifyAll
--------------------------
00000002 synthetic.SyntheticMethodTest2$A
00001000 synthetic.SyntheticMethodTest2$A

В приведенной выше программе мы присваиваем значение полю x и также вызываем метод с тем же именем. Они необходимы для запуска компилятора для генерации синтетических методов. Вы можете видеть, что он сгенерировал три метода, предположительно установщик и получатель для поля x и синтетический метод для метода x (). Эти синтетические методы, однако, не перечислены в следующем списке, возвращаемом getMethods (), поскольку они являются синтетическими методами и поэтому не доступны для универсального вызова. В этом смысле они являются частными методами.

Числа гекса могут быть интерпретатором, глядя на константы, определенные в классе java.lang.reflect.Modifier:

00001008 SYNTHETIC|STATIC
00000002 PRIVATE
00000111 NATIVE|FINAL|PUBLIC
00000011 FINAL|PUBLIC
00000001 PUBLIC
00001000 SYNTHETIC

В списке есть два конструктора. Есть частный и синтетический. Приват существует, так как мы его определили. Синтетическое, с другой стороны, существует, потому что мы вызывали приватное извне. Мостовых методов, однако, пока не было.

Дженерики и наследование

Пока все хорошо, но мы все еще не видели никаких «изменчивых» методов.

Глядя на исходный код java.lang.reflec.Modifier, вы можете увидеть, что константа 0x00000040 определяется дважды. Однажды как VOLATILE и один раз как BRIDGE (последний является частным пакетом и не предназначен для общего пользования).

Чтобы иметь такой метод, очень простая программа сделает:

package synthetic;
import java.lang.reflect.Method;
import java.util.LinkedList;
public class SyntheticMethodTest3 {
public static class MyLink extends LinkedList<String> {
@Override
public String get(int i) {
return "";
}
}
public static void main(String[] args) {
for (Method m : MyLink.class.getDeclaredMethods()) {
System.out.println(String.format("%08X", m.getModifiers()) + " " + m.getReturnType().getSimpleName() + " " + m.getName());
}
}
}

У нас есть связанный список, в котором есть метод get (int), возвращающий String. Давайте не будем обсуждать проблемы с чистым кодом. Это пример кода для демонстрации темы. Те же проблемы возникают и в чистом коде, хотя сложнее и сложнее добраться до того момента, когда это вызывает проблему.

Выход говорит

00000001 String get
00001041 Object get

у нас есть два метода get (). Тот, который появляется в исходном коде, а другой — синтетический и мостовой. Декомпилятор javap говорит, что сгенерированный код:

public java.lang.String get(int);
Code:
Stack=1, Locals=2, Args_size=2
0:  ldc  #2; //String
2:  areturn
LineNumberTable:
line 12: 0
public java.lang.Object get(int);
Code:
Stack=2, Locals=2, Args_size=2
0:  aload_0
1:  iload_1
2:  invokevirtual  #3; //Method get:(I)Ljava/lang/String;
5:  areturn

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

Зачем нам этот синтетический метод? Кто будет этим пользоваться. Например, код, который хочет вызвать metadget (int), используя переменную, которая не имеет тип MyLink:

List<?> a = new MyLink();
Object z = a.get(0);

Он не может вызвать метод, возвращающий String, потому что в List его нет. Чтобы сделать его более наглядным, давайте переопределим метод add () вместо get ():

package synthetic;
import java.util.LinkedList;
import java.util.List;
public class SyntheticMethodTest4 {
public static class MyLink extends LinkedList<String> {
@Override
public boolean add(String s) {
return true;
}
}
public static void main(String[] args) {
List a = new MyLink();
a.add("");
a.add(13);
}
}

Мы можем видеть, что метод моста

public boolean add(java.lang.Object);
Code:
Stack=2, Locals=2, Args_size=2
0:  aload_0
1:  aload_1
2:  checkcast  #2; //class java/lang/String
5:  invokevirtual  #3; //Method add:(Ljava/lang/String;)Z
8:  ireturn

не только называет оригинал. Он также проверяет, что преобразование типов в порядке. Это делается во время выполнения, а не самой JVM. Как и следовало ожидать, он вырвет в строке 18:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at synthetic.SyntheticMethodTest4$MyLink.add(SyntheticMethodTest4.java:1)
at synthetic.SyntheticMethodTest4.main(SyntheticMethodTest4.java:18)

Когда вы в следующий раз получите вопрос об изменчивых методах, вы можете знать даже больше, чем интервьюер.