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

Операторы и выражения в Groovy

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

Перед чтением: Операторы — общие понятия оператора, операнда, приоритетов и типов операций без привязки к языку.


Операторы и выражения в Groovy

Groovy поддерживает большинство операторов из Java, плюс некоторые уникальные.

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


Арифметические операторы

Арифметические операторы в Groovy предназначены для выполнения математических операций над числами. К ним относятся:

  • + — сложение. Применяется к числам, а также к строкам, где он выполняет конкатенацию.
  • - — вычитание. Используется для получения разности двух числовых значений.
  • * — умножение. Вычисляет произведение двух чисел.
  • / — деление. В отличие от Java (5 / 22), в Groovy 5 / 2 даёт 2.5 (double). Если хотя бы один операнд — BigDecimal, результат тоже BigDecimal; для точных денежных расчётов используйте суффикс G (10G / 3G).
  • % — остаток от деления. Позволяет получить остаток после целочисленного деления одного числа на другое.

Groovy также поддерживает составные операторы присваивания, такие как +=, -=, *=, /=, %=. Эти операторы сочетают арифметическую операцию с присваиванием результата обратно переменной. Например, выражение a += 5 эквивалентно a = a + 5.

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

println 5 / 2 // 2.5 — не целочисленное деление, как в Java
println 10G / 3G // BigDecimal
println 'Hello' + ' World' // конкатенация строк
println 17 % 5 // 2

Разбор:

  • 5 / 2 в Groovy даёт 2.5 (double), потому что хотя бы один операнд трактуется как дробный контекст деления.
  • 10G / 3G использует литералы BigDecimal (суффикс G) — результат тоже BigDecimal.
  • + между строками склеивает их в одну строку.
  • % возвращает остаток от деления: 17 % 52.

Операторы сравнения

Операторы сравнения используются для проверки отношений между двумя значениями. Groovy предоставляет следующие операторы:

  • == — равенство. В отличие от Java, где этот оператор сравнивает ссылки на объекты, в Groovy он вызывает метод equals() для сравнения содержимого объектов. Это делает код более предсказуемым и соответствующим ожиданиям разработчика.
  • != — неравенство. Является логическим отрицанием оператора ==.
  • < — меньше.
  • > — больше.
  • <= — меньше или равно.
  • >= — больше или равно.

Особое внимание заслуживает оператор <=>, известный как оператор космического корабля или пространственный оператор. Он выполняет трёхстороннее сравнение и возвращает одно из трёх возможных значений: -1, если левый операнд меньше правого; 0, если они равны; и 1, если левый операнд больше правого. Этот оператор особенно полезен при реализации сортировки или при необходимости получить информацию о порядке двух значений за одну операцию. Он вызывает метод compareTo(), если оба операнда реализуют интерфейс Comparable.

assert (3 <=> 5) == -1
assert ('b' <=> 'a') == 1
def sorted = ['cherry', 'apple', 'banana'].sort { a, b -> a <=> b }
println sorted // [apple, banana, cherry]

Разбор:

  • 3 <=> 5 возвращает -1, потому что 3 меньше 5.
  • Для строк 'b' <=> 'a' даёт 1 (лексикографически b больше a).
  • В sort { a, b -> a <=> b } замыкание сравнивает два элемента для сортировки.
  • Итоговый список упорядочен по возрастанию строк.

Логические операторы

Логические операторы позволяют комбинировать булевы выражения и управлять логикой программы. В Groovy поддерживаются следующие логические операторы:

  • && — логическое И. Возвращает true, только если оба операнда истинны. Поддерживает короткое замыкание: если первый операнд ложен, второй не вычисляется.
  • || — логическое ИЛИ. Возвращает true, если хотя бы один из операндов истинен. Также поддерживает короткое замыкание: если первый операнд истинен, второй не вычисляется.
  • ! — логическое НЕ. Инвертирует булево значение операнда.

Groovy также допускает использование словесных аналогов — and, or, not. Эти ключевые слова функционально эквивалентны символическим операторам, но иногда повышают читаемость кода, особенно в сложных условиях.

def role = 'admin'
def active = true
def allowed = active and (role == 'admin' or role == 'ops')
def denied = not allowed
println allowed // true
println denied // false

Разбор:

  • and / or — те же логические операторы, что && / ||, с коротким замыканием.
  • Скобки задают приоритет: сначала проверка роли, затем and с active.
  • not инвертирует булево значение (аналог !).
  • Словесная форма удобна в длинных бизнес-условиях и DSL.

Условный (тернарный) оператор

Тернарный оператор предоставляет компактную форму записи условного выражения. Его синтаксис:

def result = condition ? trueValue : falseValue

Разбор:

  • condition вычисляется как булево выражение.
  • Если условие истинно, переменная result получает trueValue.
  • Если условие ложно, выбирается falseValue.
  • Тернарный оператор делает простые ветвления компактнее, чем полный if/else.

Здесь condition — булево выражение. Если оно истинно, результатом всего выражения становится trueValue; в противном случае — falseValue. Тернарный оператор часто используется для инициализации переменных в зависимости от условия, что делает код более лаконичным по сравнению с полной конструкцией if-else.


Оператор безопасного доступа

Оператор безопасного доступа ?. решает распространённую проблему работы с объектами, которые могут быть null. Вместо того чтобы вызывать исключение при попытке доступа к свойству или методу null-объекта, этот оператор возвращает null, если левый операнд равен null. Например:

def value = object?.property

Разбор:

  • ?. выполняет безопасный доступ к свойству property.
  • Если object == null, выражение вернёт null без исключения.
  • Если объект существует, будет вызван обычный доступ к свойству.
  • Это уменьшает количество защитных if (obj != null) в коде.

Если object равен null, выражение object?.property вернёт null, а не вызовет NullPointerException. Это особенно полезно при работе с цепочками вызовов, где любой элемент может отсутствовать. Например, person?.address?.city безопасно пройдёт по всей цепочке, возвращая null, если хотя бы один из промежуточных объектов не определён.


Оператор Elvis

Оператор Elvis ?: представляет собой сокращённую форму тернарного оператора, предназначенную для задания значения по умолчанию. Его синтаксис:

def name = input ?: "default"

Разбор:

  • Elvis-оператор ?: проверяет левую часть по правилам Groovy truth.
  • Если input "истинен" (не null, не пустой, не false), он и вернётся.
  • Иначе вернётся строка "default".
  • Это типичный способ задать дефолт без многословного условия.

Это выражение эквивалентно:

def name = input ? input : "default" // упрощённо; см. truthiness в Groovy

Разбор:

  • Это полная форма того же Elvis-паттерна.
  • Логика дублирует input дважды: в условии и в результате true-ветки.
  • Поэтому в Groovy обычно предпочитают более короткую форму input ?: "default".
  • Обе версии возвращают значение по умолчанию, когда вход "ложный".

Оператор Elvis возвращает левый операнд, если он "истинен" в смысле Groovy (truthiness) — не null, не false, не 0, не пустая строка '', не пустая коллекция или карта). В противном случае возвращается правый операнд. Это делает код более читаемым при установке значений по умолчанию, особенно в конфигурациях или при обработке пользовательского ввода.


Операторы диапазона

Groovy предоставляет мощный и выразительный оператор диапазона .., который создаёт объект типа Range. Этот оператор позволяет задавать последовательности значений, включая как числовые, так и символьные. Например:

def numbers = 1..5 // [1, 2, 3, 4, 5]
def letters = 'a'..'d' // ['a', 'b', 'c', 'd']

Разбор:

  • 1..5 создаёт включающий диапазон целых чисел.
  • 'a'..'d' показывает, что диапазоны работают и для символов.
  • Объект Range можно итерировать, проверять contains, использовать в условиях.
  • Такой синтаксис заменяет ручное построение списков и счётчиков.

Диапазоны можно использовать в циклах, условиях, а также как коллекции — они реализуют интерфейс java.util.List. Это делает их удобными для итерации, проверки принадлежности (in) и других операций, характерных для списков.

Существует также исключающий диапазон оператором ..<, который не включает правую границу:

def exclusive = 1..<5 // [1, 2, 3, 4]

Разбор:

  • Оператор ..< исключает правую границу диапазона.
  • Пример создаёт последовательность 1,2,3,4, но не включает 5.
  • Такой формат удобен для индексов и циклов "до N, не включая N".

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


Оператор индексации

Groovy расширяет стандартную индексацию массивов и коллекций, делая её доступной для любых объектов через перегрузку метода getAt() и putAt(). Оператор [] используется для получения или установки значения по ключу или индексу:

def list = [10, 20, 30]
def value = list[1] // 20

def map = [name: 'Alice', age: 30]
def name = map['name'] // 'Alice'

Разбор:

  • list[1] вызывает getAt(1) у списка и возвращает второй элемент.
  • map['name'] обращается к значению по ключу в Map.
  • Один и тот же синтаксис [] работает и для индексов, и для ключей.
  • В пользовательских классах можно определить getAt/putAt, чтобы подключить такой же стиль доступа.

Более того, Groovy позволяет использовать этот оператор даже с пользовательскими классами, если они реализуют соответствующие методы. Это открывает возможности для создания DSL (Domain-Specific Languages), где поведение индексации может быть адаптировано под конкретную предметную область.


Оператор вызова метода

Оператор . в Groovy используется для вызова методов и доступа к свойствам объекта. Однако Groovy добавляет гибкость — если метод не найден во время выполнения, вызывается специальный метод methodMissing(), что позволяет динамически обрабатывать вызовы. Это лежит в основе многих метапрограммных возможностей языка.

Кроме того, Groovy поддерживает оператор *., известный как оператор распространения (spread operator). Он применяет метод ко всем элементам коллекции:

def names = ['Alice', 'Bob', 'Charlie']
def upperNames = names*.toUpperCase() // ['ALICE', 'BOB', 'CHARLIE']

Разбор:

  • names*. — spread-оператор: применяет вызов ко всем элементам коллекции.
  • toUpperCase() вызывается для каждой строки в списке.
  • Результат — новый список преобразованных значений (upperNames).
  • Это короче и чище, чем ручной цикл с временным массивом.

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


Оператор объединения строк

Groovy поддерживает интерполяцию строк с помощью двойных кавычек. Внутри таких строк можно встраивать выражения, заключённые в ${}:

def name = 'Groovy'
def greeting = "Привет, ${name}!" // "Привет, Groovy!"

Разбор:

  • Переменная name хранит исходное значение для подстановки.
  • В двойных кавычках создаётся GString с интерполяцией ${name}.
  • Выражение внутри ${...} может быть не только переменной, но и любым кодом.
  • Итоговая строка формируется при вычислении выражения.

Это не отдельный оператор в традиционном смысле, но механизм, который делает работу со строками более естественной и читаемой. Интерполяция работает только в GString (Groovy-строках), а не в обычных Java-строках, создаваемых одинарными кавычками.


Операторы присваивания

Помимо стандартного =, Groovy поддерживает множество составных операторов присваивания — +=, -=, *=, /=, %= и другие. Эти операторы работают не только с числами, но и с коллекциями, строками и другими типами, если соответствующие методы определены.

Например, для строк:

def text = 'Hello'
text += ' World' // 'Hello World'

Разбор:

  • += для строк означает конкатенацию и присваивание результата обратно в text.
  • Внутри это эквивалентно text = text + ' World'.
  • Строки неизменяемы, поэтому создаётся новый объект строки.
  • Конструкция удобна для накопления коротких текстовых сообщений.

Для списков:

def list = [1, 2]
list += 3 // [1, 2, 3]

Разбор:

  • Для списка += добавляет элемент и обновляет ссылку переменной.
  • Операция выглядит как арифметическое присваивание, но работает по правилам коллекций.
  • Результат — расширенный список с новым элементом в конце.
  • Это более выразительно, чем ручной вызов add в простых сценариях.

Такое поведение достигается за счёт перегрузки операторов на уровне методов, что делает язык гибким и интуитивным.


Операторы сравнения на идентичность

Groovy сохраняет оператор is() для проверки идентичности ссылок, аналогично Java. Однако, в отличие от Java, оператор == в Groovy не сравнивает ссылки, а вызывает equals(), что делает его более безопасным и ожидаемым в большинстве случаев. Если требуется именно сравнение по ссылке, используется метод is():

def a = new String('test')
def b = new String('test')
println a == b // true (сравнение содержимого)
println a.is(b) // false (разные объекты)

Разбор:

  • Создаются два разных объекта String с одинаковым содержимым.
  • a == b возвращает true, потому что сравнивается содержимое (equals).
  • a.is(b) возвращает false, потому что ссылки указывают на разные объекты.
  • Пример подчёркивает различие между value equality и reference equality.

Операторы регулярных выражений

Groovy вводит удобный синтаксис для работы с регулярными выражениями с помощью оператора ~:

def pattern = ~/foo/
def matcher = 'foo bar' =~ pattern
if (matcher) {
println matcher[0] // 'foo'
}

Разбор:

  • ~/foo/ компилирует регулярное выражение в объект Pattern.
  • 'foo bar' =~ pattern создаёт Matcher для поиска частичных совпадений.
  • Проверка if (matcher) в Groovy truth означает "совпадение найдено".
  • matcher[0] возвращает первое найденное совпадение.

Оператор =~ создаёт объект Matcher, который можно использовать как булево значение (указывает, найдено ли совпадение) и как список совпадений. Оператор ==~ проверяет, совпадает ли вся строка с шаблоном:

println '123' ==~ /\d+/ // true

Разбор:

  • ==~ требует полного совпадения всей строки с шаблоном.
  • Шаблон /\d+/ означает "одна или более цифр".
  • Строка '123' полностью состоит из цифр, поэтому результат true.
  • Для частичных совпадений используют =~, а не ==~.

Эти операторы значительно упрощают текстовую обработку и делают код компактным.


Перегрузка операторов

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

  • +plus()
  • -minus()
  • *multiply()
  • ==equals()
  • []getAt() / putAt()
  • <<leftShift()

Это позволяет создавать API, которые выглядят как встроенные в язык, повышая выразительность и читаемость. Например, можно реализовать сложение двух временных интервалов или конкатенацию пользовательских коллекций с помощью знакомого символа +.


Приоритет операторов и читаемость условий

Сложные условия лучше группировать скобками даже при знании приоритета. Такой код проще читать в ревью и проще менять без регрессий.

def isProd = env == "prod"
def hasAccess = user?.active && (user.role in ["admin", "ops"])
def canDeploy = isProd && hasAccess

Разбор:

  • isProd выделяет отдельную бизнес-проверку окружения.
  • hasAccess объединяет безопасную навигацию user?.active и проверку роли через in.
  • Скобки вокруг (user.role in ["admin", "ops"]) делают приоритет логики явным.
  • canDeploy собирает итоговое условие из двух именованных частей, упрощая чтение и ревью.

Если выражение длиннее одной строки, выносите части в именованные переменные, как в примере выше. Это снижает вероятность ошибок в &&/||.


Практика для коллекций и null-безопасности

Операторы Groovy раскрываются сильнее всего на коллекциях и nullable-данных:

def prices = [100G, 200G, 300G]
def discounted = prices.collect { it * 0.9G }

def city = user?.profile?.address?.city ?: "Unknown city"
def hasEditor = "editor" in (user?.roles ?: [])

Разбор:

  • 100G, 200G, 300G задают BigDecimal, что важно для точных вычислений.

  • collect { it * 0.9G } применяет скидку к каждому элементу и возвращает новый список.

  • user?.profile?.address?.city безопасно проходит цепочку вложенных объектов.

  • Elvis в (user?.roles ?: []) подставляет пустой список, если ролей нет.

  • Оператор in читабельно проверяет наличие роли в коллекции.

  • ?. убирает цепочки проверок if (x != null);

  • ?: задает значение по умолчанию;

  • in удобно проверяет принадлежность к коллекции.

Подробнее по типам и коллекциям: Типы данных и объявление переменных.


Сквозной кейс — фильтрация и правила скидок

Для каталога книг удобно описывать бизнес-правила прямо выражениями:

def books = [
[title: "Groovy in Action", price: 50G, active: true],
[title: "Legacy Java", price: 35G, active: false],
[title: "DSL Patterns", price: 70G, active: true]
]

def visible = books.findAll { it.active && it.price >= 40G }
def withDiscount = visible.collect { b ->
b + [discountPrice: (b.price * 0.9G)]
}

Разбор:

  • books — список карт с полями книги (title, price, active).
  • findAll { it.active && it.price >= 40G } фильтрует только активные книги с ценой от порога.
  • collect { b -> ... } преобразует каждый элемент в новую карту.
  • Выражение b + [discountPrice: ...] создаёт расширенную карту с новым вычисленным полем.
  • 0.9G сохраняет вычисления в BigDecimal, чтобы не терять точность скидки.

Здесь сразу видна связка операторов &&, >=, * и литералов коллекций.

Дальше по кейсу: Циклы и управляющие конструкции.