Elliot Nelson
Elliot Nelson

Reputation: 11557

How to reference subclass from a static superclass method in Groovy

A simplified version of what I'm trying to do in Groovy:

class Animal {
    static def echo() {
        println this.name  // ie "class.name"
    }
}

class Dog extends Animal {
}

class Cat extends Animal {
}

Dog.echo()
Cat.echo()

// Output:
//  => Animal
//  => Animal
//
// What I want:
//  => Dog
//  => Cat

I think what I'm asking here is: when I call a static method on an object, and the static method is defined in the object's superclass, is there a way to obtain the actual type of the object?

Upvotes: 2

Views: 1139

Answers (1)

Szymon Stepniak
Szymon Stepniak

Reputation: 42262

A static method is not defined in the object context, but in the class context. You might get confused by the presence of this in the Groovy static method. However, it's only a syntactic sugar that eventually replaces this.name with Animal.class.name.

If you compile the Animal class from your example with a static compilation enabled, you will see that it compiles to the following Java equivalent (result after decompiling the .class file):

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

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;

public class Animal implements GroovyObject {
    public Animal() {
        MetaClass var1 = this.$getStaticMetaClass();
        this.metaClass = var1;
    }

    public static Object echo() {
        DefaultGroovyMethods.println(Animal.class, Animal.class.getName());
        return null;
    }
}

You can see that the following line in the echo method:

DefaultGroovyMethods.println(Animal.class, Animal.class.getName());

operates directly on the Animal class name. So from the echo method perspective, it doesn't matter how many classes extend it. As long as those classes invoke echo method defined in the Animal class, you will always see Animal printed as a result.

And there is even more than that. If you use the following compiler configuration script:

config.groovy

withConfig(configuration) {
    ast(groovy.transform.CompileStatic)
    ast(groovy.transform.TypeChecked)
}

and then compile the script (let's call it script.groovy) using this configuration option with the following command:

groovyc --configscript=config.groovy script.groovy

then you will see something like this after decompiling the .class file:

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

import groovy.lang.Binding;
import org.codehaus.groovy.runtime.InvokerHelper;

public class script extends groovy.lang.Script {
    public script() {
    }

    public script(Binding context) {
        super(context);
    }

    public static void main(String... args) {
        InvokerHelper.runScript(script.class, args);
    }

    public Object run() {
        Animal.echo();
        return Animal.echo();
    }
}

You can see that even though you have invoked Dog.echo() and Cat.echo() in your Groovy script, the compiler replaced these calls with the double Animal.echo() invocation. It happened because calling this static method on any other subclass does not make any difference.

Possible solution: applying double dispatch

There is one way to get the expected output - override echo static method in Dog and Cat class. I can assume that your real method may do something more than the exemplary echo method you have shown above, so you might need to call the super echo method from a parent class. But... there are two problems: (1) you can't use super.echo() in the static context, and (2) it doesn't solve the problem, because parent method still operates in the Animal class context.'

To solve this kind of issue you might want to mimic a technique called double dispatch. In short - when we don't have information about the caller in the method that was called, let's allow the caller to pass this information with the method call. Consider the following example:

import groovy.transform.CompileStatic

@CompileStatic
class Animal {
    // This is a replacement for the previous echo() method - this one knows the animal type from a parameter
    protected static void echo(Class<? extends Animal> clazz) {
        println clazz.name
    }

    static void echo() {
        echo(Animal)
    }
}

@CompileStatic
class Dog extends Animal {
    static void echo() {
        echo(Dog)
    }
}

@CompileStatic
class Cat extends Animal {
    static void echo() {
        echo(Cat)
    }
}

Animal.echo()
Dog.echo()
Cat.echo()

This may sound like a boilerplate solution - it requires implementing echo method in each subclass. However, it encapsulates the echo logic in the method that requires Class<? extends Animal> parameter, so we can let every subclass to introduce their concrete subtype. Of course, this is not a perfect solution. It requires implementing echo method in each subclass, but there is no other alternative way. Another problem is that it doesn't stop you from calling Dog.echo(Animal) which will cause the same effect as calling Animal.echo(). This double dispatch like approach is more like introducing a shorthand version of echo method which uses the common static echo method implementation for simplicity.

I don't know if this kind of approach solves your problem, but maybe it will help you find a final solution.

Upvotes: 3

Related Questions