Типы данных и объявление переменных в Groovy
Дальше: Справочник Groovy · Groovy и Java · операции с коллекциями — ниже
Типы данных и объявление переменных в Groovy
Groovy работает поверх JVM и использует те же базовые типы, что Java, но добавляет динамическую типизацию через def, удобные литералы коллекций и строк с интерполяцией. Понимание, когда указывать тип явно, а когда полагаться на вывод, помогает писать и скрипты, и промышленный код в Gradle или Grails.
Подробный справочник: Справочник по Groovy. Отличия от Java (==, GString): Groovy и Java.
Статическая и динамическая типизация
В Groovy поддерживаются оба подхода:
- Явный тип (
int,String,List<Integer>) — предсказуемость, лучшая поддержка IDE и рефакторинга, типична для библиотек и доменного кода. def— тип выводится при присваивании; удобен в скриптах, тестах и DSL, но усложняет анализ кода в больших проектах.
def count = 10 // Integer
int fixed = 10 // явно int
def name = 'Groovy' // String
String title = "Hello" // GString или String в зависимости от кавычек
Разбор:
def count = 10включает вывод типа: в рантайме этоInteger, хотя тип не записан явно.int fixed = 10фиксирует примитивный тип и делает контракт переменной очевидным в API/сигнатурах.def name = 'Groovy'с одинарными кавычками создаёт обычныйStringбез интерполяции.String title = "Hello"показывает, что двойные кавычки в Groovy могут даватьGStringпри наличии подстановок, но здесь строка совместима соString.
На этапе компиляции Groovy всё равно генерирует байт-код JVM; "динамичность" означает, что компилятор не всегда требует аннотации типа в исходнике, а не отсутствие типов в рантайме.
Примитивные и ссылочные типы
| Категория | Типы | Замечание |
|---|---|---|
| Целые | byte, short, int, long, BigInteger | Для больших чисел — BigInteger |
| Дробные | float, double, BigDecimal | Деньги и точные расчёты — BigDecimal |
| Логический | boolean | true / false |
| Символ | char | Один символ в одинарных кавычках |
| Строки | String, GString | См. ниже |
long big = 1_000_000_000L
BigDecimal price = 19.99G // суффикс G — BigDecimal
boolean active = true
char letter = 'A'
Разбор:
- Суффикс
Lу1_000_000_000Lпринудительно задаёт типlong. - Подчёркивания в числе (
1_000_000_000) улучшают читаемость и не влияют на значение. - Суффикс
Gу19.99GсоздаётBigDecimal, полезный для точных денежных расчётов. boolean active = trueзадаёт булев флаг состояния.char letter = 'A'хранит одиночный символ, а не строку.
Строки — String и GString
- Одинарные кавычки
'...'— обычная строка без интерполяции. - Двойные кавычки
"..."— GString: подстановка${выражение}. - Тройные кавычки
'''...'''/"""..."""— многострочный текст.
def user = 'Alice'
def greeting = "Hello, ${user}!" // Hello, Alice!
def multiline = '''Line 1
Line 2'''
Разбор:
userобъявлен как строка в одинарных кавычках (String).- В
greetingиспользуется GString:${user}интерполирует значение переменной внутрь шаблона. - Тройные одинарные кавычки создают многострочный литерал без интерполяции.
- Такой формат полезен для шаблонов, SQL и текстовых блоков без ручных
\n.
Смешивание GString и String в сравнении иногда даёт неожиданный результат: оператор == в Groovy вызывает equals(), а не сравнение ссылок.
def plain = 'Hello'
def interpolated = "Hello"
assert plain == interpolated // true — сравнение по содержимому
assert plain.is(interpolated) // false — разные классы (String и GString)
Разбор:
plain— объектString,interpolatedможет бытьGStringв зависимости от контекста построения.==в Groovy сравнивает содержимое (черезequals()), поэтому строки считаются равными.is()проверяет ссылочную идентичность (это один и тот же объект или нет).- Пример показывает разницу между равенством значений и равенством ссылок.
Для идентичности объектов используйте is() (или === в новых версиях Groovy).
Объявление переменных и констант
def mutable = 1
final int MAX = 100 // нельзя переназначить ссылку
def (a, b) = [1, 2] // множественное присваивание
Разбор:
mutable— обычная переменная, которой можно присваивать новое значение.final int MAX = 100фиксирует ссылку/значение переменнойMAX, повторное присваивание запрещено.def (a, b) = [1, 2]распаковывает список в несколько переменных за одну операцию.- Множественное присваивание удобно при возврате нескольких значений из функции.
В методах последнее выражение без return считается возвращаемым значением — это влияет на то, как выглядят "типизированные" геттеры и замыкания.
Коллекции
Play ITЗагрузка интерактивного демо…
Play ITЗагрузка интерактивного демо…
Play ITЗагрузка интерактивного демо…
Play ITЗагрузка интерактивного демо…
Groovy создаёт коллекции литералами — без new ArrayList<>():
def list = [1, 2, 3] // List
def set = [1, 2, 3] as Set // Set
def map = [name: 'Alice', age: 25] // Map (ключи — строки по умолчанию)
Разбор:
[1, 2, 3]создаёт литерал списка (List) без явногоnew ArrayList().as Setвыполняет приведение к множеству и убирает дубликаты по правиламSet.[name — 'Alice', age: 25]создаётMap, гдеnameиageинтерпретируются как строковые ключи.- Литералы коллекций — одна из ключевых причин лаконичности Groovy-кода.
Операции (литералы Groovy — те же java.util.List / Map / Set):
| Тип | Добавить | Прочитать | Заменить | Удалить |
|---|---|---|---|---|
List | << v, add(v) | list[i] | list[i] = v | removeAt(i) |
Map | map[k] = v | map[k] | map[k] = v | remove(k) |
Set | add(v) | contains(v) | — | remove(v) |
Частые операции через GDK (Groovy Development Kit — расширения к стандартным классам Java):
list.each { println it }
list.findAll { it > 1 }
map.collect { k, v -> "$k=$v" }
Разбор:
eachитерирует все элементы коллекции и выполняет действие для каждого.- В замыкании
it— неявный параметр текущего элемента (когда параметр один). findAll { it > 1 }фильтрует список и возвращает только элементы, удовлетворяющие условию.map.collect { k, v -> ... }перебирает пары ключ-значение и преобразует их в новый список строк.
Сниппет: добавление и доступ к элементам
def list = [10, 20]
list << 30 // то же, что list.add(30)
list[0] = 99 // замена по индексу
def map = [env: 'dev', port: 8080]
map['env'] = 'prod'
map << [debug: true] // добавить пару в Map
Разбор:
- Оператор
<<дляListвызываетleftShiftи добавляет элемент в конец. list[0] = 99— синтаксический сахар надputAt(0, 99).- Для
Mapдоступmap['env']читает и пишет значение по ключу. map << [debug: true]объединяет карты: появляется новый ключdebug.
Сниппет: срез списка через диапазон
def nums = [0, 1, 2, 3, 4, 5]
def middle = nums[1..3] // [1, 2, 3]
def head = nums[0..<2] // [0, 1]
Разбор:
nums[1..3]берёт подсписок с индексами 1, 2, 3 включительно.nums[0..<2]использует исключающую границу: элементы с индексами 0 и 1.- Диапазоны в индексации работают так же, как в циклах
for (x in 1..5). - Исходный список
numsпри этом не меняется — создаётся новый фрагмент.
Сниппет: slash-строка для regex
def email = 'user@example.com'
def ok = email ==~ /[^@]+@[^@]+\.[^@]+/
println ok // true
Разбор:
- Строка в форме
/.../— slashy literal для регулярного выражения без лишних экранирований\. - Оператор
==~проверяет, что вся строка совпадает с шаблоном (не только фрагмент). [^@]+означает "один и более символов, кроме@".- Результат
ok—boolean, удобный для валидации ввода в скриптах.
Диапазоны (Range)
Диапазон — отдельный тип для итераций и срезов:
def inclusive = 1..5 // 1, 2, 3, 4, 5
def exclusive = 1..<5 // 1, 2, 3, 4
('a'..'d').each { println it }
Разбор:
1..5создаёт включающий диапазон (правая граница входит).1..<5создаёт исключающий диапазон (правая граница не входит).- Диапазоны можно строить не только для чисел, но и для символов (
'a'..'d'). eachпо диапазону работает так же, как по списку:itполучает текущее значение.
Приведение типов и проверки
def value = '42'
int n = value as int // NumberFormatException при ошибке
Integer safe = value?.toInteger() // безопасная навигация
def obj = []
if (obj instanceof List) {
println 'Это список'
}
Разбор:
value as intвыполняет явное преобразование строки в число; при неверном формате будет исключение.value?.toInteger()использует безопасный вызов?.: еслиvalue == null, метод не вызывается и вернётсяnull.instanceof Listпроверяет тип объекта во время выполнения.- Пример
obj = []создаёт пустой список, поэтому условие истинно и печатается сообщение.
Оператор ?. вызывает метод только если объект не null. Оператор Elvis ?: подставляет значение по умолчанию:
def city = person?.address?.city ?: 'Unknown'
Разбор:
- Цепочка
person?.address?.cityбезопасно проходит по вложенным полям безNullPointerException. - Если любой промежуточный объект
null, левая часть выражения становитсяnull. - Elvis-оператор
?:подставляет'Unknown', когда левая часть "ложная" по правилам Groovy truth. - Конструкция заменяет громоздкие
if/elseпри работе с nullable-данными.
Типичные ошибки
- Путать
==иis— для идентичности объектов используйтеis, для равенства значений —==(черезequals). - Использовать
defвезде — в API и доменных моделях лучше явные типы. - Считать Groovy "чисто динамическим" — при
@CompileStaticмногие проверки переносятся на этап компиляции.
Связанные материалы
- Особенности и расширения Groovy — метапрограммирование, DSL, AST.
- Справочник по Groovy — полные таблицы операторов и GDK.
Как выбирать типы в реальном коде
На практике тип выбирают по цене ошибки и по сроку жизни кода:
- в одноразовом скрипте для миграции удобно использовать
def, чтобы писать быстрее; - в публичных методах сервиса лучше указывать явные типы, чтобы IDE и ревьюер сразу видели контракт;
- в финансовых расчётах используйте
BigDecimal, чтобы избежать накопления ошибок округления; - в DTO и доменных моделях фиксируйте типы полей явно, даже если переменные внутри метода объявлены через
def.
class InvoiceLine {
String item
BigDecimal price
int qty
}
BigDecimal total(List<InvoiceLine> lines) {
lines.collect { it.price * it.qty }
.inject(0G) { acc, v -> acc + v }
}
Разбор:
InvoiceLineзадаёт доменную модель строки счёта с явными типами полей.total(List<InvoiceLine> lines)принимает типизированный список и возвращаетBigDecimal.collect { it.price * it.qty }преобразует список объектов в список сумм по каждой позиции.inject(0G) { acc, v -> acc + v }аккумулирует итог с начальным значением0G(BigDecimal).- Такой pipeline читается декларативно и даёт точный денежный результат без ошибок округления
double.
Здесь сигнатура total(...) и поля класса типизированы явно, поэтому контракт функции понятен сразу.
Частые сценарии и готовые решения
// 1) Безопасно достать число из текста
def raw = "120"
Integer port = raw?.isInteger() ? raw.toInteger() : 8080
// 2) Значение по умолчанию для пустой коллекции
def users = inputUsers ?: []
// 3) Группировка по ключу
def grouped = [
[name: "A", role: "dev"],
[name: "B", role: "qa"],
[name: "C", role: "dev"]
].groupBy { it.role }
Разбор:
raw?.isInteger() ? raw.toInteger() : 8080валидирует вход и выбирает дефолтный порт при нечисловом значении.inputUsers ? — []защищает отnull, гарантируя, что дальше код работает со списком.groupBy { it.role }группирует список словарей по роли и возвращаетMap<Role, List<User>>.- В результате ключи мапы — роли (
dev,qa), значения — списки соответствующих элементов.
Этот набор покрывает типичные задачи "входные данные", "значения по умолчанию" и "преобразование коллекций" в сервисном коде.
Сквозной кейс — модель данных каталога книг
Ниже стартовая модель, которую можно использовать дальше в статьях про операторы, циклы, OOP, БД, тесты и CI.
class CatalogBook {
Long id
String title
String author
BigDecimal price
boolean active
List<String> tags = []
}
Разбор:
CatalogBookописывает структуру сущности каталога: идентификатор, метаданные и признаки публикации.Long idиBigDecimal priceвыбраны для типичной бизнес-модели (идентификаторы и цены).boolean activeотражает состояние публикации без дополнительных enum/строк.List<String> tags = []сразу инициализирует пустую коллекцию, чтобы избежатьnullпри дальнейшей работе с тегами.- Это удобная базовая модель для последующих примеров по фильтрации, БД и тестированию.
Почему именно так:
BigDecimalдля цены;booleanдля простого флага публикации;List<String>для тегов, чтобы тренировать операции с коллекциями.
Продолжение кейса: Операторы и выражения.