Reputation: 3102
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:
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.
Upvotes: 0
Views: 420
Reputation: 3276
Well... you came across two concepts in Groovy, Static Type Checking (TypeChecked) and Flow typing. Both of them might seem peculiar at first.
TypeChecked
has so-called "Type checking assignments" rules. Here is a snippet from that referred page:
An object o of type A can be assigned to a variable of type T if and only if:
- T equals A
- or T is one of String, boolean, Boolean or Class (this one is the most relevant for the question)
- ...
For example, if you change the initial String
type to Boolean
you will also be surprised but that will be in line with the Groovy spec and the output will be:
true
class java.lang.Boolean
true
class java.lang.Boolean
If you are curious why the output has two true
values you might want to read about The Groovy Truth.
Flow typing is an important concept of Groovy in type checked mode and an extension of type inference. The idea is that the compiler is capable of inferring the type of variables in the flow of the code, not just at initialization.
We are also interested in another statement from that link:
It is important to understand that it is not the fact of declaring a variable with def that triggers type inference. Flow typing works for any variable of any type. Declaring a variable with an explicit type only constrains what you can assign to the variable.
So, if you change the initial variable type from String
to def
you will see a different result:
abc
class java.lang.String
123
class java.lang.Integer
When the initial type is String
it will always be a String
variable and you can assign an object of any type to a String variable according to the "Type checking assignments" rules.
So, answering your questions:
Is this expected behavior or a bug? Sources?
Yes, this is expected. Please refer to the links and explanation above.
Why is 123 a String now? Is there some autoboxing/casting/type-promotion going on? Can I stop this?
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):
Flow typing has been introduced to reduce the difference in semantics between classic and static Groovy.
Upvotes: 1