Search code examples
javagenericskotlintype-erasurevisitor-pattern

Java/Kotlin cast exception for visitor pattern with generic return type


I'm trying to use something like the visitor pattern, but with return values.

However, although there are no explicit casts, I'm getting a ClassCastException:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.CharSequence;
    at Printer.combine(...)
    at Split.accept(...)
    at MWEKt.main(...)

Code:

interface TreeElem {
    fun <T> accept(visitor: TreeVisitor<T>): T
}

class Leaf: TreeElem {
    override fun <T> accept(visitor: TreeVisitor<T>): T {
        return visitor.visit(this)
    }
}

class Split(val left: TreeElem, val right: TreeElem): TreeElem {
    override fun <T> accept(visitor: TreeVisitor<T>): T {
        return visitor.combine(  // this causes cast error
            visitor.visit(this),
            left.accept(visitor),
            right.accept(visitor))
    }
}

interface TreeVisitor<T> {
    // multiple implementations with different T in future (only one in this example)
    fun visit(tree: Leaf): T
    fun visit(tree: Split): T
    fun combine(vararg inputs: T): T
}

class Printer: TreeVisitor<CharSequence> {
    override fun combine(vararg inputs: CharSequence): CharSequence { // error here
        return inputs.joinToString(" ")
    }
    override fun visit(tree: Leaf): CharSequence { return "leaf" }
    override fun visit(tree: Split): CharSequence { return "split" }
}

fun main(args: Array<String>) {
    val tree = Split(Leaf(), Leaf())
    val printer = Printer()
    println(tree.accept(printer))
}

I don't know what the problem is. Am I trying to do something impossible, or am I failing to express it correctly, or is type erasure making something that should be possible impossible?

My thoughts so far:

  1. Printer.combine expects CharSequences;
  2. I'm calling a generic overload of TreeElem.accept that returns CharSequence
  3. The compiler probably inserts a cast into the JVM code (type erasure?)
  4. But the runtime types are compatible, so the cast should work

Since the last point is in conflict with realist, I'm probably understanding something incorrectly.

EDIT: I've translated the MWE to Java to see if it's a Kotlin issue and to attract an answer:

interface TreeElem {
    <T> T accept(TreeVisitor<T> visitor);
}

class Leaf implements TreeElem {
    public <T> T accept(TreeVisitor<T> visitor) {
        return visitor.visit(this);
    }
}

class Split implements TreeElem {
    private TreeElem left;
    private TreeElem right;
    Split(TreeElem left, TreeElem right) {
        this.left = left;
        this.right = right;
    }
    public <T> T accept(TreeVisitor<T> visitor) {
        return visitor.combine(
            visitor.visit(this),
            left.accept(visitor),
            right.accept(visitor));
    }
}

interface TreeVisitor<T> {
    T visit(Leaf tree);
    T visit(Split tree);
    T combine(T... inputs);
}

class Printer implements TreeVisitor<CharSequence> {
    public CharSequence combine(CharSequence... inputs) {
        StringBuilder text = new StringBuilder();
        for (CharSequence input : inputs) {
            text.append(input);
        }
        return text;
    }
    public CharSequence visit(Leaf tree) { return "leaf"; }
    public CharSequence visit(Split tree) { return "split"; }
}

public class MWEjava {
    public static void main(String[] args) {
        TreeElem tree = new Split(new Leaf(), new Leaf());
        Printer printer = new Printer();
        System.out.println(tree.accept(printer));
    }
}

The error is the same for the Java case.


Solution

  • I'm pretty sure this is a duplicate of this question: https://stackoverflow.com/a/9058259/4465208

    However, to provide a specific solution, you could replace the vararg argument with a List<T> instead, which will work just fine:

    class Split(val left: TreeElem, val right: TreeElem) : TreeElem {
        override fun <T> accept(visitor: TreeVisitor<T>): T {
            return visitor.combine(listOf(
                    visitor.visit(this),
                    left.accept(visitor),
                    right.accept(visitor)))
        }
    }
    
    interface TreeVisitor<T> {
        fun combine(inputs: List<T>): T
        // ...
    }
    
    class Printer : TreeVisitor<CharSequence> {
        override fun combine(inputs: List<CharSequence>): CharSequence {
            return inputs.joinToString(" ")
        }
        // ...
    }
    

    Not as pretty, but it plays nice with generics.