Sobes.tech
Middle+

Как работят ламбда изразите и какво се случва 'под капака'?

sobes.tech AI

Отговор от AI

Изразите lambda в Java работят чрез следните механизми:

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

  2. Динамично извикване (invokeDynamic): Вместо да генерира анонимен вътрешен клас в байткода по време на компилация, както се правеше за старите версии на анонимните класове, Java използва инструкцията invokeDynamic. Тази инструкция беше добавена в Java 7 за поддръжка на динамично типизирани езици на JVM, а в Java 8 се използва за реализиране на lambda изрази и препратки към методи. invokeDynamic отлага разрешаването на извикването на метода до изпълнение (runtime).

Какво се случва "под капака":

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

  • Компилаторът анализира lambda израза.
  • Определя типа на функционалния интерфейс, към който се отнася lambda.
  • Генерира специален синтетичен метод (често с префикс lambda$) в същия клас, който съдържа логиката на lambda израза. Този метод има същата сигнатура като абстрактния метод на функционалния интерфейс.
  • На мястото на използване на lambda, компилаторът генерира инструкция invokeDynamic. Тази инструкция съдържа препратка към bootstrap method (LambdaMetafactory.metafactory).

На етапа на изпълнение (runtime):

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

По този начин, lambda-ите не винаги създават нов клас за всяка инстанция. За lambda-ите, които не захващат променливи от външния контекст (stateless lambda), реализацията на функционалния интерфейс може да бъде сингълтон или инстанция, която се кешира и използва повторно. Lambda-ите, които захващат променливи (stateful lambda), изискват създаването на нов екземпляр на класа, който ще съхранява захванатите променливи.

Използването на invokeDynamic позволява на JIT-компилатора да оптимизира изпълнението на извикванията на методи на функционалния интерфейс по-ефективно, отколкото традиционният модел на анонимните класове, тъй като JIT може да взема по-информирани решения по време на изпълнение.

// Пример за функционален интерфейс
@FunctionalInterface
interface MyFunction {
    String apply(String input);
}

public class LambdaExample {
    public static void main(String[] args) {
        // Използване на lambda израз
        MyFunction func = s -> "Processed: " + s;

        String result = func.apply("Test");
        System.out.println(result);
    }
}