Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Groovy's @CompileStatic and map constructors

Tags:

groovy

I'm using @CompileStatic for the first time, and confused as to how Groovy's map constructors work in this situation.

@CompileStatic
class SomeClass {
    Long id
    String name

    public static void main(String[] args) {
        Map map = new HashMap()
        map.put("id", 123L)
        map.put("name", "test file")
        SomeClass someClass1 = new SomeClass(map) // Does not work
        SomeClass someClass2 = map as SomeClass   // Works
    }
}

Given the code above I see the following error when trying to compile

Groovyc: Target constructor for constructor call expression hasn't been set

If @CompileStatic is removed, both constructors work properly.

Can anyone explain why new SomeClass(map) does not compile with @CompileStatic? And a possible addition, why does map as SomeClass still work?

like image 357
mnd Avatar asked Jun 10 '15 21:06

mnd


2 Answers

Groovy actually does not give you a "Map-Constructor". The constructors in your class are what you write down. If there are none (like in your case), then there is the default c'tor.

But what happens, if you use the so called map c'tor (or rather call it "object construction by map")? The general approach of groovy is like this:

  • create a new object using the default c'tor (this is the reason, why the construction-by-map no longer works, if there would be just e.g. SomeClass(Long id, String name))
  • then use the passed down map and apply all values to the properties.

If you disassmble your code (with @CompileDynamic (the default)) you see, that the construction is handled by CallSite.callConstructor(Object,Object), which boils down to this this code area.

Now bring in the version of this construction by map, that is more familiar for the regular groovyist: SomeClass someClass3 = new SomeClass(id: 42L, name: "Douglas").

With the dynamic version of the code, the disassembly of this looks actually alot like your code with the map. Groovy creates a map from the param(s) and sends it off to callConstructor - so this is actually the same code path taken (minus the implicit map creation).

For now ignore the "cast-case", as it is actually the same for both static and dynamic: it will be sent to ScriptBytecodeAdapter.asType which basically gives you the dynamic behaviour in any case.

Now the @CompileStatic case: As you have witnessed, your call with an explicit map for the c'tor no longer works. This is due to the fact, that there never was an explicit "map-c'tor" in the first place. The class still only has its default c'tor and with static compilation groovyc now can just work with the things that are there (or not if there aren't in this case).

What about new SomeClass(id: 42L, name: "Douglas") then? This still works with static compilation! The reason for this is, that groovyc unrolls this for you. As you can see, this simply boils down to def o = new SomeClass(); o.setId(42); o.setName('Douglas'):

new           #2  // class SomeClass
dup
invokespecial #53 // Method "<init>":()V
astore_2
ldc2_w        #54 // long 42l
dup2
lstore_3
aload_2
lload_3
invokestatic  #45 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
invokevirtual #59 // Method setId:(Ljava/lang/Long;)V
aconst_null
pop
pop2
ldc           #61 // String Douglas
dup
astore        5
aload_2
aload         5
invokevirtual #65 // Method setName:(Ljava/lang/String;)V
like image 172
cfrick Avatar answered Nov 11 '22 21:11

cfrick


As the CompileStatic documentation says:

will actually make sure that the methods which are inferred as being called will effectively be called at runtime. This annotation turns the Groovy compiler into a static compiler, where all method calls are resolved at compile time and the generated bytecode makes sure that this happens

As a result, a constructor with a Map argument is searched in the static compilation to "resolve it at compile time", but it is not found and thereby there is a compilation error:

Target constructor for constructor call expression hasn't been set

Adding such a constructor solves the issue with the @CompileStatic annotation, since it is resolved at compile time:

import groovy.transform.CompileStatic

@CompileStatic
class SomeClass {
    Long id
    String name

    SomeClass(Map m) {
        id = m.id as Long
        name = m.name as String
    }

    public static void main(String[] args) {
        Map map = new HashMap()
        map.put("id", 123L)
        map.put("name", "test file")
        SomeClass someClass1 = new SomeClass(map) // Now it works also
        SomeClass someClass2 = map as SomeClass   // Works
    }
}

You can check StaticCompilationVisitor if you want to dig deeper.

Regarding the line

SomeClass someClass2 = map as SomeClass

You are using there the asType() method of Groovy's GDK java.util.Map, so it is therefore solved at runtime even in static compilation:

Coerces this map to the given type, using the map's keys as the public method names, and values as the implementation. Typically the value would be a closure which behaves like the method implementation.

like image 3
jalopaba Avatar answered Nov 11 '22 21:11

jalopaba