groovy静态编译容易引发的问题

groovy静态编译容易引发的问题

groovy为了提高性能,有一个静态编译的注解@CompileStatic,在类上添加该注解后,编译出来的class文件会更加静态化,更像java文件编译出来的class字节码。
但是因此会导致一些问题,今天我就遇到一个,下面将其记录下来。

众所周知,groovy是动态类型的,如下面重载的函数,使用java调用和groovy调用结果是不同的。

1. 测试groovy非静态编译

在groovy中,定义变量时使用的是Object,是在调用时决定调用哪一个方法的,下面两次调用test方法,均能正确识别出对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Test {
static void main(String[] args) {
Object param = "a"
test(param) //---> String

Object param2 = 1
test(param2) //---> Integer
}

static void test(Object b) {
println "Object"
}

static void test(String a) {
println "String"
}

static void test(Integer b) {
println "Integer"
}
}

2.测试java

上面同样的代码,使用java写就会变得不一样,因为java是在编译期间就决定应该调用哪个方法的,下面三个方法中,只会调用参数为Object的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test2 {

public static void main(String[] args) {
Object param = "a";
test(param); //---> Object

Object param2 = 1;
test(param2); //---> Object

}

static void test(Object a) {
System.out.println("Object");
}

static void test(String b) {
System.out.println("String");
}

static void test(Integer b) {
System.out.println("Integer");
}
}

3.测试groovy静态编译

现在已经明白groovy和java调用的区别了,那如果将groovy进行静态编译后,结果是什么样子呢?
使用下面代码测试后,发现,groovy启用静态编译后仍然能够识别到对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@CompileStatic
class Test {

static void main(String[] args) {
Object param = "a"
test(param) //---> String

Object param2 = 1
test(param2) //---> Integer
}

static void test(Object b) {
println "Object"
}

static void test(String a) {
println "String"
}

static void test(Integer b) {
println "Integer"
}
}

4.加大难度测试groovy静态编译

既然添加静态编译注解后,为啥编译出来的代码的运行行为仍然和非静态编译的groovy效果一样呢?我怀疑是代码太简单了,groovy能识别出类型,我将情况弄得再复杂一些。
为了给groovy编译器增加难度,我将参数先存储到list里,再取出,看groovy能否识别。
经过下面的测试结果,完全出乎我的预期,groovy将Integer强转成String了,他做了转换。即使参数是Date对象,他仍然将其转成了字符串,这种转换在大部分情况下都是不符合正常人预期的。
大部分情况下会导致问题。而移除@CompileStatic注解后,因为是在运行时判断类型的,所以能够识别出正确的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@CompileStatic
class Test {

static void main(String[] args) {
List list = Arrays.asList("a", 1,new Date())
test(list.get(0)) //---> String

test(list.get(1)) //---> String

test(list.get(2)) //---> String
}

static void test(Object b) {
println "Object"
}

static void test(String a) {
println "String"
}

static void test(Integer b) {
println "Integer"
}
}

上面这种不符合预期的方法查找就是产生bug的根源,并且idea 点击ctrl左键跳转的方法,与实际选择的方法不一致,这也增加了查找bug的难度。所以我建议减少使用静态编译注解的使用,虽然其能够提高点性能,但可能结果是无法预期的。

5.测试java中正确,groovy中不正确的例子。内层闭包使用外层闭包变量,无法正确识别变量类型

下面的代码中,我们使用了静态编译,其中map操作符中的pop参数是String类型的,然而真实调用时,触发testFunction的却是参数为Object的。
这是因为我们在使用pop参数时,没有在当前闭包中使用,而是再创建了一个闭包,我们在内层闭包中使用pop时,没有将pop识别成String类型,而在java中因为是强类型,是可以识别的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@CompileStatic
class Test {

static void main(String[] args) {
List<String> list = Arrays.asList("a", "b", "c")

Object response = list.stream()
.map({ pop ->
return {
testFunction(pop)
return "sss"
}.call()
})
.collect(Collectors.toList());

System.out.println(response)
}

static void testFunction(Object a) { //此函数被调用
System.out.println("object");
}

static void testFunction(String a) {
System.out.println("String");
}
}

将上面的代码中添加一行,在第一个闭包中使用一下pop变量,那么就能正确识别到变量pop是String类型,从而调用参数为String的testFunction方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@CompileStatic
class Test {

static void main(String[] args) {
List<String> list = Arrays.asList("a", "b", "c")

Object response = list.stream()
.map({ pop ->
def a = pop //在外层闭包中使用一次pop变量
return {
testFunction(pop)
return "sss"
}.call()
})
.collect(Collectors.toList());

System.out.println(response)
}

static void testFunction(Object a) {
System.out.println("object");
}

static void testFunction(String a) { //此函数被调用
System.out.println("String");
}
}

第五条描述的现象(静态编译下,内层闭包直接使用外层闭包参数,导致参数类型无法识别)也是我今天第一次发现。我还没有查找资料,我猜应该是bug吧,毕竟这样与java差距太大,不符合常人预期,后面有发现我再继续更新。


groovy静态编译容易引发的问题
https://www.huangchaoyu.com/3892320602.html
作者
hcy
发布于
2021年10月16日
更新于
2024年8月17日
许可协议