“Groovy cast” tries too hard
Groovy has an entire three different types of type-casts that can occur. The first two are inherited from Java, albeit with substantially more permissive semantics.
Implicit casting occurs when a type other than the one expected occurs in a particular context. Numeric types may be coërced to each other or to strings, and objects may be up- or down-cast; Java only permits widdening coërsions and up-casting in implicit casts, but Groovy allows both directions due to its dynamic type system. For example, in the expression
2+3.14
, the value2
is implicitly coërced fromint
todouble
(orBigDecimal
in the case of Groovy) in order for the types to match, resulting in a final value of 5.14.C-style casting uses a notation borrowed from C, in which the desired type for an expression is placed in parantheses before the expression to cast. This has similar semantics to implicit casts, except that the type is selected by the programmer, and the cast will occur even if the compiler would not otherwise think it necessary. In Java, explicit casts are the only way to get narrowing coërsions and down-casts. In
2+(int)3.14
, the value3.14
is implicitly narrowed to anint
, so the result of the whole expression is 5.
Groovy’s third type of cast, usually called the Groovy cast, follows the
expression to cast, and is formed by placing the desired type after the keyword
as
. Most programmers believe it to be merely an alternate syntax to C-style,
and some prefer the syntax, even though it violates the inherent right-to-left
flow of information which normally characterises expressions in the C
family. There’s also a number of other oddities involving this syntax. The
Groovy cast operator has surprisingly high precedence — 3/2 as double
yields
1.5 (ie, it is parsed as 3/(2 as double)
); (int)1.5 as String
throws a
GroovyCastException
because the String conversion occurs first. 3/2 as
double < 1
is a syntax error, since the <
looks like a generic parameter.
In actuallity, the Groovy cast has semantics distinct from that of the C-style
cast. A Groovy cast will attempt to perform a number of object-to-object
coërsions (as opposed to strict casts). Some of these are actually useful:
[1,2,3] as Set
creates a set of Integer
s concisely, working around Groovy’s
lack of a built-in set literal syntax. On the other hand, most of these
conversions are ill-defined. For example, casting a value with unknown type to
a List
results in a List
containing that one value, unless that value has
a separate conversion to List
, or is null
.
One of the most bizarre and surprising of these conversions, however, is that
any object may be cast to any interface — it doesn’t need to implement the
interface, or even have any of its methods. Granted, Java interfaces are a bit
problematic, mainly owing to Java’s ban on default interface implementations,
and the complete obliviousness of many interface designers on what an interface
exactly is (see the remove
method on interface
Iterator
). But
what Groovy does is downright ridiculous.
To demonstrate, consider the below program, which you can run yourself.
Main.java
1package gl.lin;
2
3import java.util.Iterator;
4
5public class Main {
6 public static void main(String args[]) {
7 Iterator<Object> it = GetIterator.getInstance();
8 System.out.println("Got iterator: " + it);
9
10 while (it.hasNext())
11 System.out.println(it.next());
12 }
13}
GetIterator.groovy
1package gl.lin;
2
3class GetIterator {
4 static Iterator<Object> getInstance() {
5 return new Object() as Iterator; // !
6 }
7}
The Java code simply asks the Groovy code for an Iterator
, then does
something completely reasonable by assuming that what it got really was an
Iterator
. The Groovy code, however, just creates a raw Object
(which has
none of Iterator
’s methods) and Groovy-casts it to an Iterator
. Here’s what
happens when we execute this program:
1$ ./gradlew run
2
3...
4
5Got iterator: Object_delegateProxy@3f69582a
6Exception in thread "main" groovy.lang.MissingMethodException: No signature of m
7ethod: java.lang.Object.hasNext() is applicable for argument types: () values: [
8]
9Possible solutions: inspect(), hashCode(), wait(), collect()
10 at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptByteco
11deAdapter.java:55)
12 at org.codehaus.groovy.runtime.InvokerHelper$invokeMethod.call(Unknown S
13ource)
14 at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSi
15teArray.java:42)
16 at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCa
17llSite.java:108)
18 at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCa
19llSite.java:124)
20 at Object_delegateProxy.hasNext(Script1.groovy:19)
21 at gl.lin.Main.main(Main.java:10)
As can be seen in the output, the value returned by Groovy was an Iterator
,
as far as the type system is concerned. Yet the moment it calls one of the
Iterator
methods, an exception is thrown to the Java code, since there is no
method Object.hasNext()
.
The particular exception type being thrown is also of interest. Since Java has
so many interfaces like Iterator
where some part (like remove()
) ends up
becomming considered “optional” — because it really should be part of a
different (sub)interface — convention is to throw an
UnsupportedOperationException
from methods that a particular implementation
of an interface does not support. Groovy instead throws a
MissingMethodException
, which isn’t even part of the Java standard libraries,
but instead within Groovy.
This example is contrived, since the Groovy code pretty much says it will do one thing and then immediately does something different. However, in larger code-bases, such things do occur on accident. Groovy’s pathological sometimes-dynamic-sometimes-weak-pretends-to-be-static type system leads many programmers to insert numerous paranoid explicit casts; those who favour Groovy casts wind up performing nonsensical “conversions” that nobody thought possible, rather than asserting that values had particular types.
All in all, the Groovy cast is almost completely a non-feature:
It is hard to read. The syntax has counter-intuitive precedence, and violates the language’s direction of flow of information as well as the end-weight theorem. Especially with shorter type names, a Groovy cast at the end of an expression is easily missed.
It is largely redundant with the C-style cast, which is generally easier to read except within the most simple of expressions. Those few useful additional conversions performed by the Groovy cast (such as
List
→Set
) should really be provided by Groovy’s implementation of the C-style cast.The vast majority of “conversions” provided by the Groovy cast are useless, have varying semantics depending on the run-time value being cast, or undermine the Java interface system. Values which have passed through a Groovy cast cannot be expected to have any particular semantics or methods, even if the resulting static type of the value would suggest otherwise.