Middle+
45
questionbank

За счет чего работают лямбда-выражения и что происходит 'под капотом'?

Получайте ответы на вопросы в реальном времени с Sobes Copilot
Ответ от нейросети sobes.tech

Лямбда-выражения в Java работают за счет следующих механизмов:

  1. Функциональные интерфейсы: Лямбда-выражение должно быть присвоено переменной, возвращено из метода или передано в метод, тип которого является функциональным интерфейсом. Функциональный интерфейс — это интерфейс с ровно одним абстрактным методом (SAM - Single Abstract Method). Аннотация @FunctionalInterface является опциональной, но рекомендуется для проверки компилятором.

  2. Инвокация динамики (invokeDynamic): Вместо генерации анонимного внутреннего класса в байткоде на этапе компиляции, как это делалось для старых версий анонимных классов, Java использует инструкцию invokeDynamic. Эта инструкция была добавлена в Java 7 для поддержки динамически типизированных языков на JVM, а в Java 8 используется для реализации лямбда-выражений и ссылок на методы. invokeDynamic откладывает разрешение вызова метода до момента выполнения (runtime).

Что происходит 'под капотом':

На этапе компиляции:

  • Компилятор анализирует лямбда-выражение.
  • Он определяет тип функционального интерфейса, к которому приводится лямбда.
  • Генерируется специальный синтетический метод (часто с префиксом lambda$) в том же классе, который содержит логику лямбда-выражения. Этот метод имеет такую же сигнатуру, как и абстрактный метод функционального интерфейса.
  • В месте использования лямбда-выражения компилятор генерирует инструкцию invokeDynamic. Эта инструкция содержит ссылку на bootstrap method (LambdaMetafactory.metafactory).

На этапе выполнения (runtime):

  • Когда JVM встречает инструкцию invokeDynamic, она первый раз вызывает bootstrap method (LambdaMetafactory.metafactory).
  • Bootstrap method (LambdaMetafactory.metafactory) динамически генерирует в памяти загрузчик вызова (call site).
  • Загрузчик вызова создает и возвращает экземпляр класса, который реализует функциональный интерфейс. Этот экземпляр может быть:
    • Создан на лету (например, с помощью сгенерированного Constant CallSite).
    • Кэширован для последующего использования (для stateless лямбд).
    • Представлен как ссылка на уже существующий экземпляр функционального интерфейса (например, для stateless лямбд, реализующих Serializable).
  • Этот сгенерированный класс делегирует вызов абстрактного метода функционального интерфейса сгенерированному компилятором синтетическому методу (lambda$).
  • После первого вызова bootstrap method, загрузчик вызова запоминает, какой объект нужно возвращать, и последующие вызовы invokeDynamic для той же лямбды могут быстро получить этот объект или вызвать его напрямую без повторного вызова bootstrap method.

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

Использование invokeDynamic позволяет JIT-компилятору оптимизировать выполнение вызовов методов функционального интерфейса более эффективно, чем традиционная модель анонимных классов, поскольку JIT может принимать более информированные решения во время выполнения.

java