Groovy学习笔记三(数据类型)

Groovy数据类型

Groovy在语言层面支持一套数据类型,这意味着groovy提供了直接声明和特定的操作符,包括了字符串、正则表达式和数字等简单类型,也包括范围(Range)、数组和映射等相关的数据集合。
本篇只介绍简单的数据类型,复杂的数据类型在后续文章中进行介绍。
本篇先介绍groovy的通用处理方式(一切皆对象),然后分别介绍groovy原声支持的数据类型。

一、无处不在的对象

groovy是纯面向对象的语言,一切皆对象。java是除了内置数据类型(八种基础类型)之外才是面向对象,我们将对比java的基本数据类型然后再介绍groovy中的处理方式,最后介绍groovy和java是怎样进行自动封装和拆装基本类型的。

1、java类型

在java中一共有两种数据类型:

  • 专有类型(也称为内置类型,即八种基础类型)
  • 引用类型(如:Object、String等)
    两种数据类型采取不用的处理方式,java中的专有类型的变量的值往往是固定的(如:数组/字符/true/false等),并且专有类型是不可以进行自定义的。
    引用类型是除了专有数据类型之外的任何类型——即一个对象(在C/C++中的表现形式为指针)。

不能在专有类型上进行方法调用
在java中不能像对象一样对待专有类型,并且在使用容器的时候,不可以存储基础类型(只能存储专有类型对应的引用类型)。
专有类型的操作符(如:+/-/*/%等)不是所有引用类型都支持的。

例子:有两组整数集合,相加放入第三个集合。

//Java code
List<Integer> results=new ArrayList();
for(int i = 0; i < listOne.size() ; i++){
    results.add(listOne.get(i)+listTwo.get(i));
}

2、Groovy类型

在groovy中,一切皆对象。groovy主张尽量交给计算机处理数据,程序员只需要做少量的事情。

在上小节中的例子,在groovy中实现是非常简单的。

//Groovy code
results.add(first.plus(second));

从上面的例子中可以看出,groovy为java原声的集合类扩展了一些方法使得数据的处理更加方便。

为了支持groovy的完全面向对象,groovy不支持专有数据类型。groovy在设计的时候完全废除了专有类型,当groovy需要使用一个专有类型的时候,实际上操作的是java平台上已经对专有类型封装过的引用类型。

专有类型和他们的包装类型

专有类型 引用类型 描述 范围
byte java.lang.Byte 8位、有符号的,以二进制补码表示的整数 -128(-2^7)至127(2^7-1)
short java.lang.Short 16位、有符号的,以二进制补码表示的整数 -2^15至2^15-1
int java.lang.Integer 32位、有符号的二进制补码表示的整数 -2^31至2^31-1
long java.lang.Long 64位、有符号的二进制补码表示的整数 -2^63至2^63-1
float java.lang.Float 单精度、32位、符合IEEE754标准的浮点数 -2^128至2^127
double java.lang.Double 双精度、64位、符合IEEE754标准的浮点数 -2^1024至2^1023
boolean java.lang.Boolean 表示一位的信息 true/false
char java.lang.Char 单一的16位Unicode字符 /

其中float的指数为为8位,double为11位,并且各自都有一个符号位。

groovy中允许声明专有类型变量,但是其实质是专有类型的包装类型实例。

在groovy中允许通过字面格式实例化java.math.BitDecimal和java.math.BitInteger
就像在java中1.0f表示的是一个float类型的数字一样。在groovy中也支持这种字面格式实例化方式。下表为在groovy中的数字格式。

类型 例子
java.lang.Integer 10,0x1234ff
java.lang.Long 100L
java.lang.Float 1.2f,4.56F
java.lang.Double 1.2d,4.56D
java.math.BigInteger 123g,234G
java.math.BigDecimal 1.23,2.34,1.3E4,2.8e4,1.23g,2.12G

在groovy中数字后是否出现g/G来表示该类型是否是BigInteger/BigDecimal的类型的,注意BigDecimal是默认的非整数类型,除非该数字后指定f/F,否则默认的非整数类型为BigDecimal

3、groovy的自动包装

装箱:把一个专有类型转为一个包装类型的动作。
拆箱:把一个包装类型转为专有类型的动作。
groovy在需要的时候自动进行装箱和拆箱处理——自动包装。

例子:

assert 'ABCDE'.indexOf(67) == 2

下面分析上面语句的包装过程,'ABCDE'是一个String类型,不需要包装的转换,67在groovy中是一java.lang.Integer类型,所以在运行时,先对其进行拆箱变为int类型,然后执行语句得到结果’2’,此时’2’是一个’int’类型,然后对其进行装箱变为’java.lang.Integer’类型,下图为groovy到java再到groovy的处理过程。
图3.1

4、没有中间层的拆箱

在某些场景下是不用执行中间层的拆箱过程的,如:1+1这个表达式中,两个数字均为’java.lang.Integer’类型的对象,groovy会通过1.plus(1)执行并生成一个新的’java.lang.Integer’对象,并不会经过中间层的拆箱过程。而此时的操作符仅仅作为方法的快捷途径,其实质还是使用的方法调用。

二、可选类型

在前面的例子中,很多时候并没有强制指定groovy的数据类型,并对其直接进行了赋值,其实,在groovy中这种场景下默认赋值为java.lang.Object类型的变量,本节会介绍groovy的动态类型。

1、指定类型

groovy允许想java这样显示地指定类型(强类型)。也可以用def关键字标记没有特定的类型要求(弱类型)。
一个变量是否显示指定数据类型是很重要的,系统是类型安全的。groovy不允许将一个定义好的类型的变量看成另外一种类型。

2、静态类型和动态类型

groovy可以在静态类型和动态类型之间进行选择。
静态类型为性能优化、编译时安全检查和IDE支持提供更多帮助,也显示变量或者方法参数相关的附加信息及方法重载,静态类型也是从反射获取有用信息的前提。
动态类型,便于编写一些特定的脚本,也对保护和规避类型有用。

三、重载操作符

重载:一个类有一个特定的行为,自类为了达到某个目的来重新了这个行为,当一门语言的操作符是基于一个方法调用时,如果这些方法能够被覆盖,这种行为称为操作符重载。

1、可重写的操作符

当一个类需要使用该操作符的时候,不需要继承任何接口,只需要实现相应的方法即可。重写不是重载!!!

操作符的方法

操作符 名称 方法 作用对象
a+b a.plus(b) number/String/Collection
a-b a.minus(b) number/String/Collection
a*b a.multiply(b) number/String/Collection
a/b a.div(b) number
a%b a.mod(b) number
a++/++a 自增 a.next() number/String/Range
a–/–a 自减 a.previous() number/String/Range
a**b 倍数 a.power(b) number
a|b a.or(b) Integral number
a & b a.and(b) Integral number
a ^ b 异或 a.xor(b) Integral number
~a a.negate() Object
a[b] 下标 a.getAt(b) Object/List/Map/String/Array
a[b]=c 下标分配 a.putAt(b,c) Object/List/Map/String/Array
a << b 左移 a.leftShift(b) Integral number/StringBuffers/Socket/List
a >> b 右移 a.rightShift(b) Integral number/StringBuffers/Socket/List
a >>> b 无符号位右移 a.rightShiftUnsigned(b) Integral number
switch(a){case b:} 分类 b.isCase(a) Object
a == b 相等 a.equals(b) Object
a != b 不等 !a.equals(b) Object
a <==> b 比较 a.compareTo(b) java.lang.Comparable
a > b 大于 a.compareTo(b)>0 java.lang.Comparable
a >= b 大于等于 a.compareTo(b)>=0 java.lang.Comparable
a < b 小于 a.compareTo(b)<0 java.lang.Comparable
a <= b 小于等于 a.compareTo(b)<=0 java.lang.Comparable
a as type 强转 a.asType(typeclass) Any type

*建议:在重写equals方法的时候,建议重写hashCode()方法,此时两个对象才能有相同的hashCode。*

注意:严格意义上来说,所有的操作符都可以被重写,上面列举的并不是全部,如.操作符用来引用字段和方法,这种行为也可以被重写。

2、实战

上节介绍了在groovy中的操作符是可以重写的,并且列出除了重写的方法,本节调两个方法进行实战验证一下。

例子:实现不同币种货币的增加。

//货币类
class Money{
    private int amount
    private String currency

    Money(int amount,String currency){
        this.amount=amount
        this.currency=currency
    }

    boolean equals(Object obj){
        if(obj == null)return false
        if(!(obj instanceof Money))return faslse
        if(currency!=obj.currency)return false
        return true
    }
    Money plus(Money other){
        if(other==null)return null
        if(!equals(other)){
            throw new IllegalArgumentException('cannot add $other.currency to $currency')
        }
        return new Money(other.amount+amount,currency)
    }
    Money plus(int other){
        return new Money(other+amount,currency)
    }
}
def buck=new Money(1,'$')
assert buck
assert buck == new Money(1,'$')
assert buck+buck == new Money(2,'$')
assert buck+1 == new Money(2,'$')

在上面的例子中我们对类使用了操作符+,这在java中是难以想象的,从两个plus方法的不同体现我们是重写而非重载

3、确保正确的工作

两个相同类型的重现操作符是比较简单的,但是不同类型之间进行操作会产生一些问题。如:1+1.0,返回的是Integer类型还是BigDecimal呢?一般解决方式是:两个参数中的一个需要升到更通用的类型,即类型强制转换

实现操作符的时候有以下三个问题需要解决:
支持的参数类型
实现操作符的时候需要考虑那种参数类型和值是被允许的,如果一个操作符接受了不适当的参数类型,那么在必要的时候请抛出异常IllegalArgumentException

设置更明确的参数
如果参数类型比定义更明确(如:接口),那么groovy将使用个返回定义的类型。例:给BigDecimal添加plus方法接受Integer作为参数,我们可以考虑把作为参数的Integer转换为BigDecimal然后再进行相加,但是反过来确实不可以的。

使用双派发处理更多的通用参数
如果参数的类型更加通用,使用当前对象作为参数调用参数自身的操作符方法,将操作符方法派给参数的操作符方法,即双派发,并且这样可以避免参数的重复,和可能不一致的代码。

Groovy的示量行为
Groovy的类型处理策略是返回一个更一般的类型。

四、使用字符串

Groovy给字符串提供了很多附加的属性。
Groovy提供了两种使用字符串的方式GString和一般字符串。
一般字符串:java.lang.String的实例。
GString:groovy.lang.GString的实例,允许占位符并且在运行时对其进行解析和处理计算。

1、字符串的样式

Java字符串的使用为文本放在双引号中,如果想嵌入动态值到字符串中,必须调用格式化方法或者进行字符串拼接。如果代码中有许多反斜杠\,代码将变得非常难读,因为必须要进行特殊字符的处理(双斜杠)。

Groovy在这方面提供了更多的选择
图3.2

  • 单引号所表示的字符串不会按照GString的类型进行处理,相当于java的String。
  • 双引号表示的意思与单引号是等价的。如果字符串中没有转义符号$,那么它会被加工成GString实例。
  • 三组引号(或者多行字符串)允许字符串的内容在多行出现,新的行数总是被转换为\n,其他所有空白的字符都会完整的按照文本原样保留,多行字符串也许是一个GString实例,这根据单引号还是双引号来决定。
  • /表示的字符串,指明字符串内容不转义反斜杠\,这在正则表达式的时候特别有用。只有在一个反斜杠接下来的是一个字符u的时候才需要进行转义,\u用来表示一个unicode的转义。

目前groovy已知的转义字符
图3.3
注意:在双引号字符串中,单引号不需要进行转义,反过来也是一样的。$在单引号字符串中不可以进行转义。

2、使用GString进行工作

GString像增加了额外功能的字符串,但是String是final类型的,不管怎样GString都可以像String一样使用,在需要的时候GString自动将自己转换为String类型。
GString的特征是通过双引号进行声明的字符串,及内部是否出现占位符${表达式}或者$reference的语法出现。

例子:

name='Groovy'
println '$name in Action'
println "$name in Action"
date=new Date()
println "year ${date.year} month ${date.month} day ${date.day}"
println "What's this"

输入结果:

$name in Action
Groovy in Action
year 117 month 10 day 2
What's this

GString的使用比较简单,在此就不再进行过多阐述了。

3、从java到groovy

从上面可以看出groovy中使用字符串变得简单了很多,但是groovy中可以作为字符串的类型也比较多,目前有:String、GString、StringBuffer。而且各自都有各自的方法,我们应该根据字符串的类型选择不同的变量。并且应该根据方法的不同来区分字符串所属的类型。

greeting='Hello Groovy'
assert greeting.startsWith('Hello')
assert greeting.getAt(0)=='H'
assert greeting[0]=='H'
assert greeting.indexOf('Groovy')==6
assert greeting.contains('Hello')
assert greeting[6..11]=='Groovy'
assert 'Hi'+greeting-'Hello'=='Hi Groovy'
assert greeting.count('o')==3

assert 'x'.padLeft(3)=='  x'
assert 'x'.padRight(3,'_')=='x__'
assert 'x'.center(3)==' x '
assert 'x'*3=='xxx'

在groovy中使用StringBuffer

greeting='Hello'
greeting<<=' Groovy'//追加文本+赋值
assert greeting instanceof java.lang.StringBuffer
greeting<<'!'//追加文本
assert greeting.toString()=='Hello Groovy!'
greeting[1..4]='i'//替换子串
assert greeting.toString()=='Hi Groovy!'

*注意:StringRef<<String返回的是一个StringBuffer,但是并没有对其进行赋值,所以要使String变为StringBuffer的时候需要对其进行赋值。

greeting='Hello'
greeting<<'!!!'
println greeting.class
println greeting
greeting<<=' Groovy'//追加文本+赋值
println greeting.class

运行结果:

class java.lang.String
Hello
class java.lang.StringBuffer

从上面的运行结果可以看出String和StringBuffer之间相互转换时候的关系。

五、使用正则表达式

正则表达式允许声明一个模式而不是编程,groovy比起java在使用正则表达式方面更加方便,并且为了方便提供了三个操作符:

  • regex查找操作符=~
  • regex匹配操作符’==~’
  • regex模式操作符’~String’

1、在字符串中使用正则表达式

做过java开发的人应该都知道,在java中使用正则表达式是十分痛苦的,因为各种转义符使得正则的阅读十分困难,groovy的出现本来就是为了让java的使用更加方便,所以这种问题也在解决范围之内。

groovy的正则使用/.../来表示表达式,并且支持占位符

assert 'abc'==/abc/
assert "\\d"==/\d/
reference = "hello"
assert reference == /$reference/
assert "\$" == /$/

从上面的例子可以看出在groovy中使用正则表达式的时候更加方便,表达也更加清晰,更加方便开发者进行开发。

符号
使用正则表达式的关键是明白各个符号的含义,下面列出一些正则表达式的符号。

符号 含义
. 除了\n之外的任意单个字符
^ 匹配字符串的开始位置
$ 匹配字符串的结束位置
[xyz] 字符集合,匹配所包含的任意一个字符
[^xyz] 负值字符集合,匹配未包含的任意字符
\d 匹配一个数字字符,等价于[0-9]
\D 匹配一个非数字字符,等价于[^0-9]
\s 匹配任何空白字符,等价于[\f\n\r\t\v]
\S 匹配任何非空白字符,等价于[^\f\n\r\t\v]
\w 匹配包括下划线的任意单词字符,等价于[A-Za-z0-9_]
\W 匹配任意非单词字符,等价于[^A-Za-z0-9_]
\b 匹配单词的边界,即单词和空格间的位置。如:[er\b]可以匹配’never’中的’er’,但不可以匹配verb
\B 匹配非单词的边界。如:[er\B]可以匹配’verb’中的’er’,但不可以匹配never
(pattern) 匹配pattern并获取这一匹配。其中需要匹配括号的请用转义字符,\(或者\)
x|y 匹配x或者y
* 匹配前面的子表达式0次或多次
+ 匹配前面的子表达式一次或多次
? 匹配前面的子表达式零次或一次
{n} n是一个非负整数。匹配确定的n次,如o{2}可以匹配food但是不可以匹配Bob
{n,} n是一个非负整数。匹配至少n次,如o{2,}不可以匹配Bob,但是可以匹配foooood中的o
{n,m} m和n均为非负整数,其中n<=m。最少匹配n次,最多匹配m次
(?:pattern) 匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用 “或” 字符 (|) 来组合一个模式的各个部分是很有用。例如, ‘industr(?:y|ies) 就是一个比 ‘industry|industries’ 更简略的表达式。
(?=pattern) 正向预查,在任何匹配 pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,’Windows (?=95|98|NT|2000)’ 能匹配 “Windows 2000” 中的 “Windows” ,但不能匹配 “Windows 3.1” 中的 “Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?!pattern) 负向预查,在任何不匹配 pattern 的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如’Windows (?!95|98|NT|2000)’ 能匹配 “Windows 3.1” 中的 “Windows”,但不能匹配 “Windows 2000” 中的 “Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始

更多使用请自行尝试,正则表达式不管在什么语言中都会使用到,近期我会抽空用groovy写一个爬虫来学习正则表达式,在后续的文章中将会进介绍

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