Groovy 2.0 其中一項最大的改進就是支持靜態類型檢查。今天我們將對這個新特性進行全方位的介紹。
Groovy 天生就是一個動態編程語言,它經常被當作是 Java 腳本語言,或者是“更好的 Java”。很多 Java 開發者經常將 Groovy 嵌入到 Java 程序中做為擴展語言來使用,更簡單的描述業務規則,將來為不同的客戶定制應用等等。對這樣一個面向 Java 的用例,開發者不需要語言提供的所有動態特性,他們經常希望 Groovy 也提供一個類似 javac 的編譯器,例如在發生一些錯誤的變量和方法名錯誤或者錯誤的類型賦值時就可以在編譯時就知道錯誤,而不是運行時才報錯。這就是為什麼 Groovy 2.0 提供了靜態類型檢查功能的原因。
靜態類型檢測器使用了 Groovy 已有強大的 AST (抽象語法樹) 轉換機制,如果你對這個機制不熟悉,你就把它當作一個可選的通過注解進行觸發的編譯器插件。這是一個可選的特性,可用可不用。要觸發靜態類型檢查,只需要在方法上使用@TypeChecked
注解即可。讓我們來看一個簡單的例子:
01
import
groovy.transform.TypeChecked
02
03
void
someMethod() {}
04
05
@TypeChecked
06
void
test() {
07
// compilation error:
08
// cannot find matching method sommeeMethod()
09
sommeeMethod()
10
11
def
name =
"oschina"
12
13
// compilation error:
14
// the variable naaammme is undeclared
15
println
naaammme
16
}
我們使用了 @TypeChecked
對 test()
方法進行注解,這讓 Groovy 編譯器在編譯期間運行靜態類型檢查來檢查指定的方法。當我們試圖用明顯錯誤的方法來調用 someMethod()
時,編譯器將會拋出兩個編譯錯誤信息表明方法和變量為定義
靜態類型檢查還能驗證返回值和變量賦值是否匹配:
01
import
groovy.transform.TypeChecked
02
03
@TypeChecked
04
Date test() {
05
// compilation error:
06
// cannot assign value of Date
07
// to variable of type int
08
int
object =
new
Date()
09
10
String[] letters = [
'o'
,
's'
,
'c'
]
11
// compilation error:
12
// cannot assign value of type String
13
// to variable of type Date
14
Date aDateVariable = letters[
0
]
15
16
// compilation error:
17
// cannot return value of type String
18
// on method returning type Date
19
return
"today"
20
}
在這個例子中,編譯器將告訴你不能將 Date 值賦值個 int 變量,你也不能返回一個 String,因為方法已經要求是返回 Date 類型數據。代碼中間的編譯錯誤信息也很有意思,不僅是說明了錯誤的賦值,還給出了類型推斷,因為類型檢測器知道 letters[0]
的類型是 String。
因為提到了類型推斷,讓我們來看看其他的一些情況,我們說過類型檢測器會檢查返回類型和值:
01
import
groovy.transform.TypeChecked
02
03
@TypeChecked
04
int
method() {
05
if
(true) {
06
// compilation error:
07
// cannot return value of type String
08
// on method returning type int
09
'String'
10
}
else
{
11
42
12
}
13
}
指定了方法必須返回 int 類型值後,類型檢查器將會檢查各種條件判斷分支的結構,包括 if/elese、try/catch、switch/case 等。在上面的例子中,如果 if 分支中返回字符串而不是 int,編譯器就會報錯。
靜態類型檢查器並不會對 Groovy 支持的自動類型轉換報告錯誤,例如對於返回 String, boolean
或 Class 的方法,Groovy 會自動將返回值轉成相應的類型:
01
import
groovy.transform.TypeChecked
02
03
@TypeChecked
04
boolean
booleanMethod() {
05
"non empty strings are evaluated to true"
06
}
07
08
assert
booleanMethod() == true
09
10
@TypeChecked
11
String stringMethod() {
12
// StringBuilder converted to String calling toString()
13
new
StringBuilder() <<
"non empty string"
14
}
15
16
assert
stringMethod()
instanceof
String
17
18
@TypeChecked
19
Class classMethod() {
20
// the java.util.List class will be returned
21
"java.util.List"
22
}
23
24
assert
classMethod() == List
而且靜態類型檢查器在類型推斷方面也足夠聰明:
01
import
groovy.transform.TypeChecked
02
03
@TypeChecked
04
void
method() {
05
def
name =
" oschina.net "
06
07
// String type inferred (even inside GString)
08
println
"NAME = ${name.toUpperCase()}"
09
10
// Groovy GDK method support
11
// (GDK operator overloading too)
12
println
name.trim()
13
14
int
[] numbers = [
1
,
2
,
3
]
15
// Element n is an int
16
for (
int
n
in
numbers) {
17
println
18
}
19
}
雖然變量 name
使用 def
進行定義,但類型檢查器知道它的類型是 String
. 因此當調用 ${name.toUpperCase()} 時,編譯器知道在調用 String 的 toUpperCase()
方法和下面的 trim()
方法。當對 int 數組進行迭代時,它也能理解數組的元素類型是 int
.
你必須牢記於心是:靜態類型檢查限制了你可以在 Groovy 使用的方法。大部分運行時動態特性是不被允許的,因為他們無法在編譯時進行類型檢查。例如不允許在運行時通過類型的元數據類(metaclasses)來添加新方法。但當你需要使用一些例如 Groovy 的 builders 這樣的動態特性時,如果你願意,你還是可以選擇靜態類型檢查。
@TypeChecked
注解可放在方法級別或者是類級別使用。如果你想對整個類進行類型檢查,直接在類級別上放置這個注解即可,否則就在某些方法上進行注解。你也可以使用 @TypeChecked(TypeCheckingMode.SKIP) 或者是
@TypeChecked(SKIP)
來指定整個類進行類型檢查除了某個方法。使用 @TypeChecked(SKIP) 必須靜態引入對應的枚舉類型。下面代碼可以用來演示這個特性,其中
greeting()
方法是需要檢查的,而 generateMarkup()
方法則不用:
01
import
groovy.transform.TypeChecked
02
import
groovy.xml.MarkupBuilder
03
04
// this method and its code are type checked
05
@TypeChecked
06
String greeting(String name) {
07
generateMarkup(name.toUpperCase())
08
}
09
10
// this method isn't type checked
11
// and you can use dynamic features like the markup builder
12
String generateMarkup(String name) {
13
def
sw =
new
StringWriter()
14
new
MarkupBuilder(sw).html {
15
body {
16
div name
17
}
18
}
19
sw.toString()
20
}
21
22
assert
greeting(
"Cédric"
).
contains
(
"<div>CÉDRIC</div>"
)
目前的 Java 並不支持一般的類型推斷,導致今天很多地方的代碼往往是相當冗長,而且樣板結構混亂。這掩蓋了代碼的實際用途,而且如果沒有強大的 IDE 支持的話代碼會很難寫。於是就有了 instanceof 檢查:你經常會在 if 條件判斷語句
中使用 instanceof 判斷。而在 if 語句結束後,你還是必須手工對變量進行強行類型轉換。而有了 Groovy 全新的類型檢查模式,你可以完全避免這種情況出現:
01
import
groovy.transform.TypeChecked
02
import
groovy.xml.MarkupBuilder
03
04
@TypeChecked
05
String test(Object val) {
06
if
(val
instanceof
String) {
07
// unlike Java:
08
// return ((String)val).toUpperCase()
09
val.toUpperCase()
10
}
else
if
(val
instanceof
Number) {
11
// unlike Java:
12
// return ((Number)val).intValue().multiply(2)
13
val.intValue() *
2
14
}
15
}
16
17
assert
test(
'abc'
) ==
'ABC'
18
assert
test(
123
) ==
'246'
上述例子中,靜態類型檢查器知道 val 參數在 if 塊中是 String 類型,而在 else if 塊中是 Number 類型,無需再做任何手工類型轉換。
靜態類型檢測器比一般理解的對象類型診斷要更深入一些,請看如下代碼:
1
import
groovy.transform.TypeChecked
2
3
// inferred return type:
4
// a list of numbers which are comparable and serializable
5
@TypeChecked test() {
6
// an integer and a BigDecimal
7
return
[
1234
,
3.14
]
8
}
在這個例子中,我們返回了數值列表,包括 Integer
和 BigDecimal
. 但靜態類型檢查器計算了一個最低的上限,實際上是一組可序列化(Serializable)和可比較(Comparable)的數值。而 Java 是不可能表示這種類型的,但如果我們使用一些交集運算,那看起來就應該是 List<Number & Serializable & Comparable>.
雖然這可能不是一個好的方法,但有時候開發者會使用一些無類型的變量來存儲不同類型的值,例如:
01
import
groovy.transform.TypeChecked
02
03
@TypeChecked test() {
04
def
var =
123
// inferred type is int
05
var =
"123"
// assign var with a String
06
07
println
var.toInteger()
// no problem, no need to cast
08
09
var =
123
10
println
var.toUpperCase()
// error, var is int!
11
}
上面代碼中 var 變量一開始是 int 類型,後來又賦值了字符串,“flow typing”算法可以理解賦值的順序,並指導 var 當前是字符串類型,這樣調用 Groovy 為 String 增加的 toInteger() 方法就沒問題。緊接著又賦值整數給 var 變量,但現在如果再次調用 toUpperCase() 就會報出編譯錯誤。
還有另外一些關於 “flow typing” 算法的特殊情況,當某個變量在一個閉包中被共享該會是怎麼樣的一種情況呢?
01
import
groovy.transform.TypeChecked
02
03
@TypeChecked test() {
04
def
var =
"abc"
05
def
cl = {
06
if
(
new
Random().nextBoolean()) var =
new
Date()
07
}
08
cl()
09
var.toUpperCase()
// compilation error!
10
}
var
本地變量先賦值了一個字符串,但是在閉包中會在一些隨機的情況下被賦值為日期類型數值。一般情況下這種只能在運行時才能報錯,因為這種錯誤是隨機發生的。因此在編譯時,編譯器是沒有機會知道 var 變量是字符串還是日期,這就是為什麼編譯器無法得知錯誤的原因。盡管這個例子有點做作,但還有更有趣的情況:
01
import
groovy.transform.TypeChecked
02
03
class
A {
void
foo() {} }
04
class
B
extends
A {
void
bar() {} }
05
06
@TypeChecked test() {
07
def
var =
new
A()
08
def
cl = { var =
new
B() }
09
cl()
10
// var is at least an instance of A
11
// so we are allowed to call method foo()
12
var.foo()
13
}
在 test()
方法中,var 先被賦值為 A 的實例,緊接著在閉包中被賦值為 B 的實例,然後調用這個閉包方法,因此我們至少可以診斷 var 最後的類型是 A。
Groovy 編譯器的所有這些檢查都是在編譯時就完成了,但生成的字節碼還是跟一些動態代碼一樣,在行為上沒有任何改變。
Groovy 的詳細介紹:請點這裡
Groovy 的下載地址:請點這裡
相關閱讀: Groovy入門教程 http://www.linuxidc.com/Linux/2013-09/89776.htm