Если вы когда-либо играли с отражением и выполняли 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)
Когда вы в следующий раз получите вопрос об изменчивых методах, вы можете знать даже больше, чем интервьюер.