Groovy语言学习

Groovy 和 Java 一样也是一门 JVM 语言,最终都会编译成 .class 文件然后运行在 JVM 上,Groovy 语言类似脚本,语法简单更灵活,所以在编写项目脚本构建上优势更加明显。
如何在Android Studio 运行Groovy程序请看我的文章 Android Studio 中如何运行 groovy 程序

1、一些前提知识

 1. Groovy注释标记和Java一样,支持// 或者 /**/。
 2. Groovy语句可以不用分号结尾。
 3. Groovy中支持动态类型,即定义变量的时候可以不指定其类型。
	Groovy中,变量定义可以使用关键字def。
	注意,虽然def不是必须的,但是为了代码清晰,建议还是使用def关键字
	def a = 1   // 可以不使用分号结尾
   	def b = "hello world"
   	def int x = 1  // 变量定义时,也可以直接指定类型
 4. 函数定义时,参数的类型也可以不指定。比如
	String testFunc(arg1, arg2){ // 无需指定参数类型
	  ...
	}
 5. 除了变量定义可以不指定类型外,Groovy中函数的返回值也可以是无类型的。比如
	def noReturnFunc() { // 无类型的函数定义,必须使用def关键字
    	last_line   // 最后一行代码的执行结果就是本函数的返回值
	}
 6. 如果指定了函数返回类型,则可不必加def关键字来定义函数。
	String getHelloWorld(){
  		return "hello world"
	}
 7. 函数返回值:Groovy的函数里,可以不使用return xxx来设置xxx为函数返回值。
	如果不使用return语句的话,则函数里最后一句代码的执行结果被设置成返回值。
	比如:下面这个函数的返回值是字符串"hello world"
	def getHelloWorld() {
     	"hello world" // 如果这是最后一行代码,则返回类型为String
      	1 // 如果这是最后一行代码,则返回类型为Integer
	}
	注意,如果函数定义时候指明了返回值类型的话,函数中则必须返回
	正确的数据类型,否则运行时报错。如果使用了动态类型的话,
	你就可以返回任何类型了。
 8. Groovy对字符串支持相当强大,充分吸收了一些脚本语言的优点:
	(1) 单引号''中的内容严格对应Java中的String,不对$符号进行转义。
   		def singleQuoteStr = 'this is $ dolloar'  
   		// 输出就是 this is $ dolloar
 	(2) 双引号""的内容则和脚本语言的处理有点像,如果字符中有$号的话,
		则它会$表达式先求值。
   		def doubleQuoteNoDollar = "this is one dollar" 
   		// 输出 this is one dollar
   		def x = 1
   		def doubleQuoteDollar = "this is $x dolloar" 
   		// 输出 this is 1 dolloar
	(3) 三个引号'''xxx'''中的字符串支持 随意 换行。比如
   		defmultieLines = ''' begin
    		line  1
    		line  2
     		end '''
 9. 除了每行代码不用加分号外,Groovy中函数调用的时候还可以不加括号。比如:
	println("test") ---> println "test"
	注意,虽然写代码的时候,对于函数调用可以不带括号,
	但是Groovy经常把属性和函数调用混淆。比如
	def getHelloWorld() {
     	"hello world" 
	}
	getHelloWorld() 
	// 如果不加括号的话,Groovy会误认为getHelloWorld是一个变量。
	所以,调用函数要不要带括号,如果这个函数是Groovy API或者Gradle API
	中比较常用的,比如println,就可以不带括号。
	否则还是带括号。Groovy自己也没有太好的办法解决这个问题。

2、Groovy的数据类型

Groovy中的数据类型主要介绍以下几种:

  1. 基本数据类型。
  2. 容器类。
  3. 闭包。

2.1 基本数据类型

作为动态语言,Groovy世界中的所有事物都是对象。所以,int,boolean这些Java中的基本数据类型,在Groovy代码中其实对应的是它们的包装数据类型。比如int对应为Integer,boolean对应为Boolean。

2.2 容器类

Groovy中的容器类很简单,就三种:

  1. List:链表,其底层对应Java中的List接口,一般用ArrayList作为真正的实现类。
  2. Map:键-值表,其底层对应Java中的LinkedHashMap。
  3. Range:范围,它其实是List的一种拓展。

下面了解一下它们的用法,下面是一些简单的例子:

  1. List类
1.变量定义:List变量由[]定义,比如
	def aList = [5,'string',true]  
	// List由[]定义,其元素可以是任何对象
2.变量存取:可以直接通过索引存取,而且不用担心索引越界。
	如果索引超过当前链表长度,List会自动往该索引添加元素
	assert aList[1] == 'string'
	assert aList[5] == null //第6个元素为空
	aList[100] = 100 //设置第101个元素的值为100
	assert aList[100] == 100
	那么,aList到现在为止有多少个元素呢?
	println aList.size  ===> 结果是101
  1. Map类
1.变量定义:Map变量由[:]定义,比如
	def aMap = ['key1':'value1','key2':true]
2.Map由[:]定义,注意其中的冒号。冒号左边是key,右边是Value。
	key必须是字符串,value可以是任何对象。
3.另外,key可以用''""包起来,也可以不用引号包起来。比如
	def aNewMap = [key1:"value",key2:true] 
	//其中的key1和key2默认被处理成字符串"key1"和"key2"
4.不过Key要是不使用引号包起来的话,也会带来一定混淆,比如
	def key1="wowo"
	def aConfusedMap=[key1:"who am i?"]
	aConfuseMap中的key1到底是"key1"还是变量key1的值“wowo”?
	显然,答案是字符串"key1"。
	如果要是"wowo"的话,则aConfusedMap的定义必须设置成:
	def aConfusedMap=[(key1):"who am i?"]
5.Map中元素的存取更加方便,它支持多种方法:
	println aMap.keyName //这种表达方法好像key就是aMap的一个成员变量一样
	println aMap['keyName'] //这种表达方法更传统一点
	aMap.anotherkey = "i am map" //为map添加新元素
  1. Range类
    Range是Groovy对List的一种拓展,变量定义和大体的使用方法如下:
1.变量定义: 由begin值+两个点+end值表示,比如
	def aRange = 1..5 //左边这个aRange包含1,2,3,4,5这5个值            
2.如果不想包含最后一个元素,则如下
	def aRangeWithoutEnd = 1..<5 //包含1,2,3,4这4个元素
	println aRange.from
	println aRange.to

2.3 闭包

2.3.1 闭包的样子
闭包,英文叫Closure,是Groovy中非常重要的一个数据类型或者说一种概念。
闭包,是一种数据类型,它代表了一段可执行的代码。其外形如下:

def aClosure = { //闭包是一段代码,所以需要用花括号括起来..
    String param1, int param2 ->  
    //这个箭头很关键。箭头前面是参数定义,箭头后面是代码
    println "this is code" //这是代码,最后一句是返回值,
   //也可以使用return,和Groovy中普通函数一样
}

简而言之,Closure的定义格式是:

def xxx = {paramters -> code} 
或者 
def xxx = {纯code} //这种case不需要->符号

无参数的闭包

def closure = {
    println("No Parameters")
}

一个参数的闭包

def closureOneParameters = {
    key -> println(key)
}

两个参数的闭包

def closure2Parameter = {
    key,value->
        if (key == 1) {
            key = key + 1
            println(key + ":" + value)
        } else if (key == 2)
            println(key + ":" + value)
}

从C/C++语言的角度看,闭包和函数指针很像。
闭包定义好后,要调用它的方法就是:闭包对象.call(参数) 或者 闭包对象(参数) 。后者更像函数指针调用的方法。
比如:

aClosure.call("this is string",100)  
或者
aClosure("this is string", 100)

在闭包中,还需要注意一点:如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫it,和this的作用类似,it代表闭包的参数。
比如:

1.	def greeting = { "Hello, $it!" }
	assert greeting('Patrick') == 'Hello, Patrick!'
	等同于:
	def greeting = { it -> "Hello, $it!"}
	assert greeting('Patrick') == 'Hello, Patrick!'
2.  但是,如果在闭包定义时,采用下面这种写法,则表示闭包没有参数!
	def noParamClosure = { -> true }
	这个时候,我们就不能给noParamClosure传参数了!
	noParamClosure ("test") //报错!

2.3.2 Closure使用中的注意点

  1. 省略圆括号
    闭包在Groovy中大量使用,比如很多类都定义了一些函数,这些函数最后一个参数都是一个闭包。比如:
public static <T> List<T>each(List<T> self, Closure closure)
上面这个函数表示针对List的每一个元素都会调用closure做一些处理。
这里的closure,就有点回调函数的感觉。

但是,在使用这个each函数的时候,我们传递一个怎样的Closure进去呢?比如:
def iamList = [1,2,3,4,5]  //定义一个List
iamList.each { 
	//调用它的each,这段代码的格式看不懂了吧?each是个函数,圆括号去哪了?
    println it
}

上面代码有两个知识点:
each函数调用的圆括号不见了!
原来,Groovy中,当函数的最后一个参数是闭包的话,可以省略圆括号。比如
def testClosure(int a1,String b1, Closure closure){
      // dosomething
     closure() //调用闭包
}
那么调用的时候,就可以免括号!
testClosure (4, "test", {
   println"i am in closure"
} )  //红色的括号可以不写..

注意,这个特点非常关键,因为以后在Gradle中经常会出现下面这样的代码:

task hello{
	doLast{
   		println'Hello world!'
	}
}

省略圆括号虽然使得代码简洁,看起来更像脚本语言,但是它这经常会让我困惑,以doLast为例,完整的代码应该按下面这种写法:

doLast({
	println'Hello world!'
})

有了圆括号,你会知道 doLast只是把一个Closure对象传了进去。很明显,它不代表这段脚本解析到doLast的时候就会调用println 'Hello world!' 。但是把圆括号去掉后,就感觉好像println 'Hello world!'立即就会被调用一样!

  1. 如何确定Closure的参数
    另外一个比较让人头疼的地方是,Closure的参数该怎么搞?还是刚才的each函数:
public static <T> List<T> each(List<T>self, Closure closure)

如何使用它呢?比如:

def iamList = [1,2,3,4,5]  //定义一个List变量
iamList.each{ //调用它的each函数,只要传入一个Closure就可以了。
  println it
}

Closure虽然很方便,但是它一定会和使用它的上下文有极强的关联。要不,作为类似回调这样的东西,我如何知道调用者传递什么参数给Closure呢?

Closure的使用有点坑,很大程度上依赖于你对API的熟悉程度,所以最初阶段,SDK查询是少不了的。
Groovy的API文档位于 http://www.groovy-lang.org/api.html

2.3.3 闭包的特性

闭包的引入让 Groovy 语言更加简单、方便,比如作为函数的最后一个参数,闭包可以单独写在函数,本小节中介绍一下闭包常见的使用形式。

闭包特性:

  • 闭包可以访问外部的变量,方法是不能访问外部变量的。
  • 闭包中可以包含代码逻辑,闭包中最后一行语句,表示该闭包的返回值,不论该语句是否冠名return关键字,如果最后一行语句没有不输入任何类型,闭包将返回null。
  • 闭包的参数声明写在‘->’符号前,调用闭包的的标准写法是:闭包名.call(闭包参数)。
  • 闭包的一些快捷写法,当闭包作为闭包或方法的最后一个参数。可以将闭包从参数圆括号中提取出来接在最后,如果闭包是唯一的一个参数,则闭包或方法参数所在的圆括号也可以省略。对于有多个闭包参数的,只要是在参数声明最后的,均可以按上述方式省略。

闭包作为函数参数:闭包作为函数参数时,跟普通的变量参数使用方式相同。

def checkKey = {
    map ->
        if (map.size() == 0) {
            println("Parametes is Null or Empty")
        }

        println(map)
}

def enqueue(key, value, closure) {
    def map = [:]
    map.put(key, value)
    closure(map)
}

enqueue(1, 2, checkKey)

通常情况下,在函数具有闭包作为参数的时候,会将闭包放在最后一个参数的位置,当闭包作为最后一个参数的时候,闭包可以抽离到函数体之外,提高函数的简洁性。

3 脚本类、文件I/O和XML操作

3.1 脚本类

1.脚本中import其他类

这部分内容若不太了解,可以参考我的博客 Android Studio 中如何运行 groovy 程序
Groovy中可以像Java那样写package,然后写类。比如在文件夹com/test/目录中放一个文件,叫Test.groovy,如下所示:
在这里插入图片描述
上面的Test.groovy和Java类就很相似了。当然,如果不声明public/private等访问权限的话,Groovy中类及其变量默认都是public的。
然后,我们在根目录下建立一个Practice.groovy文件。其代码如下所示:

def test = new Test("hello","hello world")
test.print()

在这里插入图片描述
在这里插入图片描述
如上图,提示需要导入Test类,导入后如下:
在这里插入图片描述
test.groovy中先importcom.cmbc.groovy.Test类,然后创建了一个Test类型的对象,接着调用它的print函数。

2.脚本到底是什么

Java中,我们最熟悉的是类。但是我们在Java的一个源码文件中,不能不写class(interface或者其他…),而Groovy可以像写脚本一样,把要做的事情都写在xxx.groovy中,而且可以通过groovy xxx.groovy直接执行这个脚本。

这到底是怎么实现的呢?
既然是基于Java的,Groovy会先把xxx.groovy中的内容转换成一个Java类。比如:test.groovy的代码是:

println 'Groovy world!'

Groovy这样把它转换成的Java类:执行 groovyc-d classes test.groovy
groovyc是groovy的编译命令,-dclasses用于将编译得到的class文件拷贝到classes文件夹下。

下面我们看看Practice.groovy转换得到的Practice.class,如下
在这里插入图片描述
由上图可知:

  • Practice.groovy被转换成了一个Practice类,它从script派生。
  • 每一个脚本都会生成一个static main函数。这样,当我们groovy Practice.groovy的时候,其实就是用java去执行这个main函数。
  • 脚本中的所有代码都会放到run函数中。比如,println ‘Groovy world’,这句代码实际上是包含在run函数里的。
  • 如果脚本中定义了函数,则函数会被定义在test类中。

3.脚本中的变量和作用域
xxx.groovy只要不是和Java那样的class,那么它就是一个脚本。而且脚本的代码其实都会被放到run函数中去执行。那么,在Groovy的脚本中,很重要的一点就是脚本中定义的变量和它的作用域。
Test.Groovy 和 Practice.groovy 生成的class文件对比:
在这里插入图片描述
在这里插入图片描述
举例:

def x = 1 //注意,这个x有def(或者指明类型,比如 int x = 1)
def printx(){
   println x
}

printx() 在android studio中什么也没打印。
修改后的Practice.groovy文件如下:
在这里插入图片描述
运行结果如下:
在这里插入图片描述
Practice.class如下:
在这里插入图片描述
在上图中:

printx被定义成test类的成员函数
def x = 1,这句话是在run中创建的。
所以,x=1从代码上看好像是在整个脚本中定义的,但实际上printx访问不了它。

printx是test成员函数,除非x也被定义成test的成员函数,
否则printx不能访问它。

那么,如何使得printx能访问x呢?很简单,定义的时候不要加类型和def。即:
x = 1 //注意,去掉def或者类型
def printx(){
   println x
}

修改代码如下:
在这里插入图片描述
运行结果如下:
在这里插入图片描述
Practice.class 变成如下所示:
在这里插入图片描述
由上图可知,x也没有被定义成test的成员函数,而是在run的执行过程中,将x作为一个属性添加到test实例对象中了,然后在printx中,先获取这个属性。

注意,Groovy的文档说 x = 1这种定义将使得x变成test的成员变量,但从反编译情况看,这是不对的。

虽然printx可以访问x变量了,但是假如在其他脚本调用test脚本的函数,那么就无法访问x变量了,因为它不是test的成员变量。

比如,我在测试目录下创建一个新的名为Practice1.groovy。这个Practice1将访问Practice.groovy中定义的printx函数:
在这里插入图片描述
这种方法使得我们可以将代码分成模块来编写,比如将公共的功能放到Practice.groovy中,然后使用公共功能的代码放到Practice1.groovy中。

执行Practice1.groovy,报错如下图: Practice class中找不到x,这是因为x是在Practice的run函数动态加进去的。
在这里插入图片描述
怎么解决呢?

import groovy.transform.Field;   //必须要先import
@Field x = 1  
//在x前面加上@Field标注,这样,x就彻彻底底是test的成员变量了。

在这里插入图片描述
编译后的Practice.class、Practice1.class文件如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个时候,Practice.groovy中的x就成了Practice类的成员函数了。如此,我们可以在script中定义那些需要输出给外部脚本或类使用的变量了!

运行Practice1.groovy,结果如下:在这里插入图片描述
3.2 文件I/O操作

Groovy的文件I/O操作,虽然比Java看起来简单,但要理解起来其实比较难。尤其是当你要自己查SDK并编写代码的时候。

整体说来,Groovy的I/O操作是在原有Java I/O操作上进行了更为简单方便的封装,并且使用Closure来简化代码编写。主要封装了如下一些了类:
文档网址:http://docs.groovy-lang.org/latest/html/groovy-jdk/
在这里插入图片描述
在这里插入图片描述
1.读文件
Groovy中,文件读操作简单到令人发指:

def targetFile = new File(文件名)  // File对象还是要创建的。

然后去官网看看Groovy定义的API:

  1. 读该文件中的每一行:eachLine的唯一参数是一个Closure。Closure的参数是文件每一行的内容。其内部实现肯定是Groovy打开这个文件,然后读取文件的一行,然后调用Closure…
 targetFile.eachLine{ 
   StringoneLine ->
    printlnoneLine    
} 

在这里插入图片描述

  1. 直接得到文件内容
targetFile.getBytes()  //文件内容一次性读出,返回类型为byte[]

在这里插入图片描述
根据Groovy的原则,如果一个类中有名为xxyyzz这样的属性(其实就是成员变量),Groovy会自动为它添加getXxyyzz和setXxyyzz两个函数,用于获取和设置xxyyzz属性值。
注意,get和set后第一个字母是大写的
所以,这里可以直接使用targetFile.bytes

  1. 使用InputStream.InputStream 的SDK 可到官网查看
def ism =  targetFile.newInputStream()
//操作 ism,最后记得关掉
ism.close
  1. 使用闭包操作inputStream,以后在Gradle里会常看到这种形式。
 targetFile.withInputStream{ ism ->
   操作ism. 
   不用close。Groovy会自动替你close
 }

在这里插入图片描述
2. 写文件
和读文件差不多,不再啰嗦。这里给个例子,介绍如何copy文件:

def srcFile = new File(源文件名)
def targetFile = new File(目标文件名)
targetFile.withOutputStream{ os->
  srcFile.withInputStream{ ins->
      os << ins 
      //利用OutputStream的<<操作符重载,完成从inputstream
      //到OutputStream的输出
   }
}

关于OutputStream的<<操作符重载,查看SDK文档后可知:
在这里插入图片描述
3.3 XML操作

除了I/O异常简单之外,Groovy中的XML操作也很极致。Groovy中,XML的解析提供了和XPath类似的方法,名为GPath,这是一个类,提供相应API。关于XPath,不了解的可到下面网址学习 https://www.w3school.com.cn/xpath/index.asp

下面看一个来自Groovy官方文档的GPath功能示例:

test.xml文件:
<response version-api="2.0">
       <value>
           <books>
               <book available="20" id="1">
                   <title>Don Xijote</title>
                   <author id="1">Manuel De Cervantes</author>
               </book>
               <book available="14" id="2">
                   <title>Catcher in the Rye</title>
                  <author id="2">JD Salinger</author>
              </book>
              <book available="13" id="3">
                  <title>Alice in Wonderland</title>
                  <author id="3">Lewis Carroll</author>
              </book>
              <book available="5" id="4">
                  <title>Don Xijote</title>
                  <author id="4">Manuel De Cervantes</author>
              </book>
           </books>
      </value>
</response>

下面看看怎么使用GPath:

///第一步,创建XmlSlurper类
def xparser = new XmlSlurper()
def targetFile = new File("test.xml")
//GPath
GPathResult gpathResult = xparser.parse(targetFile)
 
//现在访问test.xml中id=4的book元素,
//gpathResult代表根元素response。
//通过e1.e2.e3这种格式就能访问到各级子元素
def book4 = gpathResult.value.books.book[3]
//得到book4的author元素
def author = book4.author
//再来获取元素的textvalue
assert author.text() == ' Manuel De Cervantes '
//获取元素的属性更直观
author.@id == '4' 或者 author['@id'] == '4'
//属性一般是字符串,可通过toInteger转换成整数
author.@id.toInteger() == 4

GPath介绍完毕,再看个例子,我们在使用Gradle的时候有个需求,就是获取AndroidManifest.xml版本号(versionName)。使用GPath,一行代码就可以搞定,如下:

def androidManifest = new XmlSlurper().parse("AndroidManifest.xml")
println androidManifest['@android:versionName']
或者
println androidManifest.@'android:versionName'

4.总结
作为一门语言,Groovy是复杂的,是需要深入学习和钻研的。
从使用角度看,尤其是又限定在Gradle这个领域内,只需要了解Groovy中一些简单的知识即可。

发布了18 篇原创文章 · 获赞 1 · 访问量 773

猜你喜欢

转载自blog.csdn.net/aha_jasper/article/details/104810647
今日推荐