运算符
本章介绍 Groovy 编程语言的运算符。
1. 算术运算符
Groovy 支持你在数学和 Java 等其他编程语言中常见的算术运算符。所有 Java 算术运算符都受支持。让我们在下面的示例中逐一介绍它们。
1.1. 常规算术运算符
以下二元算术运算符在 Groovy 中可用
运算符 | 用途 | 备注 |
---|---|---|
|
加法 |
|
|
减法 |
|
|
乘法 |
|
|
除法 |
使用 |
|
余数 |
|
|
幂 |
查看关于 幂运算 的部分,以获取有关运算返回值类型的更多信息。 |
以下是一些使用这些运算符的示例
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 | “非”false 为 true |
2 | true “与” true 为 true |
3 | true “或” false 为 true |
3.1. 优先级
逻辑“非”的优先级高于逻辑“与”。
assert (!false && false) == false (1)
1 | 这里,断言为真(因为括号中的表达式为假),因为“非”的优先级高于“与”,所以它只应用于第一个“false”项;否则,它将应用于“与”的结果,将其变为真,并且断言将失败 |
逻辑“与”的优先级高于逻辑“或”。
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 标志设置为真 |
2 | 在第一种情况下,在重置 called 标志后,我们确认如果 || 的左操作数为真,则不会调用该函数,因为 || 会短路求值右操作数 |
3 | 在第二种情况下,左操作数为假,因此会调用该函数,如我们的标志现在为真所表明的那样 |
4 | 对于 && 来说也是如此,我们确认当左操作数为假时,不会调用该函数 |
5 | 但当左操作数为真时,会调用该函数 |
4. 位运算和位移运算符
4.1. 位运算符
Groovy 提供四个位运算符
-
&
: 位“与” -
|
: 位“或” -
^
: 位“异或”(排他“或”) -
~
: 位取反
位运算符可以应用于类型为 byte
、short
、int
、long
或 BigInteger
的参数。如果其中一个参数为 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 提供三个位移运算符
-
<<
: 左移 -
>>
: 右移 -
>>>
: 无符号右移
所有三个运算符都适用于左参数的类型为 byte
、short
、int
或 long
的情况。前两个运算符也可以应用于左参数的类型为 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 真值 结合使用
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 真值 兼容,因此你可以使其更加简单
result = string ? 'Found' : 'Not found'
5.3. Elvis 运算符
“Elvis 运算符”是三元运算符的简写。这种运算符的一个有用之处是,如果表达式解析为 false
-ish(如 Groovy 真值 中所述),则返回一个“合理默认”值。一个简单的示例可能如下所示
displayName = user.name ? user.name : 'Anonymous' (1)
displayName = user.name ?: 'Anonymous' (2)
1 | 使用三元运算符,你必须重复要分配的值 |
2 | 使用 Elvis 运算符,如果该值不是 false -ish,则使用该值 |
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 | result 为 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 | 定义一个重载的 doSomething 方法,接受一个 String 作为参数 |
2 | 定义一个重载的 doSomething 方法,接受一个 Integer 作为参数 |
3 | 在 doSomething 上创建一个单一的方法指针,而不指定参数类型 |
4 | 使用带 String 的方法指针会调用 doSomething 的 String 版本 |
5 | 使用带 Integer 的方法指针会调用 doSomething 的 Integer 版本 |
为了与 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! |
虽然你可以在 Pattern、Find 和 Match 运算符中使用大多数 String 形式,但我们建议大多数情况下使用斜杠字符串,以避免记住其他情况下所需的转义要求。 |
7.2. Find 运算符
除了构建模式之外,你还可以使用 find 运算符 =~
来直接创建一个 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. Match 运算符
匹配运算符 (==~
) 是 find 运算符的轻微变化,它不返回 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. 比较 Find 与 Match 运算符
通常,匹配运算符用于模式包含单个精确匹配的情况,否则 find 运算符可能更有用。
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,而不是抛出 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. 下标运算符
下标运算符是getAt
或putAt
的简写符号,取决于你在赋值左侧或右侧找到它
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 == list2 和 list1 != 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 类定义了从 User 到 Identifiable 的自定义转换规则 |
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 |
|
对象创建,显式括号 |
|
方法调用,闭包,字面量列表/映射 |
|
|
成员访问,方法闭包,字段/属性访问 |
|
|
安全解引用,传播,传播点,传播映射 |
|
|
按位取反/模式,非,类型转换 |
|
|
列表/映射/数组(安全)索引,后缀增量/减量 |
|
2 |
|
幂 |
3 |
|
前缀增量/减量,一元加,一元减 |
4 |
|
乘,除,余数 |
5 |
|
加,减 |
6 |
|
左/右(无符号)移位,包含/排除范围 |
7 |
|
小于/大于/等于,in,not in,instanceof,not instanceof,类型强制转换 |
8 |
|
相等,不相等,compareTo,identical to,not identical to |
|
正则表达式查找,正则表达式匹配 |
|
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.getAt(b) |
|
a.minus(b) |
|
a.putAt(b, c) |
|
a.multiply(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.positive() |
|
a.asType(b) |
|
a.negative() |
|
a.call() |
|
a.bitwiseNegate() |