Страницы

Поиск по вопросам

четверг, 2 января 2020 г.

Как реализованы ссылки на методы в Java 8 внутри JVM?

#java #lambda


Мне стало интересно, как реализованы ссылки на методы(::) в Java 8.
Для сравнения опишу, как они реализованы в Object Pascal - простейшим образом и эффективно.
Указатель на метод в нем это запись из двух указателей:

TMethod = record
  Code, Data: Pointer;
end;


В одном хранится адрес процедуры, в другом - адрес объекта-владельца.

Button1.OnClick = MyClick1;


транслируется всего лишь в копирование двух указателей.
Вызов такого метода - это вызов процедуры по адресу, с передачей неявного параметра this.

Но мне здесь сказали, что в Java 8 ссылки на методы это синтаксический сахар и что
за ними скрывается что то страшное. Неужели это и правда так? Кто нибудь смотрел, в
какой код на JVM транслируются ссылки на методы?
    


Ответы

Ответ 1



В Java ссылки на методы - это сокращенные лямбда-выражения: Всего существует 3 конструкции ссылок на методы: object::instanceMethod - ссылается на объектный метод предложенного объекта Class::staticMethod - ссылается на статический метод класса Class::instanceMethod - ссылается на объектный метод предложенного объекта. Действует, так же как в п.1, только с именем класса Примеры реализации этих конструкций и их лямбда-аналоги ниже: System.out::println равно x -> System.out.println(x) Math::max равно (x,y) -> Math.max(x,y) String::length равно x -> x.length() Но все таки, ссылки на методы реализуются немного по-разному(об этом ниже). По сути, лямбда выражения похожи на анонимные классы с одним методом, но реализованы по другому. Подробный разбор их реализации - ниже. Краткая выборка из статьи, про то, как лямбды работают под капотом JVM: Анонимные внутренние классы имеют нежелательные характеристики, которые могут повлиять на производительность вашего приложения. Сначала компилятор генерирует новый файл класса для каждого анонимного внутреннего класса. Создание многих файлов классов нежелательно, поскольку каждый файл класса должен быть загружен и проверен перед использованием, что влияет на производительность при запуске приложения. Если бы лямбды были переведены в анонимные внутренние классы, у вас был бы новый файл класса для каждой лямбды. Как следствие, анонимные внутренние классы увеличат потребление памяти вашим приложением. Вместо использования для лямбда-выражений отдельного класса версия Java 8 опирается на байткод-инструкцию invokedynamic, добавленную в версии Java 7. Инструкция invokedynamic ориентирована на bootstrap-метод, который, в свою очередь, создает реализацию лямбда-выражения при первом вызове этого метода. Перевод лямбда-выражения в байт-код выполняется в два этапа: Генерируется динамическая лямбда-фабрика, которая при вызове возвращает экземпляр функционального интерфейса, в который преобразовывается лямбда. Тело лямбда-выражения преобразовывается в метод, который будет вызываться с помощью invokedynamic. В случае ссылок на методы все происходит почти также, как и для лямбд - но javac не генерирует синтаксический метод(потому что он уже есть в классе из которого вызывается метод, а у лямбды такого класса нет), и может ссылаться на нужный метод напрямую. Чтобы проиллюстрировать первый шаг, давайте рассмотрим байткод, сгенерированный из простого класса, содержащего лямбду: import java.util.function.Function; public class Lambda { Function f = s -> Integer.parseInt(s); } Этот класс сгенерируется в следующий байткод: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: aload_0 5: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function; 10: putfield #3 // Field f:Ljava/util/function/Function; 13: return То, как будет выполнен второй шаг, зависит от того, является ли лямбда-выражение необработанным(лямбда не обращается ни к каким переменным вне ее тела), или замыкающим(лямбда получает доступ к переменным вне ее тела). Примечание: замыкание - когда в лямбда выражении используется переменная, объявленная вне этого выражения. В первом случае, лямбда-выражения просто превращаются в статический метод, имеющий точно такую ​​же сигнатуру лямбда-выражения, и объявляются в том же классе, где используется лямбда-выражение. Например, лямбда-выражение, объявленное в классе Lambda выше, может быть преобразовано в метод, подобный этому: static Integer lambda$1(String s) { return Integer.parseInt(s); } Случай замыкания лямбда-выражения немного сложнее, поскольку замыкающие переменные должны быть переданы методу, реализующему тело лямбда-выражения вместе с формальными аргументами лямбда-выражения. В этом случае общая стратегия заключается в использования аргументов лямбда-выражения с дополнительным аргументом для каждой внешней переменной. Давайте посмотрим на практический пример: int offset = 100; Function f = s -> Integer.parseInt(s) + offset; Соответствующая реализация метода будет сгенерирована так: static Integer lambda$1(int offset, String s) { return Integer.parseInt(s) + offset; } Также есть цитата Брайна Гетца из этого ответа на enSO: Когда компилятор встречает лямбда-выражение, он сначала понижает тело лямбды в метод(аналогичный лямбде), возможно, с некоторыми дополнительными аргументами(если лямбда замыкающая). В момент, когда лямбда-выражение будет захвачено, оно генерирует место динамического вызова(dynamic call site), которое при вызове возвращает экземпляр функционального интерфейса, в который преобразовывалось лямбда-выражение. Это место вызова называется лямбда-фабрикой(lambda-factory) для данной лямбды. Динамические аргументы лямбда-фабрики - это значения, полученные из лексического контекста. Метод начальной загрузки(bootstrap method) фабрики лямбда-выражений - это стандартизированный метод в библиотеке рантайма языка Java, который называется метафабрикой лямбда-выражения. Ссылки на методы обрабатываются так же, как и лямбда-выражения, за исключением того, что большинство ссылок на методы не нужно вводить в новый метод; мы можем просто загрузить дескриптор для ссылочного метода и передать его метафабрике. Об invokedynamic(с Хабра): Широко известно, что в виртуальной машине Java, начиная с версии 7, есть интересная инструкция invokedynamic (она же indy). Про неё многие слышали, однако мало кто знает, что она делает на самом деле. Кто-то знает, что она используется при компиляции лямбда-выражений и ссылок на методы в Java 8. Некоторые слышали, что через неё реализована конкатенация строк в Java 9. Но хотя это полезные применения indy, изначальная цель всё же немного другая: делать динамический вызов, при котором вы можете вызывать разный код в одном и том же месте. В качестве источников я использовал две статьи - доклад Java 8 Lambdas A Peek Under the Hood и статью от IBM про лямбды(а точнее, последние абзацы). В моем ответе лишь краткая выжимка из доклада, поэтому для понимания работы лямбд очень советую его прочитать. Также есть доклад про invokedynamic с конференции JUG.ru.

Комментариев нет:

Отправить комментарий