Перейти к основному содержимому

Циклы и управляющие конструкции в Groovy

Разработчику Архитектору

Сначала: Циклы в коде — общая идея повторений, виды циклов и типичные ошибки без привязки к синтаксису языка.


Groovy — циклы и ветвления

В Groovy доступны те же императивные конструкции, что в Java (for, while, break, continue), плюс итерация по коллекциям и диапазонам (for (x in list), 1..10) и замыкания для each, find, collect. Скрипты на Gradle и автотесты часто пишут именно в таком стиле — коротко и без лишней бойлерплаты.

Типичные отличия от Java на практике:

  • диапазоны 1..n и 1..<n вместо ручного счётчика;
  • for (item in collection) без явного итератора;
  • truthiness — в if и while подходят не только boolean, но и null, пустые коллекции и строки (см. правила Groovy).

Если цикл "крутится бесконечно" или пропускает элементы — сначала проверьте условие и изменение переменной в теле; общие ловушки разобраны в циклах в коде.

Интерактивное демо — пошаговый цикл на примере JavaScript (for, while). В Groovy синтаксис другой, но порядок шагов тот же. Обобщённо: циклы в коде.

Play ITЗагрузка интерактивного демо…

Ветвление if / else

Перед циклами полезно зафиксировать базовый синтаксис условий — он совпадает с Java, но в условии работает Groovy truthiness:

def score = 72
if (score >= 90) {
println 'Отлично'
} else if (score >= 60) {
println 'Зачёт'
} else {
println 'Пересдача'
}

Разбор:

  • if проверяет булево выражение score >= 90.
  • else if обрабатывает следующий диапазон, если первое условие ложно.
  • Финальный else срабатывает, когда ни одно из предыдущих условий не подошло.
  • Скобки {} задают блоки; точка с запятой после } не обязательна.

Тот же смысл на Groovy:

// for по диапазону и коллекции
for (i in 1..3) { println i }
for (item in ['a', 'b']) { println item }

// while
def n = 0
while (n < 3) { println n; n++ }

// декларативно (предпочтительно для коллекций)
[1, 2, 3].each { println it }

Разбор:

  • for (i in 1..3) итерирует диапазон и выводит числа 1, 2, 3.
  • for (item in ['a', 'b']) перебирает элементы списка без ручного индекса.
  • while (n < 3) выполняется, пока условие истинно; n++ двигает цикл к завершению.
  • [1, 2, 3].each { println it } показывает декларативный стиль: коллекция сама управляет итерацией.
  • В примере сопоставлены императивные и функциональные подходы к одной задаче.

Классический цикл for

Один из самых распространённых способов организации повторения — цикл for. В Groovy он принимает форму, близкую к синтаксису Java, но с важным расширением: вместо традиционной трёхкомпонентной структуры (инициализация; условие; шаг) Groovy предлагает упрощённую и более выразительную запись через оператор in.

Пример:

for (i in 1..5) {
println i
}

Разбор:

  • Диапазон 1..5 создаёт последовательность с включённой верхней границей.
  • Переменная i на каждой итерации получает очередное значение диапазона.
  • println i выводит текущее значение в консоль.
  • Пример иллюстрирует самый короткий способ "повторить действие N раз" в Groovy.

Здесь 1..5 — это диапазон целых чисел от 1 до 5 включительно. Переменная i последовательно принимает каждое значение из этого диапазона, и на каждой итерации выполняется тело цикла. Такой подход называется итерацией по диапазону, и он особенно удобен, когда нужно выполнить действие фиксированное число раз или обработать последовательность чисел.

Диапазоны в Groovy не ограничиваются только числами. Можно создавать диапазоны символов, например 'a'..'z', что позволяет легко перебирать алфавит или генерировать последовательности меток. Диапазон является полноценным объектом, поддерживающим методы и свойства, такие как size(), contains(), reverse() и другие, что делает его мощным инструментом даже вне контекста циклов.

Цикл for в Groovy также работает с любыми коллекциями — списками, множествами, массивами. Например:

for (name in ['Alice', 'Bob', 'Charlie']) {
println "Привет, $name!"
}

Разбор:

  • Список строк создаётся литералом ['Alice', 'Bob', 'Charlie'].
  • for (name in ...) перебирает каждый элемент и присваивает его переменной name.
  • GString "Привет, $name!" подставляет текущее имя в текст.
  • Код демонстрирует человекочитаемый паттерн "для каждого элемента выполнить действие".

В этом случае переменная name на каждой итерации получает очередной элемент списка. Такая форма записи делает код читаемым и близким к естественному языку: "для каждого имени в списке вывести приветствие".

Важно отметить, что переменная цикла в Groovy не требует предварительного объявления типа. Groovy автоматически выводит тип на основе значений, содержащихся в диапазоне или коллекции. Это упрощает синтаксис и снижает порог входа для начинающих разработчиков, сохраняя при этом строгую семантику типов во время выполнения.


Цикл while — управление по условию

Цикл while представляет собой другой подход к организации повторений — он основан исключительно на логическом условии. Его структура проста — перед каждой итерацией проверяется условие, и если оно истинно, выполняется тело цикла. После завершения тела управление возвращается к проверке условия, и процесс повторяется.

Пример:

int i = 0
while (i < 5) {
println i
i++
}

Разбор:

  • Инициализация int i = 0 задаёт стартовое состояние перед циклом.
  • while (i < 5) проверяется перед каждой итерацией.
  • В теле печатается текущее значение i.
  • i++ увеличивает счётчик; без этого шага цикл стал бы бесконечным.
  • В результате выводятся значения от 0 до 4.

Этот цикл выведет числа от 0 до 4. Переменная i инициализируется до начала цикла, её значение изменяется внутри тела цикла, а условие i < 5 контролирует момент завершения. Такой стиль управления итерациями полезен, когда количество повторений заранее неизвестно и зависит от динамически меняющихся обстоятельств — например, чтение данных из потока до достижения конца файла, ожидание изменения состояния системы, повторные попытки подключения к серверу.

Цикл while требует особой внимательности к изменению переменных, участвующих в условии. Если состояние, проверяемое в условии, не изменяется внутри тела цикла, программа может войти в бесконечный цикл — ситуация, при которой условие остаётся истинным неограниченно долго, и выполнение программы зависает. Хотя современные среды выполнения и операционные системы обычно позволяют прервать такой процесс, бесконечные циклы считаются серьёзной ошибкой проектирования логики программы.

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

def attempt = 0
do {
println "Попытка ${attempt + 1}"
attempt++
} while (attempt < 3)

Разбор:

  • Тело do { ... } выполняется до первой проверки условия.
  • Переменная attempt увеличивается внутри цикла — иначе условие никогда не станет ложным.
  • while (attempt < 3) проверяется в конце каждой итерации.
  • Будет ровно три вывода: попытки 1, 2 и 3.

Сниппет: times и upto

Когда известно точное число повторений, часто короче, чем for с диапазоном:

3.times { println "шаг ${it + 1}" }
1.upto(3) { n -> println "n = $n" }

Разбор:

  • 3.times { ... } вызывает замыкание 3 раза; it принимает значения 0, 1, 2.
  • Выражение it + 1 в примере даёт человекочитаемую нумерацию с 1.
  • 1.upto(3) { n -> ... } передаёт в замыкание явный параметр n от 1 до 3 включительно.
  • Оба варианта читаются как "повтори N раз" без ручного счётчика.

Сниппет: классический for с индексом

Когда нужен индекс элемента (например, для пакетной обработки), удобен C-стиль:

def items = ['a', 'b', 'c']
for (int i = 0; i < items.size(); i++) {
println "${i}: ${items[i]}"
}

Разбор:

  • int i = 0 инициализирует счётчик до цикла.
  • Условие i < items.size() ограничивает индекс размером списка.
  • items[i] обращается к элементу по индексу (через getAt).
  • GString "${i}: ${items[i]}" выводит и позицию, и значение — удобно в отладке.

Функциональный подход — each и другие методы коллекций

Одной из отличительных черт Groovy является глубокая интеграция функциональных идей в синтаксис и стандартную библиотеку. Вместо традиционных циклов часто предпочтительнее использовать методы, предоставляемые самими коллекциями. Самый известный из них — each.

Пример:

[1, 2, 3].each { num ->
println num
}

Разбор:

  • each — метод коллекции, который проходит по каждому элементу списка.
  • Замыкание { num -> ... } получает текущий элемент в параметре num.
  • println num выполняется для каждого значения — 1, 2, 3.
  • Это декларативный подход: описано действие, а не механика цикла.

Здесь вызывается метод each у списка [1, 2, 3]. В качестве аргумента передаётся замыкание — краткая функция, заключённая в фигурные скобки. Эта функция принимает один параметр num, который на каждой итерации получает очередной элемент списка. Тело замыкания содержит код, который должен быть выполнен для каждого элемента.

Подход с использованием each имеет несколько преимуществ. Во-первых, он декларативен — программист описывает не то, как перебирать элементы, а то, что делать с каждым элементом. Во-вторых, он изолирует логику итерации внутри метода коллекции, что снижает вероятность ошибок, связанных с индексами или условиями выхода. В-третьих, он легко комбинируется с другими функциональными методами, такими как collect, find, findAll, inject, что позволяет строить сложные цепочки преобразований данных в компактной и выразительной форме.

Например, чтобы получить квадраты всех чётных чисел из списка, можно написать:

[1, 2, 3, 4, 5, 6]
.findAll { it % 2 == 0 }
.collect { it * it }
.each { println it }

Разбор:

  • findAll { it % 2 == 0 } фильтрует только чётные элементы.
  • collect { it * it } преобразует отфильтрованные значения в квадраты.
  • each { println it } выполняет побочный эффект — печать каждого результата.
  • Такой pipeline разбивает задачу на этапы: фильтрация -> преобразование -> действие.
  • Использование it сокращает код, когда параметр замыкания один.

Этот код читается почти как предложение — "возьми список, найди все чётные числа, преобразуй их в квадраты и выведи каждый результат". Такой стиль программирования называется метод-чейнингом (chaining), и он широко используется в Groovy для создания читаемых и лаконичных решений.

Замыкания в Groovy могут принимать не только один параметр. При итерации по мапе (ассоциативному массиву) метод each передаёт два аргумента — ключ и значение:

[name: 'Alice', age: 30].each { key, value ->
println "$key: $value"
}

Разбор:

  • Литерал [name: 'Alice', age: 30] создаёт Map с двумя парами ключ-значение.
  • each { key, value -> ... } для Map передаёт в замыкание сразу два параметра.
  • GString "$key: $value" формирует человекочитаемый вывод каждой пары.
  • Такой обход удобен для логирования и формирования отчётов по структурам данных.

Если параметры не указаны явно, Groovy предоставляет неявную переменную it, которая ссылается на текущий элемент. Это особенно удобно для простых случаев:

['a', 'b', 'c'].each { println it.toUpperCase() }

Разбор:

  • Список символов/строк итерируется методом each.
  • it — текущий элемент при неявном параметре замыкания.
  • toUpperCase() приводит значение к верхнему регистру.
  • Пример показывает максимально короткую форму обработки коллекции.

Такой уровень абстракции делает код менее подверженным ошибкам и более сфокусированным на сути задачи, а не на механике её выполнения.


Вложенные циклы и сложные итерации

В реальных задачах часто требуется не просто обработать один список или диапазон, а выполнить операции над множеством связанных данных. Например, работа с таблицами, матрицами, сетками, древовидными структурами или вложенными коллекциями требует использования вложенных циклов — конструкций, где один цикл находится внутри другого.

Пример на Groovy:

def matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]

for (row in matrix) {
for (cell in row) {
println cell
}
}

Разбор:

  • matrix — список списков, имитирующий двумерную структуру (3x3).
  • Внешний цикл перебирает строки (row), внутренний — элементы строки (cell).
  • println cell выводит каждый элемент матрицы последовательно.
  • Этот шаблон применяют для таблиц, сеток и любых вложенных коллекций.

В этом фрагменте внешний цикл проходит по строкам матрицы, а внутренний — по элементам каждой строки. Такой подход позволяет последовательно обработать каждый элемент двумерной структуры. Количество уровней вложенности не ограничено, но на практике редко превышает три: чрезмерная вложенность снижает читаемость и усложняет отладку.

Groovy предоставляет альтернативу через функциональные методы. Например, вместо вложенного for можно использовать each дважды:

matrix.each { row ->
row.each { cell ->
println cell
}
}

Разбор:

  • Вариант делает ту же двойную итерацию, но через методы коллекций.
  • Внешний each работает по строкам матрицы.
  • Внутренний each обходит элементы конкретной строки.
  • Функциональный стиль уменьшает шум со счётчиками и условиями.

Такой стиль сохраняет декларативность и делает логику более явной. Более того, комбинация collect и flatten() позволяет преобразовать вложенную структуру в плоский список:

def flat = matrix.flatten()
println flat // [1, 2, 3, 4, 5, 6, 7, 8, 9]

Разбор:

  • flatten() разворачивает вложенные списки в один линейный список.
  • Исходная иерархия строк/столбцов убирается.
  • println flat показывает финальный плоский набор элементов.
  • Это удобно перед сортировкой, агрегацией и другими операциями над "плоскими" данными.

Это особенно полезно при подготовке данных для последующей обработки, когда важна линейная последовательность без иерархии.


Управление потоком выполнения — break и continue

В процессе выполнения цикла иногда возникает необходимость досрочно завершить итерацию или прервать весь цикл. Groovy поддерживает два ключевых оператора для этого: break и continue.

Оператор continue немедленно завершает текущую итерацию и переходит к следующей. Он полезен, когда часть тела цикла должна быть пропущена при определённых условиях. Например:

for (i in 1..10) {
if (i % 2 == 0) continue
println i // выведет только нечётные числа
}

Разбор:

  • Цикл идёт по диапазону от 1 до 10.
  • Условие i % 2 == 0 выявляет чётные числа.
  • continue пропускает оставшуюся часть текущей итерации для чётных значений.
  • Поэтому печатаются только нечётные числа.

Оператор break полностью прерывает выполнение цикла, независимо от того, сколько итераций осталось. Это применяется, когда дальнейшая обработка бессмысленна — например, при поиске первого подходящего элемента:

def items = ['apple', 'banana', 'cherry']
for (item in items) {
if (item.startsWith('b')) {
println "Найдено: $item"
break
}
}

Разбор:

  • Список фруктов перебирается по элементам.
  • startsWith('b') проверяет условие поиска.
  • При первом совпадении печатается найденное значение.
  • break немедленно завершает цикл, не обрабатывая оставшиеся элементы.

В случае вложенных циклов break и continue действуют только на самый внутренний цикл. Если требуется выйти из нескольких уровней сразу, Groovy предлагает использовать метки (labels), хотя такой подход встречается редко и считается признаком сложной логики, которую лучше переработать:

outer:
for (x in 1..3) {
for (y in 1..3) {
if (x * y > 3) break outer
println "$x * $y = ${x * y}"
}
}

Разбор:

  • Метка outer: привязана к внешнему циклу.
  • Внутри вложенного цикла проверяется условие x * y > 3.
  • break outer завершает сразу внешний цикл, а не только внутренний.
  • Приём мощный, но ухудшает читаемость, поэтому используется точечно.

Функциональный стиль программирования в Groovy часто делает break и continue избыточными. Методы вроде find, any, every позволяют выразить ту же логику без явного управления потоком:

def firstEven = [1, 3, 4, 5].find { it % 2 == 0 }
println firstEven // 4

Разбор:

  • find обходит коллекцию до первого подходящего элемента.
  • Условие it % 2 == 0 отбирает чётные числа.
  • После нахождения 4 перебор прекращается автоматически.
  • Это функциональный аналог цикла с break при первом совпадении.

Здесь find автоматически останавливает перебор после нахождения первого совпадения, что эквивалентно использованию break в императивном цикле.


Сравнение подходов — когда что использовать

Выбор между классическим циклом for, условным while и функциональным each зависит от контекста задачи.

Цикл for с диапазоном идеален, когда известно точное количество повторений или нужно перебрать последовательность значений. Он лаконичен, предсказуем и легко читается даже новичками.

Цикл while подходит для ситуаций, где условие завершения зависит от внешних факторов — ввод пользователя, состояние сети, результат вычисления, изменяющийся во времени. Он даёт полный контроль над логикой итерации, но требует аккуратного управления переменными, чтобы избежать зацикливания.

Методы вроде each, collect, find и другие — это инструменты для работы с данными как с единым целым. Они выражают намерение разработчика — "применить действие ко всем элементам", "преобразовать каждый элемент", "найти первый подходящий". Такой код легче тестировать, так как он не содержит изменяемого состояния в виде счётчиков или флагов. Кроме того, эти методы легко компонуются в цепочки, что способствует созданию выразительных и компактных решений.

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


Циклы и производительность

Хотя Groovy — язык с динамической типизацией и богатыми возможностями, стоит учитывать накладные расходы, связанные с использованием замыканий и методов коллекций. Каждый вызов each создаёт объект замыкания, который затем вызывается для каждого элемента. В большинстве приложений эта разница незаметна, но в критичных к производительности участках кода (например, при обработке миллионов записей) может быть предпочтительнее использовать классический for с индексом или диапазоном.

Однако преждевременная оптимизация — частая ошибка. Прежде чем заменять each на for, следует профилировать приложение и убедиться, что именно цикл является узким местом. Читаемость и поддерживаемость кода почти всегда важнее микроскопических выигрышей в скорости.


Шаблоны выбора конструкции

В реальном проекте удобно держать простое правило:

  • если нужна трансформация коллекции, выбирайте collect, findAll, groupBy;
  • если важно "остановиться на первом совпадении", выбирайте find или any;
  • если цикл зависит от внешнего состояния, используйте while;
  • если нужно N повторений, используйте диапазон 1..N или 0..<N.
def logs = ["INFO ok", "WARN slow", "ERROR fail"]
def firstError = logs.find { it.startsWith("ERROR") }
def warnings = logs.findAll { it.startsWith("WARN") }

Разбор:

  • logs задаёт набор строковых событий.
  • find возвращает первый элемент, удовлетворяющий условию (ERROR).
  • findAll возвращает все элементы, подходящие под фильтр (WARN).
  • Пример показывает выбор нужного инструмента под задачу: один результат vs список результатов.

Такой подход ускоряет чтение кода: в названии метода уже видна цель операции.


Антипаттерны в циклах

  1. Изменять исходную коллекцию в each без явной необходимости.
  2. Дублировать фильтрацию и преобразование в нескольких местах вместо одного pipeline.
  3. Использовать флаги found = false там, где подходит any/find.
// вместо ручного цикла и флага
def hasCritical = events.any { it.level == "CRITICAL" }

Разбор:

  • any проверяет, есть ли хотя бы один элемент, удовлетворяющий условию.
  • Замыкание оценивает поле level каждого события.
  • Метод останавливается при первом совпадении и возвращает true.
  • Так убирается ручной флаг found и лишний императивный код.

Связанные материалы: Операторы и выражения, Синтаксические конструкции.


Сквозной кейс — пакетная обработка каталога

Когда каталог большой, операции удобно разделять на этапы:

def books = loadBooks() // условный источник
def chunkSize = 100

for (int i = 0; i < books.size(); i += chunkSize) {
def batch = books.subList(i, Math.min(i + chunkSize, books.size()))
batch.each { book ->
if (!book.active) return
println "Indexed: ${book.title}"
}
}

Разбор:

  • loadBooks() получает исходный массив/список книг для пакетной обработки.
  • Переменная chunkSize задаёт размер одной порции данных.
  • Классический цикл for двигает индекс шагом chunkSize, формируя батчи.
  • subList(i, Math.min(...)) безопасно берёт диапазон без выхода за границы в последнем батче.
  • Внутри batch.each выполняется обработка записи; if (!book.active) return пропускает неактивные книги в текущем замыкании.
  • Такой паттерн полезен для больших массивов данных и контролируемой нагрузки.

В таком виде код сохраняет контроль над производительностью и остается читаемым.

Дальше по кейсу: ООП в Groovy.