DEV Community

k-mack
k-mack

Posted on • Edited on • Originally published at kevinmacksa.me

Groovy's @CompileStatic and Methods with the Same Name

Application programming interfaces (APIs) can get whacky, but compiled languages help users to get things semantically correct. And dynamic languages? Their ergonomic "dynamic sauce" ladled over a codebase can sometimes be less than helpful. This post is about how Groovy's @CompileStatic can help to demystify what is happening at the call site of a method that has the same name and descriptor as another.

Part I: The Java API

Let's start with what we know we cannot do in Java: No two methods in one class file may have the same name and descriptor.1 The Java compiler simply does not let us do the following:

public class MyClass { public String something() {} public static String something() {} } 
Enter fullscreen mode Exit fullscreen mode

When the above class is compiled, an error message complaining that method static void something() is already defined.

$ echo "public class MyClass{\n public String something(){}\n public static String something(){}\n}" > /tmp/MyClass.java $ "$JDK11_HOME"/bin/javac /tmp/MyClass.java /tmp/MyClass.java:3: error: method something() is already defined in class MyClass public static String something() {} ^ 1 error 
Enter fullscreen mode Exit fullscreen mode

Java 8 updated the language to permit interfaces to have static methods, so we can update our API such that it has a concrete class with an instance and a static method that use the same name and descriptor. I would not say this is a common thing to see in an API, but I have seen it. In fact, this post is based on my experience with using one :).

Let's refactor the above class to use interfaces to give the impression that it has two methods with the same name and descriptor. One interface will define String something(); another interface will define static String something(); and a concrete class will implement the two interfaces. The concrete class will compile because it actually no longer has static String something() as part of its implementation. When calling the static method, it must be through the interface, not the concrete class, because the static method is part of the interface. The code for these three files is below.

// InterfaceWithSomething.java public interface InterfaceWithSomething { String something(); } // InterfaceWithStaticSomething.java public interface InterfaceWithStaticSomething { static String something() { return "Interface's static method"; } } // Implementation.java public class Implementation implements InterfaceWithSomething, InterfaceWithStaticSomething { @Override public String something() { return "Implementation's instance method"; } } 
Enter fullscreen mode Exit fullscreen mode

We will use the above classes as our Java API. Let's get groovy.

Part II: The Groovy App

Since the JVM is our development platform, we can mix JVM languages. Let's use our Java API in some Groovy code.

// GroovyCode.groovy class GroovyCode { static void main(String[] args) { def impl = new Implementation() println impl.something() } } 
Enter fullscreen mode Exit fullscreen mode

Running this produces:

groovy -cp . GroovyCode.groovy Interface's static method 
Enter fullscreen mode Exit fullscreen mode

Cool! Wait, what? We are creating a new instance of Implementation and invoking the instance method String something(). Why is Groovy invoking the static method with the same name, especially since Java does not permit us to invoke Implementation.something() as that method is only part of InterfaceWithStaticSomething?

I actually do not know the answer to this question! What I do know is that there must be ambiguity at the call site, where or what, again, I do not know. The Groovy runtime decides to invoke InterfaceWithStaticSomething.something(). If we use the Groovy Console to inspect the AST and view the bytecode generated from GroovyCode, we see this:

public static varargs main([Ljava/lang/String;)V L0 INVOKESTATIC GroovyCode.$getCallSiteArray ()[Lorg/codehaus/groovy/runtime/callsite/CallSite; ASTORE 1 L1 LINENUMBER 4 L1 ALOAD 1 LDC 0 AALOAD LDC LImplementation;.class INVOKEINTERFACE org/codehaus/groovy/runtime/callsite/CallSite.callConstructor (Ljava/lang/Object;)Ljava/lang/Object; (itf) LDC LImplementation;.class INVOKESTATIC org/codehaus/groovy/runtime/ScriptBytecodeAdapter.castToType (Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object; CHECKCAST Implementation ASTORE 2 L2 ALOAD 2 POP L3 LINENUMBER 5 L3 ALOAD 1 LDC 1 AALOAD LDC LGroovyCode;.class ALOAD 1 LDC 2 AALOAD ALOAD 2 INVOKEINTERFACE org/codehaus/groovy/runtime/callsite/CallSite.call (Ljava/lang/Object;)Ljava/lang/Object; (itf) INVOKEINTERFACE org/codehaus/groovy/runtime/callsite/CallSite.callStatic (Ljava/lang/Class;Ljava/lang/Object;)Ljava/lang/Object; (itf) POP L4 LINENUMBER 6 L4 RETURN LOCALVARIABLE args [Ljava/lang/String; L0 L4 0 LOCALVARIABLE impl LImplementation; L2 L4 2 MAXSTACK = 4 MAXLOCALS = 3 
Enter fullscreen mode Exit fullscreen mode

Near the bottom of L3 we see the INVOKEINTERFACE instruction being used twice. At both occurrences, it is calling an interface method on CallSite, which is implemented with Groovy meta code. These two instructions dynamically invoke String something() (on the Implementation object) and println.

Ultimately, whatever the ambiguity is that is causing CallSite to select the static method over the instance method, we need to remove it. Instead of relying on the "dynamic sauce" of Groovy's runtime, we need to break through it. We need the rigid static compilation that Java gives us. We need @CompileStatic!

Part III: @CompileStatic

Here is what Groovy's documentation says about @CompileStatic.

This will let the Groovy compiler use compile time checks in the style of Java then perform static compilation, thus bypassing the Groovy meta object protocol.

When a class is annotated, all methods, properties, files, inner classes, etc. of the annotated class will be type checked. When a method is annotated, static compilation applies only to items (closures and anonymous inner clsses [sic]) within the method.

Source: https://docs.groovy-lang.org/2.4.2/html/gapi/groovy/transform/CompileStatic.html

This is exactly what we need, and the really nice thing is that we only need to annotate the method main(String[]). Let's do that and see what happens.

First, we annotate what we want to be statically compiled, which is GroovyCode's main(String[]):

class GroovyCode { @groovy.transform.CompileStatic static void main(String[] args) { Implementation impl = new Implementation() println impl.something() } } 
Enter fullscreen mode Exit fullscreen mode

Next, let's run the Groovy code:

groovy -cp . GroovyCode.groovy Implementation's static method 
Enter fullscreen mode Exit fullscreen mode

Excellent. We are now calling the method of our Java API that we originally set out to call.

Lastly, let's look at the bytecode using the Groovy Console again:

public static varargs main([Ljava/lang/String;)V L0 LINENUMBER 4 L0 NEW Implementation DUP INVOKESPECIAL Implementation.<init> ()V ASTORE 1 L1 ALOAD 1 POP L2 LINENUMBER 5 L2 LDC LGroovyCode;.class ALOAD 1 INVOKEVIRTUAL Implementation.something ()Ljava/lang/String; INVOKESTATIC org/codehaus/groovy/runtime/DefaultGroovyMethods.println (Ljava/lang/Object;Ljava/lang/Object;)V ACONST_NULL POP L3 LINENUMBER 6 L3 RETURN LOCALVARIABLE args [Ljava/lang/String; L0 L3 0 LOCALVARIABLE impl LImplementation; L1 L3 1 MAXSTACK = 2 MAXLOCALS = 2 
Enter fullscreen mode Exit fullscreen mode

We can see there is less bytecode generated. This makes sense as static compilation removes the runtime metaprogramming. Also, we can see that the two INVOKEINTERFACE instructions we noticed before have been replaced. The first is now INVOKEVIRTUAL and invokes Implementation.something() -- the API call we have been after this whole time. The second is now INVOKESTATIC and invokes the println method. Both of which are more efficient that hopping through the call site metaprogramming that was there before.

Conclusion

Sometimes you need to cut through dynamic invocation magic to ensure what you intend to happen actually happens. The example discussed in this post was attempting to invoke an instance method that had the same name as a static method provided by a Java Interface. Groovy's runtime metaprogramming invoked the static method instead of the instance method, even though the call site looked unambiguous. We corrected the behavior of the Groovy code by using the @CompileStatic annotation to statically compile the call site.

Fin.


  1. https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.6 

Top comments (0)