I'm having trouble understanding Groovy types and type promotion. And the exact promises of Groovy's @TypeChecked
annotation.
-- Or maybe I'm having trouble understanding some Groovy design philosophy.
I was playing around with the @TypeChecked annotation and it did not behave as expected. I made two example scripts and I expected both of them to fail because of type mismatches. But only ONE of the scripts fails.
The scripts are very similar. So I thought that they'd also behave in a similar way. The main difference is near the top: I either declare x as int or as String. And then I try to assign a different type to x.
Diff of scripts:
$ diff TypeChecked-fail-int-x.groovy TypeChecked-pass-String-x.groovy -y --width 70
@groovy.transform.TypeChecked @groovy.transform.TypeChecked
void m(){ void m(){
int x | String x
x = 123 | x = "abc"
println(x) println(x)
println(x.getClass()) println(x.getClass())
println() println()
x = "abc" | x = 123
println(x) println(x)
println(x.getClass()) println(x.getClass())
} }
m() m()
When I declare a variable as int but then try to assign a String I will get the expected error:
Script TypeChecked-fail-int-x.groovy
: (Groovy web console here.)
@groovy.transform.TypeChecked
void m(){
int x
x = 123
println(x)
println(x.getClass())
println()
x = "abc"
println(x)
println(x.getClass())
}
m()
Output:
$ groovy --version
Groovy Version: 3.0.10 JVM: 11.0.17 Vendor: Ubuntu OS: Linux
$ groovy TypeChecked-fail-int-x.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
/home/myuser/TypeChecked-fail-int-x.groovy: 11: [Static type checking] - Cannot assign value of type java.lang.String to variable of type int
@ line 11, column 9.
x = "abc"
^
1 error
However: if I do it THE OTHER WAY AROUND then it runs fine. I would have expected the type checker to ALSO catch this.
Script TypeChecked-pass-String-x.groovy
: (Groovy web console here.)
@groovy.transform.TypeChecked
void m(){
String x
x = "abc"
println(x)
println(x.getClass())
println()
x = 123
println(x)
println(x.getClass())
}
m()
Output:
$ groovy TypeChecked-pass-String-x.groovy
abc
class java.lang.String
123
class java.lang.String
And not only does it run but suddenly int 123
has become String "123"
!
I expected BOTH scripts to fail.
I also tried the @CompileStatic
annotation and the results were the same.
Questions:
- Is this expected behavior or a bug? Sources?
- Why is 123 a String now? Is there some autoboxing/casting/type-promotion going on? Can I stop this?
Update 2022-12-01: Fails even WITHOUT @TypeChecked
I found out something: The failing @TypeChecked script will fail even if you remove @TypeChecked. -- But now it fails with a different error message and AT RUNTIME (instead of at compile time).
I'm not sure if this all makes more or less sense to me now.
$ cat TypeChecked-fail-int-x.groovy | grep -v TypeChecked > no-typechecked.groovy
$ groovy no-typechecked.groovy
123
class java.lang.Integer
Caught: org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'abc' with class 'java.lang.String' to class 'int'
org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'abc' with class 'java.lang.String' to class 'int'
at no-typechecked.m(no-typechecked.groovy:11)
at no-typechecked.run(no-typechecked.groovy:16)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
$ groovy --version
Groovy Version: 3.0.10 JVM: 11.0.17 Vendor: Ubuntu OS: Linux
But really I wasn't that interested in the cases that @TypeChecked stopped from running. I was more interested in why did NOT stop the other case from running. And this new nugget of knowledge changes nothing about that.
Well... you came across two concepts in Groovy, Static Type Checking (TypeChecked) and Flow typing. Both of them might seem peculiar at first.
TypeChecked
TypeChecked
has so-called "Type checking assignments" rules. Here is a snippet from that referred page:For example, if you change the initial
String
type toBoolean
you will also be surprised but that will be in line with the Groovy spec and the output will be:If you are curious why the output has two
true
values you might want to read about The Groovy Truth.Flow typing
We are also interested in another statement from that link:
So, if you change the initial variable type from
String
todef
you will see a different result:When the initial type is
String
it will always be aString
variable and you can assign an object of any type to a String variable according to the "Type checking assignments" rules.Summary
So, answering your questions:
Yes, this is expected. Please refer to the links and explanation above.
Again, please refer to the links and explanation above. If you want the variable to change its type then define that variable through the
def
keyword. You can't stop this because (stating the doc again):