Привет, Хабр!
Сегодня мы рассмотрим closures в Groovy: как они устроены, зачем нужны и чем принципиально отличаются от лямбд в Java.
Анатомия Closure
Первое, что важно понять: Closure
— это полноценный объект, наследник groovy.lang.Closure
, а не синтаксический сахар, как Java‑лямбда, которая компилятором превращается в скрытый статический метод + invokedynamic
.
def adder = { int a, int b -> a + b }
println adder.getClass().name
Запустите, и в консоли всплывёт нечто вроде:
foo$_run_closure1
Если через javap -c
вскрыть класс, найдёте метод doCall
, не call
. Именно его Groovy генерит, передавая параметры строго так, как вы их объявили. А метод call
в базовом классе просто проксирует внутрь doCall
, добавляя гибкости.
Контейнер outer-this
Каждый сгенерированный closure‑класс содержит приватное финальное поле this$0
— ссылку на внешнюю (enclosing) инстанцию скрипта или класса. Отсюда и магия доступа к приватным переменным уровнем выше:
class Counter {
private int state = 0
Closure next = { ++state } // лаконичный инкремент
}
assert new Counter().next() == 1
Компилятор просто тащит ссылку на Counter
внутрь анонимного Closure
‑класса. Отсюда главный риск: если замыкание уходит жить дольше, чем владелец, можете нечаянно удержать кучу тяжёлых структур. Java‑лямбды в этом плане немножко легковеснее: они не цепляются за полный this
, а захватывают только реально используемые переменные, причём финализированные (или effectively final).
delegate / owner / this
Спецификатор |
Что хранит |
Тип данных |
Когда меняется |
---|---|---|---|
|
Enclosing объект (outer класс) |
|
Никогда: жёстко прошит в конструктор |
|
Тот, кто создал замыкание |
|
Никогда |
|
Внешняя точка разрешения методов |
|
Можно менять на лету |
Замыкание сначала ищет методы/поля в delegate
, потом в owner
, потом в this
, и лишь затем падает в NPE. Установить делегата проще простого:
class Greeter { String prefix = '>> ' }
def cl = { "Hello $name" }
cl.delegate = new Greeter()
cl.resolveStrategy = Closure.DELEGATE_FIRST
println cl.call(name: 'Groovy') // >> Hello Groovy
Под капотом Closure
вызывает this.getDelegate()
, а getDelegate()
— обычный геттер.
Стратегии разрешения вызовов
OWNER_FIRST
(дефолт) — любит стабильность, ищет у создателя.DELEGATE_FIRST
— фаворит DSL‑щиков.DELEGATE_ONLY
— железная гарантия, что забудетеthis
и не вызовете что‑то лишнее.OWNER_ONLY
— эквивалентthis.
в Java.TO_SELF
— редкий зверь, полезен вmethodMissing
.
Часто используют Builder‑миксер — переключают стратегию на время конфигурации, потом откатывают, чтобы случайно не затащить левые методы:
def withDelegate(Closure<?> cl, Object delegate, int stg = Closure.DELEGATE_FIRST) {
def oldDel = cl.delegate
def oldStg = cl.resolveStrategy
try {
cl.delegate = delegate
cl.resolveStrategy = stg
cl.call()
} finally {
cl.delegate = oldDel
cl.resolveStrategy = oldStg
}
}
Способы передачи параметров
it
def odds = (1..10).findAll { it & 1 }
Компилятор создаёт метод doCall(Object it)
(ровно один арг). Всё хорошо, пока не надо цеплять типизацию — в @CompileStatic
это превращается в Object
, теряется строгая проверка, и ловите ClassCastException
в рантайме.
Явные имена
def odds = (1..10).findAll { Integer num -> (num & 1) }
Теперь doCall(Integer num)
, и static
‑компиляция проверит типы ещё на build‑сервере.
Вариативка
def sum = { int... xs -> xs.sum() }
assert sum(1, 2, 3) == 6
Да, Groovy оборачивает varargs в int[]
, как и Java. Но прячьте такую конструкцию под @CompileStatic
, иначе рекурсивный вызов при каррировании может получить Object[]
, и снова проблемы.
Каррирование и частичное применение (curry / rcurry / ncurry)
curry
очень полезно, когда нужно было быстро соорудить параметризованный генератор задач.
Closure htmlWrap = { pre, post, body -> "$pre$body$post" }
def bold = htmlWrap.curry('<b>', '</b>')
println bold('Hi') // <b>Hi</b>
curry
создаёт новый объект Closure
, ссылка на исходный остаётся; собирайте мусор вовремя, не плодите миллионы полуфабрикатов внутри циклов.
Trampoline
Groovy умеет делать TCO через прокси‑метод trampoline()
. Работает и с Closure
, и с обычными методами:
def fib
fib = { long n, long a = 0, long b = 1 ->
n ? fib.trampoline(n - 1, b, a + b) : a
}.trampoline()
println fib(92) // не падаем, хотя рекурсия толстая
JVM‑стек остаётся тонким, потому что каждое вложение возвращает новую closure‑заготовку, а trampoline
крутит итеративный while‑loop.
Builders — MarkupBuilder, JsonBuilder, StreamingMarkupBuilder
На Builder‑DSL‑ах замыкание раскрывает весь потенциал delegate
. Например:
def report = new StringWriter()
def html = new groovy.xml.MarkupBuilder(report)
html.html {
head {
title 'Health Check'
}
body {
h1(class: 'title', 'Service Status')
table {
services.each { svc ->
tr {
td svc.name
td svc.status
}
}
}
}
}
println report.toString()
Каждая вложенная closure получает нового делегата — так MarkupBuilder меняет контекст, не подменяя owner
, что позволяет свободно писать циклы или вызывать методы скрипта, не боясь конфликтов имён.
Безопасность
Когда исполняете пользовательский Groovy‑скрипт внутри сервиса, можно включать SecureASTCustomizer
. Он способен запретить:
доступ к
System.exit
,Runtime.exec
и т.п;создание новых классов;
прямое обращение к
java.io
.
А closure в GroovyShell
можно обернуть в CompilationCustomizer
с phase = Conversion
, чтобы выдать кастомную ошибку раньше runtime‑части.
Конкурентность
Если нужен Map‑Reduce‑стиль обработки, но хочется остаться в Groovy:
@Grab('org.codehaus.gpars:gpars:1.3.1')
import static groovyx.gpars.GParsPool.withPool
withPool(8) {
def primes = (2..1_000_000).findAllParallel { n ->
(2..Math.sqrt(n)).every { n % it != 0 }
}
println primes.size()
}
findAllParallel
делит коллекцию, сериализует closure и исполняет в пуле акторов. Именно сериализует: все захваченные переменные должны быть Serializable
, иначе получите NotSerializableException
. С Java‑лямбдой такое не пройдёт, она по дефолту не Serializable
, поэтому Groovy всё равно удобнее для быстрого прототипа.
Итог
Closure в Groovy — гибок, умеет замыкаться на контекст, подменять делегатов, каррироваться, работать в DSL и даже оптимизироваться trampoline
‑рекурсией, но за каждую эту фичу вы платите производительностью и сложностью отладки. Чем чётче вы понимаете, как устроен его bytecode, какие ссылки он тянет и как управлять resolveStrategy
, тем безопаснее и чище будет ваш код. Уверен, у многих из вас есть свой опыт работы с замыканиями — делитесь опытом в комментариях.
Если вы работаете с Groovy или только начинаете разбираться в его возможностях — обратите внимание на курс Groovy Developer. Он поможет структурировать понимание языка и лучше разобраться в таких темах, как closures, делегаты и построение DSL.
Также приглашаем вас заглянуть в каталог курсов по программированию, где собраны курсы по работе с языками, фреймворками и инструментами — от общего синтаксиса до узкоспециализированных решений.
Чтобы оставаться в курсе самых актуальных технологий и трендов, подписывайтесь на Telegram-канал OTUS.
vital_pavlenko
любимые нейронки или что с наушниками у котика на превью
MaxRokatansky
Ну, съехали немного. Увлекся работой и не заметил)