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

Типы данных и объявление переменных в 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
Логическийbooleantrue / 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] = vremoveAt(i)
Mapmap[k] = vmap[k]map[k] = vremove(k)
Setadd(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 для регулярного выражения без лишних экранирований \.
  • Оператор ==~ проверяет, что вся строка совпадает с шаблоном (не только фрагмент).
  • [^@]+ означает "один и более символов, кроме @".
  • Результат okboolean, удобный для валидации ввода в скриптах.

Диапазоны (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-данными.

Типичные ошибки

  1. Путать == и is — для идентичности объектов используйте is, для равенства значений — == (через equals).
  2. Использовать def везде — в API и доменных моделях лучше явные типы.
  3. Считать Groovy "чисто динамическим" — при @CompileStatic многие проверки переносятся на этап компиляции.

Связанные материалы


Как выбирать типы в реальном коде

На практике тип выбирают по цене ошибки и по сроку жизни кода:

  • в одноразовом скрипте для миграции удобно использовать 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> для тегов, чтобы тренировать операции с коллекциями.

Продолжение кейса: Операторы и выражения.