5
votes

I was trying to create the following class:

class MyClass {

    var foos: List<Foo> = listOf()

    constructor(foos: List<Foo>) {
        this.foos = foos
    }

    constructor(bars: List<Bar>) : super() {
        this.foos = bars.map { bar ->
            Foo(bar)
        }
    }
}


However, I get an error saying:

Platform declaration clash: The following declarations have the same JVM signature ( (Ljava/util/List;)V):


I understand that they are both List objects, but they are typed with generics so I was sure it would not be a problem.

3
They both erase to List. Look up "type erasure in Java" to learn more.Sweeper

3 Answers

7
votes

You encounter this problem because in java there exists something called type erasure. And because kotlin uses the JVM it is also affected by this limitation. To give a TL;DR;

The generic type is retained in the .class file so java knows that the class (in your case List) is generic. But it can't keep track of the generic type of an instance. So instances List<Foo> and List<Bar> are both handled in their raw type form at runtime (List). Keep in mind that generics are only used at compile time to ensure type safety.

To overcome this limitation, you can make use of operator overloading in kotlin. The operator we're looking at is () which let's you invoke any instance. By using a companion object we can even make this invoke look like a constructor, and be invoked like one (MyClass()). Your code can look like this:

class MyClass(var foos: List<Foo>) {
    companion object {
        operator fun invoke(bars: List<Bar>) = MyClass(bars.map(::Foo))
    }
}

Which allows you to call it simply like this:

val mc1 = MyClass(foos) // calls constructor
val mc2 = MyClass(bars) // calls companion.invoke
2
votes

This is not a Kotlin problem, but rather a mechanism on Java generics. This mechanism is intended to avoid conflicts in legacy code that still uses raw types.

For example, if you create a class in Java like this one:

public class MyClass {

    private List<Foo> foos;

    MyClass(List<Foo> foos) {
        this.foos = foos;
    }

    MyClass(List<Bar> bars) {
        List<Foo> foos = new ArrayList<>();
        bars.forEach(bar -> foos.add(new Foo(bar)));
        this.foos = foos;
    }
}


You will get a compile time error saying:

'MyClass(List)' clases with 'MyClass(List)'; both methods have the same erasure


What type erasure does is enforcing type constraints only at compile time and discarding the element type information at runtime. Type parameters on the class are discarded during code compilation and replaced with its first bound or Object if the type parameter is unbound. Therefore, both constructors (since they're both unbounded) will be of List, hence the error.

1
votes

As others mentioned, this is due to Java's type erasure. In some situations the companion object method works but in others not so much.

One workaround I use which works if you only have a small number of constructors clashing is take advantage of Java's inheritance.

So for example, you could do this:

class MyClass {
    constructor(foos: Collection<Foo>) {
        this.myObjects = parseFoos(foos)
    }

    constructor(bars: List<Bar>) {
        this.instructions = parseBars(bars)
    }

    constructor(bazs: ArrayList<Baz>) {
        this.instructions = parseBazs(bazs)
    }
}

Note the argument changes (Collection, List, ArrayList). Your error should be gone and you should be able to pass in ArrayList objects of either Foo, Bar or Baz and Java will call the correct constructor.

Again, you can only do this for a small number of constructors and if you're happy to make some adjustments to the types of arguments you call the constructors with (e.g. declare them as ArrayList instead of List).