Как заводные замыкания работают внутри?

Лямбда Java не может изменять переменные в окружающей области (не закрывается). Но почему замыкания Groovy могут? Какова его внутренняя реализация?

Например, как получается, что замыкание может увеличивать внешнюю переменную i? Будет ли создаваться внутренний объект для каждой итерации?

for (int i = 0; i < n;) {
    { -> i++ }.run()
}

person Artem Novikov    schedule 18.03.2017    source источник


Ответы (1)


В этом случае i помещается внутри изменяемой ссылки groovy. В Java установка i будет означать изменение локальной переменной метода, в котором находится этот цикл. Что вызывает кучу технических вопросов о том, что произойдет, если объект функции покинет метод. Но в groovy i находится в куче.

Это объяснение исходит из байт-кода, насколько я понимаю, который вы можете получить с помощью команды:

javap -c <name of closure class>

Глядя на метод doCall, сначала ищется объект CallSite для функтора (поскольку лямбда-выражения могут совместно использовать свои классы, сайты вызовов в основном представляют собой наборы захваченных локальных переменных), и извлекается конкретный i:

10: getfield      #29                 // Field i:Lgroovy/lang/Reference;

Как видите, тип i равен groovy.lang.Reference. Далее число увеличивается:

27: invokestatic  #51                 // Method org/codehaus/groovy/runtime/DefaultGroovyMethods.next:(Ljava/lang/Number;)Ljava/lang/Number;

После этого результат загружается обратно в ссылку groovy по адресу:

42: invokevirtual #61                 // Method groovy/lang/Reference.set:(Ljava/lang/Object;)V

Это было бы похоже на выполнение чего-то подобного в Java:

for (AtomicInteger i = new AtomicInteger(); i.get() < n;) {
    ((Runnable) () ->  System.out.println(i.getAndIncrement())).run();
}

Где я использовал AtomicInteger для представления изменяемого int, который находится в куче, а не в слоте локальной переменной.

person Jorn Vernee    schedule 18.03.2017
comment
Спасибо. У вас есть доказательства? Если я сделаю println i.class, увижу ли я это в действии? Кроме того, когда Groovy знает, что он должен обернуть его? Компиляция или время выполнения? А как насчет @CompileStatic в этом отношении? - person Artem Novikov; 18.03.2017
comment
@ArtemNovikov Доказательство было в байт-коде функтора. Сначала я не включил его, потому что у меня возникли проблемы с отделением шаблона Groovy от фактической реализации. Я приложил все усилия. Я не очень хорошо знаком с groovy, я просто умею читать байт-код. Может быть, вы можете проверить некоторые вещи самостоятельно. Из моего собственного тестирования кажется, что groovy всегда заключает ints в ссылку groovy, а @CompileStatic этого не меняет. println i.class покажет java.lang.Integer, ссылка на groovy кажется скрытой деталью реализации. - person Jorn Vernee; 18.03.2017