clayweidinger
clayweidinger

Reputation: 103

java.lang.UnsupportedOperationException when using Groovy's Constructor Coersion

Why can't I Coerce my map into a constructor for IStringHolder?

When I run Example.main

interface IStringHolder {
    String getString()
}

class StringHolder implements IStringHolder {
    String string

    StringHolder(string) {
        this.string = string
    }

    @Override
    String getString() {
        return string
    }
}

class Example {
    public static void main(String[] args) {
        doSomething([string:"hey"] as IStringHolder)

    }

    static void doSomething(IStringHolder stringHolder) {
        println stringHolder.getString()
    }

}

I get the following exception

Exception in thread "main" java.lang.UnsupportedOperationException
    at org.codehaus.groovy.runtime.ConvertedMap.invokeCustom(ConvertedMap.java:52)
    at org.codehaus.groovy.runtime.ConversionHandler.invoke(ConversionHandler.java:124)
    at com.sun.proxy.$Proxy0.getString(Unknown Source)
    at IStringHolder$getString.call(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:117)
    at Example.doSomething(Example.groovy:26)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
    at org.codehaus.groovy.runtime.callsite.StaticMetaMethodSite.invoke(StaticMetaMethodSite.java:46)
    at org.codehaus.groovy.runtime.callsite.StaticMetaMethodSite.callStatic(StaticMetaMethodSite.java:102)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallStatic(CallSiteArray.java:56)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:194)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:206)
    at Example.main(Example.groovy:21)

Why is the exception coming from the first line in Example.doSomething?

When I coerce the map to StringHolder then it works. What is going on?

Upvotes: 0

Views: 4064

Answers (1)

AnotherDeveloper
AnotherDeveloper

Reputation: 111

The Problem

The problem with this code is this line:

doSomething([string:"hey"] as IStringHolder)

The language feature of Groovy you're attempting to leverage here is called Explicit Constructor Coercion, which looks for a constructor within the IStringHolder class that it can call with the arguments you provided.

In this case, Groovy will look within the IStringHolder class for a no-arg constructor it can call to instantiate it, then it will look for a setter setString(String str) and call it with the "hey" you provided.

The reason you're seeing UnsupportedOperationException is because there is no constructor in IStringHolder to call, because it's not an implementation.

Switching the type coercion to as StringHolder does work, but probably not as you're expecting. Had you provided simply "hey", instead of string:"hey", it would have done what you were expecting and what I detailed above. This is because the map is interpreted as a Named Argument Constructor, which causes Groovy to instantiate your object using the only constructor it can find, passing null into it, and then calling the synthetic setString(String str) method with "hey" once it has been constructed. While this yields the result you're looking for, it does it in a non-intuitive way.

Solutions

Explicit Constructor Coercion

StringHolder stringHolderImpl = [ "hey" ] as StringHolder

Here Groovy will look for a constructor that takes one-arg of type String and pass our value "hey" into it.


Implicit Constructor Coercion

StringHolder stringHolder = [ "hey" ]

This is the same as above, except Groovy is using the left-side type to determine what the right-side probably is, before it proceeds with finding the constructor, etc.

I don't recommend this only because it makes Groovy assume too much, and IDEs like Intellij will complain about this syntax.


Explicit Closure Coercion

If you're adamant about not using the implementation class StringHolder you can create an implementation at runtime via:

IStringHolder stringHolderImpl = { getString: "hey" } as IStringHolder

This creates a closure which satisfies the IStringHolder contract, so it can be easily cast to an implementation of it.


Implicit Closure Coercion

IStringHolder stringHolderImpl = { getString: "hey" }

Again I don't really recommend this style, but it works.


My Thoughts

I'm a fan of explicit constructor coercion due to the explicit nature and because it doesn't require the overhead of creating a closure. I'll use explicit closure coercion from time to time when I need an implementation of a class that only needs to exist at runtime.

What's the best option? You'll have to determine that for yourself!


tl;dr

  • You were trying to coerce to an interface instead of an implementation
  • You tried to use a Named Argument Constructor for the coercion when you needed to use a Positional Argument Constructor

Upvotes: 2

Related Questions