Unmi 学习 Groovy 之操作符重载

Java 相比于 C++ 语法来说去除了指针及显式内存释放,受到不少赞誉,的确避免很多的出错的可能性,提高了生产率。可是把操作符重载也拿掉了,也没了条件编译。这两项特性在 C# 是有的。操作符在 C++ 中似乎不是很好理解,它可以带来很大的灵活性,和操作的直观性。Java 大约想的是过宽的灵活性怕带来过度的滥用,在大型项目会有所不利。


因此由 Java 所衍生的 Groovy 脚本像 Perl、Python、Ruby 一样又有了操作符重载,脚本基于其应用领域需要更多的灵活性和自由度。

Groovy 中对操作的操作比起 C++ 中来说更简单,Groovy 中是把操作符映射到对应命名方法的调用,你只要重载了该命名方法便是重载了相应的操作符,比如说加号+ 映射到 plus(obj) 方法,你只要重载了 plus(obj) 即改变了加号+ 的形为。对于其他符号也是一样的。

举例说明,例如有一个 Basket,装着白菜和萝卜的数量分别为 cabbageAmt, radishAmt,当两个 Basket 中东西要摆放到一个大 Basket 里并返回一个新的 Basket 实例时,就要对先前两个篮子里的白菜和萝卜数量分别相加,这时候就是要重载 Basket 的 plus 方法,代码如下:
 1class Basket{
 2    int cabbageAmt,radishAmt;
 3    def plus(Basket anotherBasket){ //两个 Basket 的加操作会调用这个方法
 4        return new Basket(cabbageAmt:this.cabbageAmt+anotherBasket.cabbageAmt,
 5            radishAmt:this.radishAmt+anotherBasket.radishAmt);
 6    }
 7    String toString(){
 8        return "In that basket, the amount of cabbage is "
 9            +cabbageAmt+" and the amount of radish is "+radishAmt;
10    }
11}
12
13Basket basket1 = new Basket(cabbageAmt:1,radishAmt:2);
14Basket basket2 = new Basket(cabbageAmt:3,radishAmt:4);
15Basket basket3 = basket1 + basket2;
16println basket3;

输出加操后的结果: In that basket, the amount of cabbage is 4 and the amount of radish is 6

对于其他的操作符的重载,要重载的相应命名方法的对应表如下,也是 Groovy 语法目前所支持的所有操作符:

OperatorNameMethodWorks with
a + bPlusa.plus(b)Number,string,collection
a - bMinusa.minus(b)Number,string,collection
a * bStara.multiply(b)Number,string,collection
a / bDividea.div(b)Number
a % bModuloa.mod(b)Integral number
a++Post incrementa.next()Number,string,range
++aPre increment
a--Post decrementa.previous()Number,string,range
--aPre decrement
a**bPowera.power(b)Number
a | bNumerical ora.or(b)Integral number
a & bNumerical anda.and(b)Integral number
a ^ bNumerical xora.xor(b)Integral number
~aBitwise complementa.negate()Integral number,string
(the latter returning a regular expression pattern)
a[b]Subscripta.getAt(b)Object,list,map,string、Array
a[b] = cSubscript assignmenta.puAt(b,c)Object,list,map,StringBuffer,
Array
a << bLeft shifta.leftShift(b)Integral number, also used like "append" to StringBuffers, Writers, Files, Sockets, Lists
a >> bRight shifta.rightShift(b)Integral number
a >>> bRight shift unsigneda.rightShiftUnsigned(b)Integral number
switch(a){
case b:
}
Classificationb.isCase(a)Object, range, list, collection, pattern, closure; also used with collection c in c.grep(b), which returns all items of c where b.isCase(item)
a == bEqualsa.equals(b)Object, consider hashCode()
a != bNot equals!a.equals(b)Object
a <=> bSpaceshipa.compareTo(b)java.lang.Comparable
a > bGreater thana.compareTo(b) > 0
a >= bGreater than or equal toa.compareTo(b) >= 0
a < bLess thana.compareTo(b) < 0
a <= bLess than or equal toa.compareTo(b) <= 0
a as typeEnforced coerciona.asType(typeClass)Any type

重载 equals() 方法时,Java 强烈建议重载 hashCode() 方法,对象相等时希望 hashcode 也是一样,尽管是非必要的,参看 java.lang.Object#equals 的 API 说明。

严格意义上讲,还有更多的符号可以重载,例如,引用字段和方法的点(.) 操作符就可以被重载了。通过对 Date 的 . 操作重载可以支持以下代码的写法(这要用到 MetaClass 来改造原有类),这要再来一篇细究的:

newDate = date + 1.month + 3.days + 5.hours; //IBM DB2 数据库就支持这种更人性化的写法

还应注意以下几个情况:

1) 在 Groovy 中的 a==b 比较操作是依照 equals() 方法来比较的。而在 Java 的 a==b 比较操作,到了 Groovy 中就要用 a.is(b)

2) 有些操作已经在许多的 Java 类或是 Groovy 数据结构中实现了,那么就可以直接使用,例如对两个 Double 类型可以用 ==> 等符号进行比较;
还有 Groovy 中的集合,如
1list = [1,2,3]
2println list[2]  //Groovy 为集合实现了的索引操作符[]

3) Groovy 在使用比较操作符时对 null 值处理很优雅。换句话说,如果一个(或者两个) 值为 null,就不会抛出 java.lang.NullPointerException

比如再给上面的 Basket 加上方法
1def equals(Basket anotherBasket){
2    return this.cabbageAmt==anotherBasket.cabbageAmt
3           && this.radishAmt==anotherBasket.radishAmt
4}

再执行以下代码:
1Basket basket1 = new Basket(cabbageAmt:1,radishAmt:2);
2Basket basket2 = null;
3
4println basket1 == basket2;

执行后的输出为 false,如果按照 Java 中的思维, basket1 ==basket2 会调用 Basket 的 equals() 方法,而 basket2 为 null,在执行 equals() 方法时就会抛出 java.lang.NullPointerException 异常,而实质上是 Groovy 在生成 equals() 方法时插入了 null 检查代码,所以不抛出异常。

4) 因为被重载的符号是映射到相应的方法,所以 basket1 + basket2 实际和 basket1.plus(basket2) 是一致的,即调用第一个操作数的 plus() 方法,第二个操作数为参数。这里前后两个操作数是同类型的,所以应用加号的交互法则,写成 basket2 + basket1 也成立,因为对 baksket2.plus(basket1) 调用是合法的。

不过,要是在 Basket 类中定义如下方法:(加上一个整数,我们假定给篮子加 1 的意义在于白菜和萝卜的数量各加上 1)
1def plus(int i){
2    return new Basket(cabbageAmt:this.cabbageAmt+i,
3        radishAmt:this.radishAmt+i);
4}

那么我们写下这样的代码

basket1 + 1;

OK,没问题,如果还想当然的可以互换,写成 1 + basket1; 那就有异常了,

groovy.lang.MissingMethodException: No signature of method: java.lang.Integer.plus() is applicable for argument types: (Basket)

因为这时候是通过 Integer 来调用 plus(bakset) 的,即 1.plus(basket1),而 Integer 是没有定义 plus(Basket basket) 这样一个方法,所以出错。如果两边都定义了相应的 plus() 方法,就可以把加数和被加数互换了。这个问题在其他有符号重载特性的语言中同样是存在的,只要留意了被重载的方法内部实际是如何被调用的就不会有问题了。
参考:1. 《Java 脚本编程语言、框架与模式》第 4 章
2. 《Groovy in Action》2.2 Overriding operators 永久链接 https://yanbin.blog/unmi-study-groovy-operator-overload/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。