@月黑风高食肉虎 噗噗虎的技术博客

春节假期学习簿之Groovy学习笔记之语法浅析


这个春节过得很充实,因为学习了Groovy。“了”字用得不对,因为还在学习中。趁翔还是热得,稍做些总结。古人有云,学而时习之,不亦乐乎嘛。

Groovy与Java

如果你想要学好Groovy,你首先得是一位有经验的Java程序员。Groovy是Java程序员的出路(之一)。单独来看的话,太小众。

Groovy比Java更动态,更偏向于函数化。它支持不可变对象和闭包,但同时又没有抛弃指令式编程。它不强制你使用何种风格,所以选择权在你手上。因此,个人觉得,它更像Java程序员的出路,而不是一门独立的函数化编程语言。

动态类型与多路分发

与Java不同,Groovy是动态的。说它是动态的,是因为它是运行期多路分发/多宗绑定/多分派的(Multiple Dispatch/Multimethods 下文称多宗绑定)。

所谓多宗绑定既:注1

  • 方法的接收者与方法的参数统称为宗量。
  • 根据绑定基于多少种宗量,将绑定划分为单宗绑定和多宗绑定两种。
  • 多宗绑定既基于多种宗量对目标方法进行选择。
  • Java是,编译期多分派,运行期单宗绑定的。

我知道你看不懂,来来来,我们来看几段代码:

def var1 = 'Some String'

等同于Java中的

Object var1 = "Some String"

但是,如果你在Java中

class Test1 {

  public void someMethod(String value) {
    // process with String value...
    System.out.println("someMethod with string param");
  }
  public void someMethod(Object value) {
    // process with Object value ...
    System.out.println("someMethod with object param");
  }

  public static void main(String[] args) {
    Object var1 = "Hello World!";
    new Test1().someMethod(var1);

    String var2 = "Hello World2!";
    new Test1().someMethod(var2);
  }
}

资深Java程序员会这样告诉你

someMethod with object param
someMethod with string param

但如果你在Groovy中做同样的事情的话

def someMethod(String value) {
  // process ...
  println("someMethod with string param")
}

def someMethod(Object value) {
  // process ...
  println("someMethod with object param")
}

def var1 = "Hello World"
someMethod(var1)

String var2 = "hello world2"
someMethod(var2)

你会得到这样的结果

someMethod with string param
someMethod with string param

我知道你在想Groovy是怎么实现的,让我来告诉你。

首先,Groovy有类型猜测系统(Type inference),编译期会根据类型猜测系统的提示进行方法选择。 其次,所有Groovy的对象都有元类信息(metaClass),运行期通过把所有方法的调用分发至MOP(Meta Object Protocol,元对象协议)上来进行动态地方法选择。简单来说,有点像运行期做了个AOP,然后反射去拿各种对象信息,再去调用正确的方法。(不负责非正确比喻,请无视。当然Groovy肯定不是用AOP和反射来实现的,并且它的做法比反射理论上来说更高效)

为什么Groovy能做到这些?因为它和Java是异源同宗的,虽然两者最终编译结果都是能够在JVM上运行的class字节码,但是groovy并非先编译成Java再编译成class字节码的,所以它可以绕过Java的编译器,做一些Java做不到但是JVM能做到的事情。

譬如说像下面这种会让Java程序员抓狂的代码

def something = new Object()
something.metaClass.helloWorld = { println "Hello World from an Object!" }
something.helloWorld()

输出

Hello World from an Object!

你看!一个方法凭空就加到Object对象上惹!

函数化编程

作为一门现代化编程语言,不支持函数化编程是不行的。Groovy支持函数化编程的两大特性:

  1. 方法(闭包)可以作为头等公民来传递。
  2. 支持不可变对象(immutable)和等值(equality)而非等同(identity)比较

可传递的方法(闭包)

我们还是来看代码吧

def someClosure = { println "This is a closure and the parameter is $it" } // 闭包,只有一个参数默认it,字符串中$it将会被替换成它的值。
def callClosureMethod(Closure callback, int it) { // 这个方法调用闭包
  callback(it) // 还可以使用callback.call(it)来调用
}
callClosureMethod(someClosure, 47)

这还不是最赞的地方,还有更赞的

// 停留3秒钟后打印bye!
Thread.start { sleep 3000; println "bye!" } // 还记得Java中的Thread吗?这个就是它,后面的花括号是闭包。

你看这个闭包没有继承Runnable也没有实现run方法!如果将这样的闭包用在像addListener这样的方法上将会有多爽快!是的,你的确可以这样用!

当然,除了闭包,方法也可以传递,但是由于Java的限制,你得用&创建一个方法闭包:

def tellMe(int v) {
  println "The value of v is $v"
}

def runClosure(def c) {
  c(47)
}

def methodClosure = this.&tellMe
runClosure(methodClosure)

说到闭包就不得不谈到闭包的作用域scope,在所有函数式编程语言总,这是一个很高级的话题,一旦谈到这个问题,不外乎难倒面试/教做人/欺负新人/装逼的。为去歧义,明确说一下,这里的“闭包的作用域”指的不是闭包本身的作用域,而是指闭包代码块内各个变量的作用域。

在Groovy中,闭包的作用域是可以通过更改delegate来改变的。闭包代码块内,所有变量的引用,都是通过变量前加前缀delegate.来解析的,而delegate本身所指向的对象是可以改变的,默认指向的是闭包的所有者(owner),即定义闭包的对象。 并且,“变量前加前缀delegate.”这个操作,即闭包内变量的解析,也是可以通过为闭包设置resolveStrategy属性来改变的。可用的resolveStrategyOWNER_ONLY, OWNER_FIRST(默认), DELEGATE_ONLY, DELEGATE_FIRST, SELF_ONLY。这样Groovy闭包内作用域就变得非常灵活可变了。

不可变对象和等值比较

Groovy语法本身没有对这个特性提供特别的支持,它是通过提供注解和覆盖Java的比较方法来实现的。

考虑下面这段Java代码

// java bean
class Player {
  private String name;
  public Player() { } // 空构造器
  public Player(String name) { this.name = name; }
  public String getName() { return this.name; } // 只读属性
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof Player)) {
			return false;
		}
		Player other = (Player) obj;
		if (name == null) {
			if (other.name != null) {
				return false;
			}
		}
		else if (!name.equals(other.name)) {
			return false;
		}
		return true;
	}
  @Override
	public int hashCode() {
    // 老程序员会告诉你,覆盖了equals也必须覆盖hashCode……
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}
}
public class PlayGame {
  public static void main(String[] args) {
    Player player1 = new Player("player1");
    Player player2 = new Player("player2");
    Player anotherPlayer1 = new Player("player1");

    System.out.println(player1 == player2);
    System.out.println(player1 == anotherPlayer1);
    System.out.println(player1.equals(player2));
    System.out.println(player1.equals(anotherPlayer1));
  }
}

结果如下

false
false
false
true

Java中双等比较的是两个变量是否是同一个引用,而想要做值比较的话,就得用equals,如果是自己写得bean的话,就得像上面这样写许多代码。

再考虑下面这段java代码

int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
List<Integer> listA = new ArrayList(Arrays.asList(a));
List<Integer> listB = new ArrayList(Arrays.asList(b));

System.out.println(a == b);
System.out.println(listA == listB);
System.out.println(a.equals(b));
System.out.println(listA.equals(listB));

结果如下

false
false
false
true

像上面这样的代码,一般是用来为难面试的和实习生同学的,因为这样的不一致导致很多刚入门的同学头昏眼花的。 Groovy中的比较就很直接了

def a = [1, 2, 3]
def b = [1, 2, 3]
println a == b
println a.equals(b)
println a.is(b) // 等同于java的==

结果如下

true
true
false

回到前面一个例子中来,在Groovy中,想要自己写一个bean并且做等值比较,那是相当的爽快

@groovy.transform.Immutable
class Player {
  String name
}

def player1 = new Player(name: "player1")
def player2 = new Player(name: "player2")
def player3 = new Player(name: "player1")

println player1 == player2
println player1 == player3
println player1.equals(player2)
println player1.equals(player3)

结果如下

false
true
false
true

重点就在于@groovy.transform.Immutable这个注解上,你可以尝试一下删除这个注解,然后看看运行的结果会有什么变化。 好奇的同学肯定会问这是怎么实现的?答案就是AST Transformation(语义树变换)。眼熟的同学肯定会说,我看到了lombok

是的,神奇之处就在于此,其实Java也有同样的能力(lombok)。Groovy自带了很多AST变换的功能,不仅如此,还提供了可以一些附加工具,让程序员可以很方便地访问AST节点来做编译期检查甚至改变语法树的强大功能。

这些都是很高级的功能了,就不在此展开了。(其实是我还没掌握)

总结

这里挑选了Groovy的几个特性做了简介,但这只是强大Groovy的一部分功能,而且都是很抽象的理论上的特性,下次再记录一些比较实用的功能会比较好一点。

注1: 参考《深入理解Java虚拟机-JVM高级特性与最佳实践》,周志明著,2011年9月版,8.3.2.分派,209页。