Could not find matching constructor for anonymous class

Viewed 761

Consider the following code sample

class A {
  int data
}


class B extends A {}
def o1 = new B(data: 1)
// This works correctly.


def o2 = new A(data:1) {}
// This will throw the following error
// Exception thrown
//
// groovy.lang.GroovyRuntimeException: Could not find matching constructor for: A(LinkedHashMap)
//  at ConsoleScript2$1.<init>(ConsoleScript2)
//  at ConsoleScript2.run(ConsoleScript2:11)
//  at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
//  at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
//  at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

To me, the anonymous one should be the same as the named class. But it turns out that Groovy treats them differently. I want to know how to fix it. Thank you.

1 Answers

You see this error because of the nature of the dynamic map constructor - it is not added explicitly to the generated classes, but it is called through the CallSite.callConstructor(obj,map) method instead. However, there is a solution to that problem.

Consider the following exemplary test.groovy script:

class A {
    int data
}

class B extends A {}

def a1 = new B(data: 1)
def a2 = new A(data: 2) {}

println a1
println a2

When you decompile generated A.class file, you will something like this:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import groovy.transform.Generated;
import groovy.transform.Internal;
import java.beans.Transient;
import org.codehaus.groovy.runtime.callsite.CallSite;

public class A implements GroovyObject {
    private int data;

    @Generated
    public A() {
        CallSite[] var1 = $getCallSiteArray();
        super();
        MetaClass var2 = this.$getStaticMetaClass();
        this.metaClass = var2;
    }

    @Generated
    @Internal
    @Transient
    public MetaClass getMetaClass() {
        MetaClass var10000 = this.metaClass;
        if (var10000 != null) {
            return var10000;
        } else {
            this.metaClass = this.$getStaticMetaClass();
            return this.metaClass;
        }
    }

    @Generated
    @Internal
    public void setMetaClass(MetaClass var1) {
        this.metaClass = var1;
    }

    @Generated
    public int getData() {
        return this.data;
    }

    @Generated
    public void setData(int var1) {
        this.data = var1;
    }
}

This class has only one no-args constructor. When you decompile the test.class file (compiled Groovy script file), you will see something like this:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import groovy.lang.Binding;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;

public class test extends Script {
    public test() {
        CallSite[] var1 = $getCallSiteArray();
        super();
    }

    public test(Binding context) {
        CallSite[] var2 = $getCallSiteArray();
        super(context);
    }

    public static void main(String... args) {
        CallSite[] var1 = $getCallSiteArray();
        var1[0].call(InvokerHelper.class, test.class, args);
    }

    public Object run() {
        CallSite[] var1 = $getCallSiteArray();
        Object a1 = var1[1].callConstructor(B.class, ScriptBytecodeAdapter.createMap(new Object[]{"data", 1}));
        Object a2 = new test.1(ScriptBytecodeAdapter.createMap(new Object[]{"data", 2}));
        var1[2].callCurrent(this, a1);
        return var1[3].callCurrent(this, a2);
    }

    public class 1 extends A {
    }
}

Take a look at how objects a1 and a2 are initialized. The a1 object is initialized in the following way:

Object a1 = var1[1].callConstructor(B.class, ScriptBytecodeAdapter.createMap(new Object[]{"data", 1}));

It uses the CallSite.callConstructor() method to mimic the map constructor which does not exist in the A class. If we look at how the object a2 is initialized we will find this:

Object a2 = new test.1(ScriptBytecodeAdapter.createMap(new Object[]{"data", 2}));

We can see that Groovy in the case of the anonymous class (which is not anonymous at all - Groovy generates a class anyway), Groovy uses a direct constructor call. And it fails, because there is no A(LinkedHashMap) constructor in the parent class.

Solution

Luckily, there is a solution to this problem - you can use @MapConstructor and @InheritConstructors annotations to force creating map constructor in the A class, and to inherit this constructor in the B class. Take a look at this working example:

import groovy.transform.InheritConstructors
import groovy.transform.MapConstructor

@MapConstructor
class A {
    int data
}

@InheritConstructors
class B extends A {}

def a1 = new B(data: 1)
def a2 = new A(data: 2) {}

println a1
println a2

The only requirement is to use at least the Groovy 2.5 version which introduced the @MapConstructor annotation.

Related