本章涵盖 Groovy 编程语言中的运算符。

1. 算术运算符

Groovy 支持在数学和其他编程语言(如 Java)中常见的熟悉算术运算符。所有 Java 算术运算符都受支持。让我们通过以下示例来了解它们。

1.1. 普通算术运算符

Groovy 中提供了以下二元算术运算符:

运算符 用途 备注

+

加法

-

减法

*

乘法

/

除法

对于整数除法,请使用 intdiv(),有关除法返回类型的更多信息,请参阅关于整数除法的章节。

%

求余

**

幂运算

有关幂运算返回类型的更多信息,请参阅关于幂运算的章节。

以下是这些运算符的几个使用示例:

assert  1  + 2 == 3
assert  4  - 3 == 1
assert  3  * 5 == 15
assert  3  / 2 == 1.5
assert 10  % 3 == 1
assert  2 ** 3 == 8

1.2. 一元运算符

+- 运算符也可用作一元运算符。

assert +3 == 3
assert -4 == 0 - 4

assert -(-1) == 1  (1)
1 请注意使用括号括住表达式,以便将一元减号应用于该被括住的表达式。

就一元算术运算符而言,++(增量)和 --(减量)运算符都可用,前缀和后缀表示法均可使用。

def a = 2
def b = a++ * 3             (1)

assert a == 3 && b == 6

def c = 3
def d = c-- * 2             (2)

assert c == 2 && d == 6

def e = 1
def f = ++e + 3             (3)

assert e == 2 && f == 5

def g = 4
def h = --g + 1             (4)

assert g == 3 && h == 4
1 后缀增量将在表达式求值并赋值给 b 后递增 a
2 后缀减量将在表达式求值并赋值给 d 后递减 c
3 前缀增量将在表达式求值并赋值给 f 前递增 e
4 前缀减量将在表达式求值并赋值给 h 前递减 g

对于布尔值上的一元非运算符,请参阅条件运算符

1.3. 赋值算术运算符

我们上面看到的二元算术运算符也可以以赋值形式使用。

  • +=

  • -=

  • *=

  • /=

  • %=

  • **=

让我们看看它们的应用。

def a = 4
a += 3

assert a == 7

def b = 5
b -= 3

assert b == 2

def c = 5
c *= 3

assert c == 15

def d = 10
d /= 2

assert d == 5

def e = 10
e %= 3

assert e == 1

def f = 3
f **= 2

assert f == 9

2. 关系运算符

关系运算符允许对象之间的比较,以确定两个对象是否相同或不同,或者一个是否大于、小于或等于另一个。

提供以下运算符:

运算符 用途

==

等于

!=

不等于

<

小于

<=

小于或等于

>

大于

>=

大于或等于

===

恒等(Groovy 3.0.0 起)

!==

不恒等(Groovy 3.0.0 起)

以下是使用这些运算符进行简单数字比较的一些示例。

assert 1 + 2 == 3
assert 3 != 4

assert -2 < 3
assert 2 <= 2
assert 3 <= 4

assert 5 > 1
assert 5 >= -2

===!== 都受支持,它们分别与调用 is() 方法和对 is() 方法的调用取反相同。

import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode
class Creature { String type }

def cat = new Creature(type: 'cat')
def copyCat = cat
def lion = new Creature(type: 'cat')

assert cat.equals(lion) // Java logical equality
assert cat == lion      // Groovy shorthand operator

assert cat.is(copyCat)  // Groovy identity
assert cat === copyCat  // operator shorthand
assert cat !== lion     // negated operator shorthand

3. 逻辑运算符

Groovy 为布尔表达式提供了三个逻辑运算符:

  • &&:逻辑“与”

  • ||:逻辑“或”

  • !:逻辑“非”

让我们通过以下示例来说明它们:

assert !false           (1)
assert true && true     (2)
assert true || false    (3)
1 “非”假为真
2 真“与”真为真
3 真“或”假为真

3.1. 优先级

逻辑“非”的优先级高于逻辑“与”。

assert (!false && false) == false   (1)
1 在这里,断言为真(因为括号中的表达式为假),因为“非”的优先级高于“与”,所以它只应用于第一个“假”项;否则,它将应用于“与”的结果,将其变为真,断言将失败。

逻辑“与”的优先级高于逻辑“或”。

assert true || true && false        (1)
1 在这里,断言为真,因为“与”的优先级高于“或”,因此“或”最后执行并返回真,因为它有一个真参数;否则,“与”将最后执行并返回假,因为它有一个假参数,断言将失败。

3.2. 短路求值

逻辑 || 运算符支持短路求值:如果左操作数为真,则它知道结果无论如何都将为真,因此它不会评估右操作数。只有当左操作数为假时,才会评估右操作数。

同样,对于逻辑 && 运算符:如果左操作数为假,则它知道结果无论如何都将为假,因此它不会评估右操作数。只有当左操作数为真时,才会评估右操作数。

boolean checkIfCalled() {   (1)
    called = true
}

called = false
true || checkIfCalled()
assert !called              (2)

called = false
false || checkIfCalled()
assert called               (3)

called = false
false && checkIfCalled()
assert !called              (4)

called = false
true && checkIfCalled()
assert called               (5)
1 我们创建一个函数,每当它被调用时,将 called 标志设置为 true。
2 在第一种情况下,重置 called 标志后,我们确认如果 || 的左操作数为 true,则不调用该函数,因为 || 会短路右操作数的评估。
3 在第二种情况下,左操作数为 false,因此调用了该函数,这由我们的标志现在为 true 的事实表明。
4 同样对于 &&,我们确认在左操作数为 false 时不调用该函数。
5 但是当左操作数为 true 时,该函数被调用。

4. 位运算符和位移运算符

4.1. 位运算符

Groovy 提供四种位运算符:

  • &:按位“与”

  • |:按位“或”

  • ^:按位“异或”(独占“或”)

  • ~:按位取反

位运算符可应用于 byteshortintlongBigInteger 类型的参数。如果其中一个参数是 BigInteger,则结果将是 BigInteger 类型;否则,如果其中一个参数是 long,则结果将是 long 类型;否则,结果将是 int 类型。

int a = 0b00101010
assert a == 42
int b = 0b00001000
assert b == 8
assert (a & a) == a                     (1)
assert (a & b) == b                     (2)
assert (a | a) == a                     (3)
assert (a | b) == a                     (4)

int mask = 0b11111111                   (5)
assert ((a ^ a) & mask) == 0b00000000   (6)
assert ((a ^ b) & mask) == 0b00100010   (7)
assert ((~a) & mask)    == 0b11010101   (8)
1 按位与
2 按位与返回公共位
3 按位或
4 按位或返回所有“1”位
5 设置掩码以仅检查最后 8 位
6 自身按位异或返回 0
7 按位异或
8 按位取反

值得注意的是,原始类型的内部表示遵循 Java 语言规范。特别是,原始类型是有符号的,这意味着对于按位取反,最好始终使用掩码来仅检索必要的位。

在 Groovy 中,位运算符是可重载的,这意味着您可以为任何类型的对象定义这些运算符的行为。

4.2. 位移运算符

Groovy 提供三种位移运算符:

  • <<:左移

  • >>:右移

  • >>>:无符号右移

所有这三个运算符都适用于左参数类型为 byteshortintlong 的情况。前两个运算符也可以应用于左参数类型为 BigInteger 的情况。如果左参数是 BigInteger,则结果将是 BigInteger 类型;否则,如果左参数是 long,则结果将是 long 类型;否则,结果将是 int 类型。

assert 12.equals(3 << 2)           (1)
assert 24L.equals(3L << 3)         (1)
assert 48G.equals(3G << 4)         (1)

assert 4095 == -200 >>> 20
assert -1 == -200 >> 20
assert 2G == 5G >> 1
assert -3G == -5G >> 1
1 使用 equals 方法而不是 == 来确认结果类型

在 Groovy 中,位移运算符是可重载的,这意味着您可以为任何类型的对象定义这些运算符的行为。

5. 条件运算符

5.1. 非运算符

“非”运算符用感叹号 (!) 表示,并反转底层布尔表达式的结果。特别是,可以将 not 运算符与 Groovy truth 结合使用。

assert (!true)    == false                      (1)
assert (!'foo')   == false                      (2)
assert (!'')      == true                       (3)
1 true 的否定是 false
2 'foo' 是一个非空字符串,评估为 true,因此否定返回 false
3 '' 是一个空字符串,评估为 false,因此否定返回 true

5.2. 三元运算符

三元运算符是一种快捷表达式,相当于一个将某个值赋给变量的 if/else 分支。

而不是

if (string!=null && string.length()>0) {
    result = 'Found'
} else {
    result = 'Not found'
}

你可以写

result = (string!=null && string.length()>0) ? 'Found' : 'Not found'

三元运算符也与 Groovy truth 兼容,因此你可以让它更简单。

result = string ? 'Found' : 'Not found'

5.3. Elvis 运算符

“Elvis 运算符”是三元运算符的缩写。它方便的一个实例是,如果表达式解析为 false(如Groovy truth中),则返回一个“合理默认”值。一个简单的示例如下所示:

displayName = user.name ? user.name : 'Anonymous'   (1)
displayName = user.name ?: 'Anonymous'              (2)
1 使用三元运算符,您必须重复要赋值的值
2 使用 Elvis 运算符,如果被测试的值不是 false,则使用该值

使用 Elvis 运算符可以减少代码的冗长性,并减少重构时出错的风险,因为它消除了在条件和正返回值中重复被测试表达式的需要。

5.4. Elvis 赋值运算符

Groovy 3.0.0 引入了 Elvis 运算符,例如:

import groovy.transform.ToString

@ToString(includePackage = false)
class Element {
    String name
    int atomicNumber
}

def he = new Element(name: 'Helium')
he.with {
    name = name ?: 'Hydrogen'   // existing Elvis operator
    atomicNumber ?= 2           // new Elvis assignment shorthand
}
assert he.toString() == 'Element(Helium, 2)'

6. 对象运算符

6.1. 安全导航运算符

安全导航运算符用于避免 NullPointerException。通常,当您引用一个对象时,您可能需要验证它是否不为 null,然后才能访问该对象的方法或属性。为了避免这种情况,安全导航运算符将简单地返回 null 而不是抛出异常,如下所示:

def person = Person.find { it.id == 123 }    (1)
def name = person?.name                      (2)
assert name == null                          (3)
1 find 将返回一个 null 实例
2 使用空安全运算符可以防止 NullPointerException
3 结果为 null

6.2. 直接字段访问运算符

通常在 Groovy 中,当你编写这样的代码时

class User {
    public final String name                 (1)
    User(String name) { this.name = name}
    String getName() { "Name: $name" }       (2)
}
def user = new User('Bob')
assert user.name == 'Name: Bob'              (3)
1 公共字段 name
2 一个返回自定义字符串的 name 的 getter
3 调用 getter

user.name 调用会触发对同名属性的调用,也就是说,在这里,调用 name 的 getter。如果你想检索字段而不是调用 getter,你可以使用直接字段访问运算符:

assert user.@name == 'Bob'                   (1)
1 使用 .@ 强制使用字段而不是 getter

6.3. 方法指针运算符

方法指针运算符 (.&) 可用于将方法引用存储在变量中,以便稍后调用它。

def str = 'example of method reference'            (1)
def fun = str.&toUpperCase                         (2)
def upper = fun()                                  (3)
assert upper == str.toUpperCase()                  (4)
1 str 变量包含一个 String
2 我们将 str 实例上 toUpperCase 方法的引用存储在名为 fun 的变量中。
3 fun 可以像普通方法一样调用
4 我们可以检查结果是否与我们直接在 str 上调用它相同。

使用方法指针有多个优点。首先,这种方法指针的类型是 groovy.lang.Closure,因此它可以在任何需要闭包的地方使用。特别是,它适合将现有方法转换为策略模式的需求。

def transform(List elements, Closure action) {                    (1)
    def result = []
    elements.each {
        result << action(it)
    }
    result
}
String describe(Person p) {                                       (2)
    "$p.name is $p.age"
}
def action = this.&describe                                       (3)
def list = [
    new Person(name: 'Bob',   age: 42),
    new Person(name: 'Julia', age: 35)]                           (4)
assert transform(list, action) == ['Bob is 42', 'Julia is 35']    (5)
1 transform 方法获取列表中的每个元素,并在它们上调用 action 闭包,返回一个新列表。
2 我们定义一个函数,它接收一个 Person 并返回一个 String
3 我们在此函数上创建了一个方法指针
4 我们创建要收集描述符的元素列表
5 方法指针可以在期望 Closure 的地方使用。

方法指针由接收器和方法名称绑定。参数在运行时解析,这意味着如果你有多个同名方法,语法没有区别,只有在运行时才会解析要调用的适当方法。

def doSomething(String str) { str.toUpperCase() }    (1)
def doSomething(Integer x) { 2*x }                   (2)
def reference = this.&doSomething                    (3)
assert reference('foo') == 'FOO'                     (4)
assert reference(123)   == 246                       (5)
1 定义接受 String 作为参数的重载 doSomething 方法
2 定义接受 Integer 作为参数的重载 doSomething 方法
3 doSomething 上创建一个方法指针,不指定参数类型。
4 使用带有 String 的方法指针会调用 doSomethingString 版本。
5 使用带有 Integer 的方法指针会调用 doSomethingInteger 版本。

为了与 Java 8 方法引用期望保持一致,在 Groovy 3 及更高版本中,您可以将 new 用作方法名,以获取指向构造函数的方法指针。

def foo  = BigInteger.&new
def fortyTwo = foo('42')
assert fortyTwo == 42G

同样在 Groovy 3 及更高版本中,您可以获取指向类的实例方法的方法指针。此方法指针接受一个额外的参数,即用于调用方法的接收器实例。

def instanceMethod = String.&toUpperCase
assert instanceMethod('foo') == 'FOO'

为了向后兼容,任何恰好具有正确调用参数的静态方法在此情况下将优先于实例方法。

6.4. 方法引用运算符

Groovy 3+ 中的 Parrot 解析器支持 Java 8+ 方法引用运算符。方法引用运算符 (::) 可用于在期望函数式接口的上下文​​中引用方法或构造函数。这与 Groovy 的方法指针运算符提供的功能有所重叠。事实上,对于动态 Groovy,方法引用运算符只是方法指针运算符的别名。对于静态 Groovy,该运算符会生成类似于 Java 为相同上下文生成的字节码。

以下脚本中显示了一些突出显示各种支持的方法引用情况的示例:

import groovy.transform.CompileStatic
import static java.util.stream.Collectors.toList

@CompileStatic
void methodRefs() {
    assert 6G == [1G, 2G, 3G].stream().reduce(0G, BigInteger::add)                           (1)

    assert [4G, 5G, 6G] == [1G, 2G, 3G].stream().map(3G::add).collect(toList())              (2)

    assert [1G, 2G, 3G] == [1L, 2L, 3L].stream().map(BigInteger::valueOf).collect(toList())  (3)

    assert [1G, 2G, 3G] == [1L, 2L, 3L].stream().map(3G::valueOf).collect(toList())          (4)
}

methodRefs()
1 类实例方法引用:add(BigInteger val) 是 BigInteger 中的一个实例方法
2 对象实例方法引用:add(BigInteger val) 是对象 3G 的实例方法
3 类静态方法引用:valueOf(long val) 是 BigInteger 类的静态方法
4 对象静态方法引用:valueOf(long val) 是对象 3G 的静态方法(在正常情况下,有些人认为这种风格不好)

以下脚本中显示了一些突出显示各种支持的构造函数引用情况的示例:

@CompileStatic
void constructorRefs() {
    assert [1, 2, 3] == ['1', '2', '3'].stream().map(Integer::valueOf).collect(toList())  (1)

    def result = [1, 2, 3].stream().toArray(Integer[]::new)                           (2)
    assert result instanceof Integer[]
    assert result.toString() == '[1, 2, 3]'
}

constructorRefs()
1 类构造函数引用
2 数组构造函数引用

7. 正则表达式运算符

7.1. 模式运算符

模式运算符 (~) 提供了一种创建 java.util.regex.Pattern 实例的简单方法。

def p = ~/foo/
assert p instanceof Pattern

虽然通常,您会在斜线字符串中找到带有表达式的模式运算符,但它可以在 Groovy 中与任何类型的 String 一起使用。

p = ~'foo'                                                        (1)
p = ~"foo"                                                        (2)
p = ~$/dollar/slashy $ string/$                                   (3)
p = ~"${pattern}"                                                 (4)
1 使用单引号字符串
2 使用双引号字符串
3 美元斜杠字符串允许您使用斜杠和美元符号而无需转义它们。
4 你也可以使用 GString!
虽然您可以将大多数字符串形式与模式、查找和匹配运算符一起使用,但我们建议大多数情况下使用斜线字符串,以省去记住否则所需的转义要求。

7.2. 查找运算符

除了构建模式之外,您还可以使用查找运算符 =~ 直接创建 java.util.regex.Matcher 实例。

def text = "some text to match"
def m = text =~ /match/                                           (1)
assert m instanceof Matcher                                       (2)
if (!m) {                                                         (3)
    throw new RuntimeException("Oops, text not found!")
}
1 =~ 使用右侧的模式创建针对 text 变量的匹配器。
2 =~ 的返回类型是 Matcher
3 等同于调用 if (!m.find(0))

由于 Matcher 通过调用其 find 方法强制转换为 boolean,因此 =~ 运算符与 Perl 的 =~ 运算符的简单使用保持一致,当它作为谓词出现时(在 if?: 等中)。当目的是迭代指定模式的匹配项时(在 while 等中),直接在匹配器上调用 find() 或使用 iterator DGM。

7.3. 匹配运算符

匹配运算符 (==~) 是查找运算符的一个细微变体,它不返回 Matcher,而是返回一个布尔值,并要求严格匹配输入字符串。

m = text ==~ /match/                                              (1)
assert m instanceof Boolean                                       (2)
if (m) {                                                          (3)
    throw new RuntimeException("Should not reach that point!")
}
1 ==~ 将主题与正则表达式匹配,但匹配必须严格。
2 因此,==~ 的返回类型是 boolean
3 等同于调用 if (text ==~ /match/)

7.4. 比较查找运算符与匹配运算符

通常,当模式涉及单个精确匹配时,使用匹配运算符;否则,查找运算符可能更有用。

assert 'two words' ==~ /\S+\s+\S+/
assert 'two words' ==~ /^\S+\s+\S+$/         (1)
assert !(' leading space' ==~ /\S+\s+\S+/)   (2)

def m1 = 'two words' =~ /^\S+\s+\S+$/
assert m1.size() == 1                          (3)
def m2 = 'now three words' =~ /^\S+\s+\S+$/    (4)
assert m2.size() == 0                          (5)
def m3 = 'now three words' =~ /\S+\s+\S+/
assert m3.size() == 1                          (6)
assert m3[0] == 'now three'
def m4 = ' leading space' =~ /\S+\s+\S+/
assert m4.size() == 1                          (7)
assert m4[0] == 'leading space'
def m5 = 'and with four words' =~ /\S+\s+\S+/
assert m5.size() == 2                          (8)
assert m5[0] == 'and with'
assert m5[1] == 'four words'
1 等价,但不鼓励显式 ^ 和 $,因为它们不需要。
2 没有匹配,因为有前导空格。
3 一个匹配
4 ^ 和 $ 表示需要精确匹配
5 零匹配
6 一个匹配,从第一个单词开始贪婪地匹配。
7 一个匹配,忽略前导空格。
8 两个匹配

8. 其他运算符

8.1. 展开运算符

展开点运算符 (*.),通常简写为展开运算符,用于对聚合对象的所有项目调用操作。它等同于对每个项目调用操作并将结果收集到列表中。

class Car {
    String make
    String model
}
def cars = [
       new Car(make: 'Peugeot', model: '508'),
       new Car(make: 'Renault', model: 'Clio')]       (1)
def makes = cars*.make                                (2)
assert makes == ['Peugeot', 'Renault']                (3)
1 构建一个 Car 项目列表。该列表是对象的集合。
2 对列表调用展开运算符,访问每个项目的 make 属性。
3 返回一个对应于 make 项目集合的字符串列表。

表达式 cars*.make 等同于 cars.collect{ it.make }。Groovy 的 GPath 符号在引用的属性不是包含列表的属性时允许使用快捷方式,在这种情况下它会自动展开。在前面提到的情况下,可以使用表达式 cars.make,尽管通常建议保留显式展开点运算符。

展开运算符是 null 安全的,这意味着如果集合中的元素为 null,它将返回 null 而不是抛出 NullPointerException

cars = [
   new Car(make: 'Peugeot', model: '508'),
   null,                                              (1)
   new Car(make: 'Renault', model: 'Clio')]
assert cars*.make == ['Peugeot', null, 'Renault']     (2)
assert null*.make == null                             (3)
1 构建一个其中一个元素为 null 的列表。
2 使用展开运算符不会抛出 NullPointerException
3 接收者也可能为 null,在这种情况下返回值为 null

展开运算符可用于实现 Iterable 接口的任何类。

class Component {
    Integer id
    String name
}
class CompositeObject implements Iterable<Component> {
    def components = [
        new Component(id: 1, name: 'Foo'),
        new Component(id: 2, name: 'Bar')]

    @Override
    Iterator<Component> iterator() {
        components.iterator()
    }
}
def composite = new CompositeObject()
assert composite*.id == [1,2]
assert composite*.name == ['Foo','Bar']

当处理本身包含聚合的数据结构聚合时,使用展开点运算符的多次调用(这里是 cars*.models*.name)。

class Make {
    String name
    List<Model> models
}

@Canonical
class Model {
    String name
}

def cars = [
    new Make(name: 'Peugeot',
             models: [new Model('408'), new Model('508')]),
    new Make(name: 'Renault',
             models: [new Model('Clio'), new Model('Captur')])
]

def makes = cars*.name
assert makes == ['Peugeot', 'Renault']

def models = cars*.models*.name
assert models == [['408', '508'], ['Clio', 'Captur']]
assert models.sum() == ['408', '508', 'Clio', 'Captur'] // flatten one level
assert models.flatten() == ['408', '508', 'Clio', 'Captur'] // flatten all levels (one in this case)

对于集合的集合,考虑使用 collectNested DGM 方法而不是展开点运算符。

class Car {
    String make
    String model
}
def cars = [
   [
       new Car(make: 'Peugeot', model: '408'),
       new Car(make: 'Peugeot', model: '508')
   ], [
       new Car(make: 'Renault', model: 'Clio'),
       new Car(make: 'Renault', model: 'Captur')
   ]
]
def models = cars.collectNested{ it.model }
assert models == [['408', '508'], ['Clio', 'Captur']]

8.1.1. 展开方法参数

在某些情况下,方法调用的参数可能存在于一个需要适配到方法参数的列表中。在这种情况下,您可以使用展开运算符调用方法。例如,假设您有以下方法签名:

int function(int x, int y, int z) {
    x*y+z
}

然后如果你有以下列表:

def args = [4,5,6]

你可以直接调用方法,无需定义中间变量。

assert function(*args) == 26

甚至可以将普通参数与展开参数混合使用。

args = [4]
assert function(*args,5,6) == 26

8.1.2. 展开列表元素

当在列表字面量中使用时,展开运算符的行为就像展开元素的内容被内联到列表中一样。

def items = [4,5]                      (1)
def list = [1,2,3,*items,6]            (2)
assert list == [1,2,3,4,5,6]           (3)
1 items 是一个列表。
2 我们想将 items 列表的内容直接插入到 list 中,而无需调用 addAll
3 items 的内容已内联到 list 中。

8.1.3. 展开映射元素

展开映射运算符的工作方式与展开列表运算符类似,但适用于映射。它允许您将映射的内容内联到另一个映射字面量中,如下例所示:

def m1 = [c:3, d:4]                   (1)
def map = [a:1, b:2, *:m1]            (2)
assert map == [a:1, b:2, c:3, d:4]    (3)
1 m1 是我们想要内联的映射。
2 我们使用 *:m1 符号将 m1 的内容展开到 map 中。
3 map 包含 m1 的所有元素。

展开映射运算符的位置是相关的,如下例所示:

def m1 = [c:3, d:4]                   (1)
def map = [a:1, b:2, *:m1, d: 8]      (2)
assert map == [a:1, b:2, c:3, d:8]    (3)
1 m1 是我们想要内联的映射。
2 我们使用 *:m1 符号将 m1 的内容展开到 map 中,但展开后重新定义了键 d
3 map 包含所有预期的键,但 d 被重新定义了。

8.2. 范围运算符

Groovy 支持范围概念并提供了创建对象范围的符号 (..)。

def range = 0..5                                    (1)
assert (0..5).collect() == [0, 1, 2, 3, 4, 5]       (2)
assert (0..<5).collect() == [0, 1, 2, 3, 4]         (3)
assert (0<..5).collect() == [1, 2, 3, 4, 5]         (4)
assert (0<..<5).collect() == [1, 2, 3, 4]           (5)
assert (0..5) instanceof List                       (6)
assert (0..5).size() == 6                           (7)
1 一个简单的整数范围,存储在局部变量中。
2 一个 IntRange,包含边界。
3 一个 IntRange,不包含上限。
4 一个 IntRange,不包含下限。
5 一个 IntRange,不包含下限和上限。
6 一个 groovy.lang.Range 实现 List 接口。
7 这意味着您可以在其上调用 size 方法。

范围实现是轻量级的,这意味着只存储下限和上限。您可以从任何具有 next()previous() 方法来确定范围内下一个/上一个项目的 Comparable 对象创建范围。例如,您可以通过这种方式创建字符范围:

assert ('a'..'d').collect() == ['a','b','c','d']

8.3. 飞船运算符

飞船运算符 (<=>) 委托给 compareTo 方法。

assert (1 <=> 1) == 0
assert (1 <=> 2) == -1
assert (2 <=> 1) == 1
assert ('a' <=> 'z') == -1

8.4. 下标运算符

下标运算符是 getAtputAt 的简写表示法,具体取决于它在赋值的左侧还是右侧。

def list = [0,1,2,3,4]
assert list[2] == 2                         (1)
list[2] = 4                                 (2)
assert list[0..2] == [0,1,4]                (3)
list[0..2] = [6,6,6]                        (4)
assert list == [6,6,6,3,4]                  (5)
1 [2] 可以用来代替 getAt(2)
2 如果位于赋值的左侧,将调用 putAt
3 getAt 也支持范围
4 putAt 也一样
5 列表被修改了。

下标运算符与 getAt/putAt 的自定义实现结合使用,是解构对象的便捷方式。

class User {
    Long id
    String name
    def getAt(int i) {                                             (1)
        switch (i) {
            case 0: return id
            case 1: return name
        }
        throw new IllegalArgumentException("No such element $i")
    }
    void putAt(int i, def value) {                                 (2)
        switch (i) {
            case 0: id = value; return
            case 1: name = value; return
        }
        throw new IllegalArgumentException("No such element $i")
    }
}
def user = new User(id: 1, name: 'Alex')                           (3)
assert user[0] == 1                                                (4)
assert user[1] == 'Alex'                                           (5)
user[1] = 'Bob'                                                    (6)
assert user.name == 'Bob'                                          (7)
1 User 类定义了一个自定义的 getAt 实现。
2 User 类定义了一个自定义的 putAt 实现。
3 创建一个示例用户。
4 使用下标运算符,索引为 0 允许检索用户 ID。
5 使用下标运算符,索引为 1 允许检索用户名。
6 我们可以使用下标运算符写入属性,这得益于对 putAt 的委托。
7 并检查确实是 name 属性被更改了。

8.5. 安全索引运算符

Groovy 3.0.0 引入了安全索引运算符,即 ?[],类似于 ?.。例如:

String[] array = ['a', 'b']
assert 'b' == array?[1]      // get using normal array index
array?[1] = 'c'              // set using normal array index
assert 'c' == array?[1]

array = null
assert null == array?[1]     // return null for all index values
array?[1] = 'c'              // quietly ignore attempt to set value
assert null == array?[1]

def personInfo = [name: 'Daniel.Sun', location: 'Shanghai']
assert 'Daniel.Sun' == personInfo?['name']      // get using normal map index
personInfo?['name'] = 'sunlan'                  // set using normal map index
assert 'sunlan' == personInfo?['name']

personInfo = null
assert null == personInfo?['name']              // return null for all map values
personInfo?['name'] = 'sunlan'                  // quietly ignore attempt to set value
assert null == personInfo?['name']

8.6. 成员运算符

成员运算符 (in) 等同于调用 isCase 方法。在 List 的上下文中,它等同于调用 contains,如下例所示:

def list = ['Grace','Rob','Emmy']
assert ('Emmy' in list)                     (1)
assert ('Alex' !in list)                    (2)
1 等同于调用 list.contains('Emmy')list.isCase('Emmy')
2 成员否定等同于调用 !list.contains('Emmy')!list.isCase('Emmy')

8.7. 身份运算符

在 Groovy 中,使用 == 进行相等性测试与在 Java 中使用相同的运算符不同。在 Groovy 中,它调用 equals。如果你想比较引用相等性,你应该使用 is,如下例所示:

def list1 = ['Groovy 1.8','Groovy 2.0','Groovy 2.3']        (1)
def list2 = ['Groovy 1.8','Groovy 2.0','Groovy 2.3']        (2)
assert list1 == list2                                       (3)
assert !list1.is(list2)                                     (4)
assert list1 !== list2                                      (5)
1 创建一个字符串列表。
2 创建另一个包含相同元素的字符串列表。
3 使用 ==,我们测试对象相等性,等同于 Java 中的 list1.equals(list2)
4 使用 is,我们可以检查引用是否不同,等同于 Java 中的 list1 == list2
5 使用 ===!==(Groovy 3.0.0 起支持并推荐),我们也可以检查引用是否不同,等同于 Java 中的 list1 == list2list1 != list2

8.8. 强制转换运算符

强制转换运算符 (as) 是强制类型转换的一种变体。强制转换将对象从一种类型转换为另一种类型,而要求它们兼容赋值。让我们举一个例子:

String input = '42'
Integer num = (Integer) input                      (1)
1 String 无法赋值给 Integer,因此在运行时会产生 ClassCastException

这可以通过使用强制转换来解决。

String input = '42'
Integer num = input as Integer                      (1)
1 String 无法赋值给 Integer,但使用 as 将其强制转换Integer

当一个对象被强制转换为另一个对象时,除非目标类型与源类型相同,否则强制转换将返回一个对象。强制转换的规则因源类型和目标类型而异,如果找不到转换规则,强制转换可能会失败。通过 asType 方法可以实现自定义转换规则。

class Identifiable {
    String name
}
class User {
    Long id
    String name
    def asType(Class target) {                                              (1)
        if (target == Identifiable) {
            return new Identifiable(name: name)
        }
        throw new ClassCastException("User cannot be coerced into $target")
    }
}
def u = new User(name: 'Xavier')                                            (2)
def p = u as Identifiable                                                   (3)
assert p instanceof Identifiable                                            (4)
assert !(p instanceof User)                                                 (5)
1 User 类定义了从 UserIdentifiable 的自定义转换规则。
2 我们创建一个 User 实例
3 我们将 User 实例强制转换为 Identifiable
4 目标是 Identifiable 的实例
5 目标不再是 User 的实例。

8.9. 菱形运算符

菱形运算符 (<>) 是一个语法糖运算符,用于支持与 Java 7 中同名运算符的兼容性。它用于指示应从声明中推断泛型类型。

List<String> strings = new LinkedList<>()

在动态 Groovy 中,这完全不被使用。在静态类型检查的 Groovy 中,它也是可选的,因为无论是否存在此运算符,Groovy 类型检查器都会执行类型推断。

8.10. 调用运算符

调用运算符 () 用于隐式调用名为 call 的方法。对于任何定义了 call 方法的对象,您可以省略 .call 部分,转而使用调用运算符。

class MyCallable {
    int call(int x) {           (1)
        2*x
    }
}

def mc = new MyCallable()
assert mc.call(2) == 4          (2)
assert mc(2) == 4               (3)
1 MyCallable 定义了一个名为 call 的方法。请注意,它不需要实现 java.util.concurrent.Callable
2 我们可以使用经典方法调用语法调用该方法。
3 或者我们可以借助调用运算符省略 .call

9. 运算符优先级

下表列出了所有 Groovy 运算符的优先级顺序。

级别 运算符 名称

1

new   ()

对象创建,显式括号

()   {}   []

方法调用,闭包,列表/映射字面量

.   .&   .@

成员访问、方法闭包、字段/属性访问

?.   *   *.   *:

安全解引用,展开,展开点,展开映射

~   !   (type)

按位取反/模式,非,类型转换

[]   ?[]   ++   --

列表/映射/数组(安全)索引,后置增/减量

2

**

幂运算

3

+` {nbsp} `--` {nbsp} `   -

前置增/减量,一元加,一元减

4

*   /   %

乘法,除法,求余

5

+   -

加法,减法

6

<<   >>   >>>   ..   ..<   <..<   <..

左/右(无符号)位移,包含/不包含范围

7

<   <=   >   >=   in   !in   instanceof   !instanceof   as

小于/大于/或等于,包含,不包含,instanceof,not instanceof,类型强制转换

8

==   !=   <=>   ===   !==

等于,不等于,比较,恒等于,不恒等于

=~   ==~

正则表达式查找,正则表达式匹配

9

&

二进制/按位与

10

^

二进制/按位异或

11

|

二进制/按位或

12

&&

逻辑与

13

||

逻辑或

14

? :

三元条件

?:

Elvis 运算符

15

=   **=   *=   /=   %=   +=   -=  
<<=   >>=   >>>=   &=   ^=   |=     ?=

各种赋值

10. 运算符重载

Groovy 允许您重载各种运算符,以便它们可以与您自己的类一起使用。考虑这个简单的类:

class Bucket {
    int size

    Bucket(int size) { this.size = size }

    Bucket plus(Bucket other) {                     (1)
        return new Bucket(this.size + other.size)
    }
}
1 Bucket 实现了一个名为 plus() 的特殊方法。

只需实现 plus() 方法,Bucket 类现在就可以与 + 运算符一起使用,如下所示:

def b1 = new Bucket(4)
def b2 = new Bucket(11)
assert (b1 + b2).size == 15                         (1)
1 两个 Bucket 对象可以使用 + 运算符相加。

所有(非比较器)Groovy 运算符都有一个对应的方法,您可以在自己的类中实现这些方法。唯一的要求是您的方法是公共的,具有正确的名称,并且具有正确数量的参数。参数类型取决于您想要在运算符右侧支持的类型。例如,您可以支持以下语句:

assert (b1 + 11).size == 15

通过实现具有此签名的 plus() 方法。

Bucket plus(int capacity) {
    return new Bucket(this.size + capacity)
}

以下是运算符及其对应方法的完整列表:

运算符 方法 运算符 方法

+

a.plus(b)

a[b]

a.getAt(b)

-

a.minus(b)

a[b] = c

a.putAt(b, c)

*

a.multiply(b)

a in b

b.isCase(a)

/

a.div(b)

<<

a.leftShift(b)

%

a.mod(b)

>>

a.rightShift(b)

**

a.power(b)

>>>

a.rightShiftUnsigned(b)

|

a.or(b)

++

a.next()

&

a.and(b)

--

a.previous()

^

a.xor(b)

+a

a.positive()

as

a.asType(b)

-a

a.negative()

a()

a.call()

~a

a.bitwiseNegate()