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?
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:
SomeClass(Long id, String name)
)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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With