Groovy学习笔记二(基础篇)

Groovy语言基础

本篇将介绍groovy语言的基础知识,并介绍groovy中断言assert的使用。

1、代码外观

一门语言除了关键字和类名等之外,还有许多其他的符号,如:花括号、圆括号、注释、语句结束符等。针对语言的不同,这些符号也有不同的意义,本节将介绍groovy中的代码外观。

1.1、注释

groovy的代码注释和java非常类似,可以说是基本上一致,但是groovy本身是可以直接作为脚本来使用的,所以首先我们来介绍下java中没有的注释。

脚本注释
groovy中的脚本注释像shell脚本注释一样,语句是固定的,下面列举出shell和groovy的脚本注释。

#!/bin/bash 
# 这是shell脚本注释
#!/usr/bin/groovy
//这是groovy脚本注释

单行注释

//这是groovy的单行注释

多行注释

/**
 * 这是groovy的多行注释一
 * 这是groovy的多行注释二
 */

javadoc风格
类似于多行注释,但是这种病注释支持groovydoc用来生成文档,groovydoc和javadoc使用的语法是等价的。

2.2、groovy与java语法比较

因为groovy和java的语法是在太相似,所以经常给人一种groovy是java语法的超集。尽管两种语法是非常相似的,但是还是有一些不同的。

groovy中和java语法的相同部分

  • 相同的包处理机制(包的声明和import语句)
  • 类和方法的定义(嵌套类除外)
  • 控制结构语句(循环除外)
  • 操作符、表达式和赋值
  • 异常处理
  • 变量声明(部分不同)
  • 对象实例化,引用和取消引用对象,方法调用

groovy增加的部分

  • 通过新的表达式和操作符访问java对象
  • 多种途径声明对象
  • 提供新的控制接口来进行流程控制
  • 引入新的数据类型和响应的操作符与表达式
  • 一切皆对象

从上面两点可以看出,groovy可以当做是java和这些附加功能的和。

2.3、groovy的简洁性

groovy允许忽略一些在java中必须的语法元素,忽略这些元素的结果是使得代码更加简洁,表达更加清晰。

例子:将一个String转为URL对象

 //在java中的表达
 java.net.URLEncoder.encode("a b");
 //在groovy中的表达
 URLEncode.encode 'a b'

groovy用尽可能简洁的语句表述了我们的目标,忽略了包的前缀,圆括号和封号。

groovy中的圆括号是可选的,这是建立在消除棱模两可的情况和groovy语言规范中概述有限处理规则上的。但是有时候这样的代码不够直观,省略括号可能引起误解。

groovy自动导入java中如下的包和类

  • groovy.lang.*
  • groovy.util.*
  • java.lang.*
  • java.util.*
  • java.net.*
  • java.io.*
  • java.math.BigInteger
  • java.math.BigDecimal
  • 我们可以直接使用上述的类而不需要导包*

2、断言assert

断言对java开发者来说应该是不陌生的,一般使用断言来测试程序的结果是否正确,可以使用断言来贯穿整个应用程序从而保证程序是没有任何逻辑上的矛盾。groovy使用assert作为断言关键字。

下面提供一个简单的断言使用例子:

assert true
assert 1 == 1
def x = 1
assert x == 1
def y == 1;assert y == 1

在groovy中,一行有多个执行语句的时候可以使用封号隔开,最后一个执行语句可以不使用封号

使用断言的好处

  • 用来表示程序的状态
  • 经常用来替换行注释,因为注释可能没人注意,并且注释无法验证程序的正确性,断言表明改行程序一定是正确执行的。在真实的业务代码中就想一个个小的单元测试。

3、groovy预览

和很多开发语言一样,groovy也有一个语言规范来分类语句、表达式等,本节只是groovy的一个预览。

3.1、声明类

类是面向对象语言的基础。类用来定义一个对象的结构。

下面距离定义一个简单的类:

//Book.groovy
class Book{
    private String title
    Book(String title){
        this.title = title
    }
    String getTitle(){
        returen title
    }
}

从上述代码中可以看出,在声明类的时候没有指定类修饰符,在groovy中默认的类修饰符是public

3.2、使用脚本

groovy脚本是一个扩展名为.groovy的文本文件,这个文件能直接在命令行中执行。注意:在java中,我们必须要先编译.java代码为.class字节码文件,然后才能交给JVM来运行。但是在groovy中,脚本文件可以直接被运行,至少表面上是这样的(没有生成中间文件)。

下面使用一个例子来表示groovy脚本的使用,并且使用到了上面的Book.groovy类。

//myfile.groovy
Book gina = new Book('Groovy in Action')
assert gina.getTitle() == 'Groovy in Action'
assert getTitleBackwards(gina) == 'noitcA ni yvoorG'
println 'none error!!!'

String getTitleBackwards(book) {
  title = book.getTitle()
  return title.reverse()
}

下面为运行截图,注:Book.groovy和myfile.groovy为同一目录下的文件。

图2.1

从上面的例子中我们可以看出,在声明方法getTitleBackwards()之前调用了该方法,这也是groovy与其他脚本语言如Ruby不同的地方,一个groovy脚本被完整构建——也就是说,在执行之前脚本被置换、编译和产生类。另一点是我们在该脚本中使用了Book类,可以看出groovy语言联合了脚本和面向对象语言的优点。

3.3、GroovyBeans

JavaBeans是一个显露出属性的普通java类。
什么是属性?
属性不能单独存在,是一个命名概念,如果一个类暴露了名称结构的get/set方法,那么被暴露的变量可以描述成该类的一个属性。get/set方法叫做访问者方法。
GroovyBean即在groovy语言中的JavaBean,但是groovy在语法上会有一些区别。

使用途径

  • 自动生成访问者方法(无需单独声明get/set方法即可直接使用)
  • JavaBeans的简化访问方式
  • 事情处理器的简化注册
class Book {
String title //声明一个属性
}
def groovyBook = new Book()
//通过显示的方法调用来使用属性
groovyBook.setTitle('Groovy conquers the world')
assert groovyBook.getTitle() == 'Groovy conquers the world'
//通过groovy的快捷方式来使用属性
groovyBook.title = 'Groovy in Action'
assert groovyBook.title == 'Groovy in Action'

在上面的例子中,groovy没有声明get/set方法直接可以使用,同时,groovyBook.title不是直接对类属性的访问,而是对该类属性的访问者方法的简写形式。

3.4、处理文本

groovy在操作字符串方面比java更加强大,在groovy中,字符串大多数还是使用java.lang.String类型来处理,但是groovy提供了一些途径使得操作字符串更加方便。

GString

在groovy中,字符串能出现在单引号或者双引号中,双引号中允许使用占位符,并在需要的时候自动解析,这是GString类型。

def name = 'Groovy'
def book = 'Groovy in Action'
assert "$name in Action" == book

正则表达式

groovy使得正则表达式的使用更加容易,并且使用特殊的语法来声明一个正则表达式。

下面举例一些简单的正则表达式:

assert '12345' =~ /\d+/
assert 'xxxxx' == '12345'.replaceAll(/\d/,'x')

groovy中,使用/.../来表示一个正则表达式,并且使用=~来表示是否匹配该正则表达式

3.5一切皆对象

在groovy中,一切皆对象,虽然在学习java的时候也是这样表述自己面向对象的特点,但是java并不能说是一切皆对象,因为在java中还存在着基础数据类型。但是在groovy中,基础数据类型也是对象。

在java中,不允许在基础数据类型上调用方法,如果x是一个基础的数据类型int,则不能写x.toString()这样的代码,如果y是一个对象,则不能写y*2这样的代码。

但是在groovy中,这两种情况都是允许的。

def x = 1
def y = 2
assert x + y == 3
assert x.plus(y) == 3
assert x instanceOf Integer

3.6使用lists/maps/ranges

在很多开发语言中都有容器类型——数组,如在java中专门有数组类型[],但是也提供了一些容器类型来被使用。Groovy简化了容器的处理,并且从某种意义上去掉了数组类型,增加了操作符的支持,并且在java的类库上补充了更多的方法。

Lists——数组

Java通过方括号和下标来表示和索引数组类型,这种方式我们称为下标操作符,groovy采用了同样的语法来支持List——java.utils.List的实例(注意:这是List对象,并不是数组类型)java.utils.List允许向列表中增加或者删除对象,允许在运行时改变列表的大小,保存在列表中的对象不受类型的限制!!!;另外,groovy可以通过超出列表范围的数组来索引列表(即下标超出List长度的时候没有ArrayIndexOutOfBoundsException异常抛出),再一次表明可以改变列表的大小,此外,列表也可以直接在代码中指定。

下面举一个例子:

//罗马数字列表
def roman = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII']
//访问列表
assert roman[4] == 'IV'
//扩张列表
roman[8] = 'VIII'
assert roman.size() == 9

这是groovy中的一段代码,看上去和java中使用数组没什么区别,但是注意,在groovy中的数组类型是java.utils.List类,groovy对其语法进行了扩充使得List的使用更加简单。并且在下标越界的时候可以对其赋值并且动态改变了数组的大小。

Maps——映射

一个map是用来给一个键分配值的强类型,map中可以通过key找回值,而list通过位置找回值。
和java不同的是,groovy在语言级别支持map,允许使用特定的操作符来操作map,这是一个清晰和简单的语法,maps的操作语法像键值对数组,通过冒号分隔键和值一起被获取。

使用map的例子:

def http = [
    100 : 'CONTINUE',
    200 : 'OK',
    400 : 'BAD REQUEST'
]
assert http[200] == 'OK'
http[500] = 'INTERNAL SERVER ERROR'
assert http.size() == 4

从上面的例子中可以看出和java中使用map的区别,groovy不管是在操作上还是在表达上都比java要简单很多,而且在声明赋值的时候更加清晰。

ranges——范围

在java中并没有ranges相关的类,但是基本上都可以理解这个概念。

def x = 1..10
assert x.contains(5)
assert x.contains(15) == false
assert x.size() == 10
assert x.from == 1
assert x.to == 10
assert x.reverse() == 10..1

上面的例子很直观,所以不用再表达什么。

3.7、结构控制

结构控制是让一个程序语言控制代码执行的流程,groovy中if-elsewhileswithtry-catch-finally与java都是一样的。
在条件中,null被当做false处理,not-null被处理成true,for循环也有特殊的表达for(i in x){}的方式,x可以是任何对象,groovy内部知道如何迭代,如:Iterator/enumeration/collection/range/map/中的任何对象。

//在一行的if语句
if (false) assert false
//null表示false
if (null){
    assert false
}else{
    assert true
}
//典型的while
def i = 0
while (i < 10) {
    i++
}
assert i == 10
//迭代一个range
def clinks = 0
for (remainingGuests in 0..9) {
    clinks += remainingGuests
}
assert clinks == (10*9)/2
//迭代一个列表
def list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for (j in list) {
    assert j == list[j]
}
//以闭包为参数的each方法
list.each() { item ->
    assert item == list[item]
}
//典型的switch
switch(3) {
    case 1 : assert false; break
    case 3 : assert true; break
    default: assert false
}

关于闭包的概念将在后续文章中单独说明,本篇不做介绍。

4、在java环境中运行groovy

前面的文章中说过groovy是可以和java无缝集成的,本节介绍如何在java环境中运行groovy程序。

4.1、本是同根生

前面使用groovyc运行程序后生成了class字节码文件,而使用groovy并没有中间文件的生成,在之前也解释过这种现象,其实在使用groovy命令的时候也生成了字节码文件,只是存储在内存中而已,最后不管是使用groovyc还是groovy命令运行groovy代码的时候都是先编译生成字节码文件再交给JVM来进行处理,只不过一个保存在磁盘上一个保存在内存中。

在java中运行groovy的方式

  • 使用groovyc编译所有的.groovy文件为.class文件,把这些文件放在java类路径中,通过java类加载器进行加载。
  • 通过groovy的类加载器在运行时直接加载.groovy文件并且生成对象,在这种方式下,没有生成任何的字节码文件,但是生成了java.lang.Class对象的实例,可以通过groovy的类加载器进行加载并使用。

4.2、GDK:groovy类库

groovy和java紧密联系,在groovy中的所有对象都是java中的一个对象实例,在运行时他们指向同一个对象。

如:可以在使用String的时候使用startWith()方法。

Groovy的类库是JDK类库的扩展,groovy类库提供了一些新的类,同时,也为已经存在的java类增加了功能,这些附加的功能就是GDK,为java类在兼容性、功能性和可表达性方面提供了重要的线索。

4.3、groovy是动态的

groovy是一门动态语言(可以在运行时修改类的能力),如:增加新的方法,但是,groovy首先会生成字节码文件,而在JVM加载字节码的时候字节码文件不能被改变,这样如何动态修改呢?

Groovy生成的字节码文件不同于java生成的字节码文件
首先,二者生成的字节码在格式上并没有什么不同,但是内容肯定是不同的,groovy生成的字节码文件通过反射进行一系列操作。

例子:

//ClassJava.java
public class ClassJava{
  public void foo(){
    System.out.println("foo method");
  }
  public static void main(String[]args){
   ClassJava java=new ClassJava();
   java.foo();
  }
}
//ClassJava.class
public class ClassJava {
    public ClassJava() {
    }
    public void foo() {
        System.out.println("foo method");
    }

    public static void main(String[] var0) {
        ClassJava var1 = new ClassJava();
        var1.foo();
    }
}
//ClassGroovy.groovy
class ClassGroovy{
  private void foo(){
   println 'foo method'
  }

  public static void main(String[] args){
    def groovy = new ClassGroovy()
    groovy.foo()
  }
}
//ClassGroovy.class
import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import org.codehaus.groovy.runtime.callsite.CallSite;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;

public class ClassGroovy implements GroovyObject {
    public ClassGroovy() {
        CallSite[] var1 = $getCallSiteArray();
        MetaClass var2 = this.$getStaticMetaClass();
        this.metaClass = var2;
    }

    private void foo() {
        CallSite[] var1 = $getCallSiteArray();
        var1[0].callCurrent(this, "foo method");
    }

    public static void main(String... args) {
        CallSite[] var1 = $getCallSiteArray();
        Object groovy = var1[1].callConstructor($get$$class$ClassGroovy());
        var1[2].call(groovy);
    }

    static {
        __$swapInit();
        Long var0 = (Long)DefaultTypeTransformation.box(0L);
        __timeStamp__239_neverHappen1509957572839 = var0.longValue();
        Long var1 = (Long)DefaultTypeTransformation.box(1509957572839L);
        __timeStamp = var1.longValue();
    }
}

上面的例子为初始化一个类并调用其中一个方法的初始代码和编译后的字节码分别在java和groovy中的表达。可以看出生成的字节码文件明显的不同。
在笔者阅读《Gradle in action》的时候,作者提到生成的字节码文件内部是使用反射对方法进行调用的,这就意味着可以在获取到MethodClass对象之后对其进行拦截操作,从而实现类运行时的动态变化,但是笔者在此处验证的时候,生成的字节码文件很多实现系列又重新被封装了,不知道是不是groovy版本的问题,不过,本系列groovy文章只是为了更好的学习gradle而学习的,不用了解太深入。

总结

本篇先是介绍了groovy中的一些基础,然后对比了groovy和java语法上的异同,接着对groovy如何在java平台上运行进行了介绍并对比了生成的字节码文件,解释了groovy是如何作为动态语言的。

本系列文参均为书籍Gradle in action的读书笔记。
本文中的部分实例及图片均来自于该书籍。
在本文Ubuntu环境下开发的。