Groovy-一种快速开发的 JVM 语言

Groovy-一种快速开发的 JVM 语言

向您展示最佳实践和惯用解决方案,帮助您充分利用Groovy语言的力量来做解决方案。

1 关于 Groovy

Groovy是JavaTM虚拟机(JVM)的动态语言。它具有完整的对象导向、可脚本性、可选类型、运算符自定义、最常见数据类型的声明、闭包和范围等高级概念、紧凑的属性语法和无缝JavaTM集成。此参考卡提供了您在编程Groovy时可能查找的信息类型。

2 开始使用 Groovy

http://groovy.codehaus.org安装Groovy,您将获得以下命令:

命令 说明
groovy 执行Groovy代码
groovyc 编译Groovy代码
groovysh 打开 Groovy shell
groovyConsole
java2groovy 迁移助手

Groovy命令附带-h和--help选项,以显示所有选项和所需的参数。典型的用法是:

执行文件MyScript.groovy

groovy MyScript

在命令行上计算(e)

groovy -e "print 12.5*Math.PI"

为每行输入打印(p)

echo 12.5  groovy -pe|"line.toDouble() * Math.PI"

内联编辑 (i) 文件 data.txt 通过反转每一行并保存备份

groovy -i.bak -pe"line.reverse()" data.txt

3 Groovy/Java集成

用Groovy,您可以像从Java一样调用任何Java代码。它是一样的。

用Java,您可以通过以下方式调用Groovy代码。请注意,您需要在class路径中放置groovy-all.jar。

交叉编译

使用groovyc、<groovyc>ant任务或IDE集成将您的groovy代码与Java代码一起编译。这使您能够像使用Java编写的Groovy代码一样使用。</groovyc>

评估

使用类groovy.util.Eval来评估Java字符串中捕获的简单代码:(int) Eval.xyz(1,2,3,"x+y+z");

GroovyShell

使用 groovy.util.GroovyShell 在 Binding 和可选的预解析中获得更大的灵活性:

GroovyShell shell= new GroovyShell();Script scpt = shell.parse("y = x*x");Binding binding = new Binding();scpt.setBinding(binding);binding.setVariable("x", 2);scpt.run();(int) binding.getVariable("y");
集成选项 特性/特性
Eval/GroovyShell 用于小表达式 + 重新加载,安全性
GroovyScriptEngine 用于依赖脚本 + 重新加载 - 类、安全性
GroovyClassLoader 包罗万象的解决方案 + 重新加载,安全性
Spring Beans 与 Spring + reloading 集成
JSR-223 语言切换简单,但 API 受限 - 重新加载,安全性 需要 Java 6

4 语言元素

类和脚本

Groovy类声明看起来像Java。默认可见性修饰符是公开的

class MyClass {  void myMethod(String argument) {  }}

当 .groovy 文件或任何其他 Groovy 代码源包含未包含在类声明中的代码时,则此代码被视为脚本,例如

println "Hello World"

脚本与类的不同之处在于它们有一个 Binding 作为未声明引用的容器(在类中是不允许的)。

println text // expected in Bindingresult = 1 // is put into Binding

可选类型

静态类型可以像在 Java 中一样使用,并且在运行时会被遵守。通过用 def 关键字替换类型声明来使用动态类型。方法和闭包声明的形式参数甚至可以省略 def。

特性

无论使用什么类型,属性都被声明为具有默认可见性修饰符的字段。

class MyClass {  String stringProp  def dynamicProp}

Java 风格的 getter 和 setter 会自动编译到字节码中。

属性被称为

println obj.stringProp // getterobj.dynamicProp = 1 // setter

无论 obj 是用 Java 还是 Groovy 编写的,都会调用相应的 getter/setter。

Multimethods

方法由运行时类型调度,允许代码如

class Pers {  String name  boolean equals(Pers other) {  name == other.name  }}assert new Pers(name:'x') == new Pers(name:'x')assert new Pers(name:'x') != 1

5 运算符

可定制的运算符

可以通过实现/覆盖相应方法来自定义运算符。

运算符号 方法
a + b a.plus(b)
a - b a.minus(b)
a * b a.multiply(b)
a / b a.div(b)
a % b a.mod(b)
a++ ++a a.next()
a-- --a a.previous()
a**b a.power(b)
a b a.or(b)
a&b a.and(b)
a^b a.xor(b)
~a ~a a.bitwiseNegate() // sometimes referred to as negate +a a.positive() // sometimes referred to as unaryMinus -a a.negative() // sometimes referred to as unaryPlus
a[b] a.getAt(b)
a[b] = c a.putAt(b, c)
a << b a.leftShift(b)
a >> b a.rightShift(b)
a >>> b a.rightShiftUnsigned(b)
switch(a){ case b: } [a].grep(b) if(a in b) b.isCase(a) // b is a classifier
a == b a.equals(b)
a != b ! a.equals(b)
a <=> b a.compareTo(b)
a > b a.compareTo(b) > 0
a >= b a.compareTo(b) >= 0
a < b a.compareTo(b) < 0
a <= b a.compareTo(b) <= 0
a as B a.asType(B)

积极寻找机会在您自己的Groovy类中实现运算符方法。这通常会导致更具表现力的代码。典型的候选者是==、<=>、+、-、<<和isCase()。参见范围。

特殊运算符

运算符 含义 NAME
a ? b : c if (a) b else c 三元,如果
a ?: b a ? a : b Elvis
a?.b a==null ? a : a.b null safe
a(*list) a(list[0], list[1], ...) spread
list*.a() [list[0].a(), list[1].a() ...] spread-dot
a.&b 将对象a中方法b作为闭包的引用 method closure
a.@field 直接访问属性 dot-at

6 简单的数据类型

数字类型

所有Groovy数字都是对象,而不是原始类型。字面声明是:

TYPE EXAMPLE LITERALS
java.lang.Integer 15, 0x1234ffff
java.lang.Long 100L, 100l
java.lang.Float 1.23f, 4.56F
java.lang.Double 1.23d, 4.56D
java.math.BigInteger 123g, 456G
java.math.BigDecimal 1.23, 4.56, 1.4E4, 2.8e4, 1.23g, 1.23G

数学运算的强制规则的一些例子是:

表达式 结果类型
1f * 2f Double
1f / 2f Double
(Byte)1 + (Byte)2 Integer
1 * 2L Long
1 / 2 BigDecimal (0.5)
(int)(1/2) Integer (0)
1.intdiv(2) Integer (0)
Integer.MAX_VALUE+1 Integer
2**31 Integer
2**33 Long
2**3.5 Double
2G + 1G BigInteger
2.5G + 1G BigDecimal
1.5G == 1.5F Boolean (true)
1.1G == 1.1F 1.1G == 1.1F

字符串类型

'literal String''''literalmultiline String'''def lang = 'Groovy'"GString for $lang""$lang has ${lang.size()} chars""""multiline GString withlate eval at ${-> new Date()}"""

GStrings 中的占位符在声明时被取消引用,但它们的文本表示在 GString -pString 转换时被查询。

/String with unescaped \ included/

正则表达式

正则表达式查找运算符 =~

正则表达式匹配运算符 ==~

正则表达式模式运算符 ~String

Examples:

def twister = 'she sells sea shells'
// 包含关键字 'she'assert twister =~ 'she'// 以 'she' 开头 且 以 'shells' 结尾assert twister ==~ /she.*shells/// 预编译def pattern = ~/she.*shells/assert pattern.matcher(twister).matches()// 匹配是可迭代的 以 'sh' 开头的单词def shwords = (twister =~ /\bsh\w*/).collect{it}.join(' ')assert shwords == 'she shells'// 通过逻辑替换assert twister.replaceAll(/\w+/){  it.size()} == '3 5 3 6'// 正则表达式组到闭包参数 查找开头和结尾相同的单词def matcher = (twister =~ /(\w)(\w+)\1/)matcher.each { full, first, rest ->  assert full in ['sells','shells']  assert first == 's'}   
符号 含义
. 任何字符
^ 行首(或文档开始,在单行模式下)
$ 行尾(或文档结尾,在单行模式下)
\d 数字字符
\D 除数字外的任何字符
\s 空白字符
\S 除空格外的任何字符
\w 单词字符
\W 除单词字符外的任何字符
\b 词界
() 分组
(x y) x 或 y 如 (Groovy
\1 反向匹配到第一组,例如用 (.)\1 查找双倍字符
x* x 出现零次或多次。
x+ x 出现一次或多次。
x? x 出现 0 次或 1 次。
x{m,n} x 至少出现“m”次,最多出现“n”次。
x{m} x 恰好出现“m”次。
[a-f] 包含字符'a'、'b'、'c'、'd'、'e'、'f'的字符类
[^a] 包含除“a”之外的任何字符的字符类
(?is:x) 评估 x 时切换模式;i 打开 ignoreCase, s 单行模式
(?=regex) positive lookahead
(?<=text) positive lookbehind

7 集合类型

范围

范围包括0..10或半完全像0.。<10。它们通常被括在括号中,因为范围操作符的优先级较低。

assert (0..10).contains(5)assert (0.0..10.0).containsWithinBounds(3.5)for (item in 0..10) { println item }for (item in 10..0) { println item }(0..<10).each { println it }

整数范围通常用于选择子列表。范围边界可以是定义pret()、next()并实现Comparable的任何类型。值得注意的例子是字符串和日期。

列表

列表看起来像数组,但类型为java.util.List以及新方法。

[1,2,3,4] == (1..4)[1,2,3] + [1] == [1,2,3,1][1,2,3] << 1 == [1,2,3,1][1,2,3,1] - [1] == [2,3][1,2,3] * 2 == [1,2,3,1,2,3][1,[2,3]].flatten() == [1,2,3][1,2,3].reverse() == [3,2,1][1,2,3].disjoint([4,5,6]) == true[1,2,3].intersect([4,3,1]) == [3,1][1,2,3].collect{ it+3 } == [4,5,6][1,2,3,1].unique().size() == [1,2,3,1].count(1) == [1,2,3,4].min() == [1,2,3,4].max() == [1,2,3,4].sum() == [4,2,1,3].sort() == [1,2,3,4][4,2,1,3].findAll{it%2 == 0} == [4,2]def anims=['cat','kangaroo','koala']anims[2] == 'koala'def kanims = anims[1..2]anims.findAll{it =~ /k.*/} ==kanimsanims.find{ it =~ /k.*/} ==kanims[0]anims.grep(~/k.*/) ==kanims

经常使用sort()方法,有三种方式:

Sort 调用 使用
col.sort() 可比对象的自然排序
col.sort { it.propname } 在比较结果之前,对每个项目应用闭包
col.sort { a,b -> a <=> b } 闭包为每次比较定义了一个比较器

列表也可以用负索引和反向范围进行索引。

def list = [0,1,2]assert list[-1] == 2assert list[-1..0] == list.reverse()assert list == [list.head()] + list.tail()

子列表分配可以使列表增长或缩小,列表可以包含不同的数据类型。

list[1..2] = ['x','y','z']assert list == [0,'x','y','z']

Maps

Maps就像具有任意类型键而不是整数的列表。因此,语法非常对齐。

def map = [a:0, b:1]

Maps可以通过传统的方括号语法访问,或者好像key是Maps的属性。

assert map['a'] == 0assert map.b == 1map['a'] = 'x'map.b = 'y'assert map == [a:'x', b:'y']

还有一个显式的 get 方法可以选择采用默认值。

assert map.c == nullassert map.get('c',2) == 2assert map.c == 2

Map迭代方法考虑了Map.Entry对象的性质。

map.each { entry ->  println entry.key  println entry.value}map.each { key, value ->  println "$key $value"}for (entry in map) {  println "$entry.key $entry.value"}

GPath

调用列表中的属性返回列表中每个项目的属性列表。

employees.address.town

返回town对象列表。

要对方法调用执行同样操作,请使用扩展点运算符。

employees*.bonus(2008)
calls the bonus method on each employee and stores theresult in a list.

闭包

闭包捕获一段逻辑和封闭范围。它们是一流的对象,可以接收消息,可以从方法调用返回,存储在字段中,并用作方法调用的参数。

在方法参数中使用

def forEach(int i, Closure yield){  for (x in 1..i) yield(x)}

用作最后一个方法参数

forEach(3) { num -> println num }

构造并分配给局部变量

def squareIt = { println it * it}forEach(3, squareIt)

将最左边的闭包参数绑定到固定参数

def multIt = {x, y -> println x * y}forEach 3, multIt.curry(2)forEach 3, multIt.curry('-')

闭包参数列表示例:

Closure.isCase(b)将b发送到闭包,并将调用结果返回为布尔值。例如

switch ('xy'){case {it.startsWith('x')} :...}[0,1,2].grep { it%2 == 0 }

8 GDK

java.lang.Object的方法

获取对象信息

println obj.dump()

或在 GUI 中

import groovy.inspect.swingui.*ObjectBrowser.inspect(obj)

打印obj的属性、方法和字段

println obj.propertiesprintln obj.class.methods.nameprintln obj.class.fields.name

动态调用方法的两种方式

obj.invokeMethod(name, paramsAry)obj."$name"(params)

其他方法

is(other) // identity checkisCase(candidate) //default:equalityobj.identity {...}; obj.with {...}print(); print(value),println(); println(value)printf(formatStr, value)printf(formatStr, value[])sleep(millis)sleep(millis) { onInterrupt }use(categoryClass) { ... }use(categoryClassList) { ... }

每个对象在 Groovy 中都是可迭代的,即使它是用 Java 实现的。您不仅可以在循环中使用任何 obj,例如

for (element in obj) { ... }

但您也可以应用以下迭代对象方法:

返回类型 PURPOSE
Boolean any {...}
List collect {...}
Collection collect(Collection collection) {...}
(void) each {...}
(void) eachWithIndex {item, index-> ...}
Boolean every {...}
Object find {...}
List findAll {...}
Integer findIndexOf {...}
Integer findIndexOf(startIndex) {...}
Integer findLastIndexOf {...}
Integer findLastIndexOf(startIndex) {...}
List findIndexValues {...}
List findIndexValues(startIndex) {...}
Object inject(startValue) {temp, item -> ...}
List grep(Object classifier) // uses classifier.isCase(item)

实现返回 Iterator 对象的 iterator() 方法,以使用上述方法为您自己的 Groovy 类提供有意义的可迭代行为。

文件和 I/0

常用的文件系统方法

def dir = new File('somedir')def cl = {File f -> println f}dir.eachDir cldir.eachFile cldir.eachDirRecurse cldir.eachFileRecurse cldir.eachDirMatch(~/.*/, cl)dir.eachFileMatch(~/.*/, cl)

常用的读方法

def file = new File('/data.txt')println file.text(also for Reader, URL, InputStream,Process)def listOfLines = file.readLines()file.eachLine { line -> ... }file.splitEachLine(/\s/) { list -> }file.withReader { reader -> ... }(also for Reader, URL, InputStream)file.withInputStream { is -> ...}(also for URL)

常用的写方法

out << 'content'for out of type File, Writer, OutputStream, Socket, and Processfile.withWriter('ASCII') {writer -> }file.withWriterAppend('ISO8859-1'){writer -> ... }

使用字符串读写

def out = new StringWriter()out << 'something'def str = out.toString()def rdr = new StringReader(str)println rdr.readLines()

连接读写

writer << reader

可写对象的特殊逻辑,例如 writeTo()

writer << obj

变换(闭包返回替换)和过滤器(闭包返回布尔值)

reader.transformChar(writer){c -> }reader.transformLine(writer){line-> }src.filterLine(writer){line-> }writer << src.filterLine {line -> }For src in File, Reader, InputStream

线程与进程

产生新线程的两种方式

def thread = Thread.start { ... }def t = Thread.startDaemon { ... }

与外部进程通信的两种方式('cmd /c' 仅适用于 Windows 平台)

today = 'cmd /c date /t'.execute().text.split(/\D/)proc = ['cmd','/c','date'].execute()Thread.start {System.out << proc.in}Thread.start {System.err << proc.err}proc << 'no-such-date' + "\n"proc << today.join('-') + "\n"proc.out.close()proc.waitForOrKill(0)

9 XML

读 XML

决定使用解析器(用于基于状态的处理)或 slurper(用于基于流的处理)

def parser = new XmlParser()def slurper = new XmlSlurper()
常见的解析方法:
parse(org.xml.saxInputSource)
parse(File)
parse(InputStream)
parse(Reader)
parse(String uri)
parseText(String text)

parser 和 slurper 的 parse 方法返回不同的对象(Node vs. GPathResult),但您可以在两者上应用以下方法:

result.name()result.text()result.toString()result.parent()result.children()result.attributes()result.depthFirst()result.iterator() // see GDK hot tip

子项、子项和属性访问的简写:

简写 RESULT
['elementName'] All child elements of that name
.elementName All child elements of that name
[index] Child element by index
['@attributeName'] The attribute value stored under that name
.'@attributeName' The attribute value stored under that name
.@attributeName The attribute value stored under that name

从博客中读取前十个标题:

def url= 'http://'+'www.groovyblogs.org/feed/rss'def rss = new XmlParser().parse(url)rss.channel.item.title[0..9]*.text()

写 XML

Groovy (Streaming-) MarkupBuilder 允许您使用逻辑生成适当的 XML,同时保持声明式样式。

def b=new groovy.xml.MarkupBuilder()b.outermost {  simple()  'with-attr' a:1, b:'x', 'content'  10.times { count ->   nesting { nested count }  }}

10 SQL

连接到DB的

直接获取一个新的 Sql 实例。例如,一个 HSQLDB

import groovy.sql.Sqldef db = Sql.newInstance('jdbc:hsqldb:mem:GInA','user-name','password','org.hsqldb.jdbcDriver')

使用数据源的替代方法

import org.hsqldb.jdbc.*def source = new jdbcDataSource()source.database = 'jdbc:hsqldb:mem:GInA'source.user = 'user-name'source.password = 'password'def db = new groovy.sql.Sql(source)

提交查询

当查询包含通配符时,使用PreparedStatement是明智的。当您在额外列表中提供值列表或语句为GString时,Groovy SQL会自动执行此操作。因此,以下每种方法都有三种变体:

method('SELECT ... ')method('SELECT ...?,?', [x,y])method("SELECT ... $x,$y")
RETURNS METHOD NAME PARAMETERS
boolean execute prepStmt
Integer executeUpdate prepStmt
void eachRow prepStmt { row -> }
void query prepStmt { resultSet -> ... }
List rows prepStmt
Object firstRow prepStmt

在上面,可以通过索引或名称从每一行中获取属性

db.eachRow('SELECT a,b ...'){ row -> println row[0] + ' ' + row.b}

与 GPath 结合

List hits = db.rows('SELECT ...')hits.grep{it.a > 0}

DataSet

无需SQL即可轻松进行数据库操作

def dataSet = db.dataSet(tablename)dataSet.add (  a: 1,  b: 'something')dataSet.each { println it.a }dataSet.findAll { it.a < 2 }

在最后一条语句中,findAll 闭包中的表达式将直接映射到 SQL WHERE 子句。

11 元编程

Categories

在运行时分配给实现共同目的的任意类的一组方法。适用于一个线程。范围仅限于闭包。

class IntCodec {  static String encode(Integer self){self.toString()}  static Integer decode(String self){self.toInteger()}}use(IntCodec) {42.encode().decode()}

ExpandoMetaClass

相同的示例,但更改适用于所有线程和无限范围。

Integer.metaClass.encode << {delegate.toString()}String.metaClass.decode << {delegate.toInteger()}42.encode().decode()

方法调用钩子

在您的 Groovy 类中,实现该方法

Object invokeMethod(String name, Object args)

拦截对不可用方法的调用。

此外,实现接口 GroovyInterceptable 以拦截对可用方法的调用。

Implement

Object getProperty(String name)void setProperty(String name, Object value)

拦截属性访问。

变体更容易处理

Object methodMissing(String name, Object args)Object propertyMissing(String name, Object args)

就像名字所暗示的那样。

除了实现上述方法外,还可以将它们添加到任意类(或对象)的 MetaClass 中,以达到相同的效果。

Integer.metaClass.methodMissing << {  String name, Object args ->  Math."$name"(delegate)}println 3.sin()println 3.cos()
CLOSURE PARAMETERS
{ ... } zero or one (implicit 'it')
{-> ... } zero
{x -> ... } one
{x=1 -> ... } one or zero with default
{x,y -> ... } two
{ String x -> ... } one with static type
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • Groovy是一门基于JVM的动态语言,很多语法和Java类似。大部分Java代码也同时是合法的Groovy代码。...
    乐百川阅读 3,519评论 0 15
  • 处理集合 Groovy为各种集合类型提供本地支持,包括列表, Map或Collection。其中大多数基于Java...
    程序员文集阅读 771评论 0 0
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,062评论 9 118
  • Groovy是一门基于JVM的动态语言,很多语法和Java类似。大部分Java代码也同时是合法的Groovy代码。...
    笑叶林阅读 4,178评论 0 2
  • 本篇学习的主要是关于Groovy的语法。Groovy是Java的超集,它可以不用生成字节码直接在JVM上运行。如果...
    掩流年阅读 242评论 0 0