How does erasure work in Kotlin?

2020-01-31 02:42发布

问题:

In Kotlin, the following code compiles:

class Foo {
    fun bar(foo: List<String>): String {
        return ""
    }

    fun bar(foo: List<Int>): Int {
        return 2;
    }
}

This code, however, does not:

class Foo {
    fun bar(foo: List<String>): String {
        return ""
    }

    fun bar(foo: List<Int>): String {
        return "2";
    }
}

Compiling this will cause the following error:

Error:(8, 5) Kotlin: Platform declaration clash: The following declarations have the same JVM signature (foo(Ljava/util/List;)Ljava/lang/String;):
    fun foo(layout: List<Int>): String
    fun foo(layout: List<String>): String

In Java, neither example will compile:

class Foo {
    String bar(List<Integer> foo) {
        return "";
    }

    Integer bar(List<String> foo) {
        return 2;
    }
}

class Foo {
    String bar(List<Integer> foo) {
        return "";
    }

    String bar(List<String> foo) {
        return "2";
    }
}

Unsurprisingly, both of the prior snippets generate the familiar compiler error:

Error:(13, 12) java: name clash: bar(java.util.List<java.lang.String>) and bar(java.util.List<java.lang.Integer>) have the same erasure

What surprises me is that the first Kotlin example works at all, and second, if it works, why does the second Kotlin example fail? Does Kotlin consider a method's return type as part of its signature? Furthermore, why do method signatures in Kotlin respect the full parameter type, in contrast with Java?

回答1:

Actually Kotlin knows the difference between the two methods in your example, but jvm will not. That's why it's a "platform" clash.

You can make your second example compile by using the @JvmName annotation:

class Foo {
  @JvmName("barString") fun bar(foo: List<String>): String {
    return ""
  }

  @JvmName("barInt") fun bar(foo: List<Int>): String {
    return "2";
  }
}

This annotation exists for this very reason. You can read more in the interop documentation.



回答2:

While @Streloks answer is correct, I wanted to dig deeper regarding why it works.

The reason why the first variant works, is that it is not prohibited within the Java Byte code. While the Java compiler complains about it, i.e. the Java language specification does not allow it, the Byte code does, as was also documented in https://community.oracle.com/docs/DOC-983207 and in https://www.infoq.com/articles/Java-Bytecode-Bending-the-Rules. In the Byte code every method call refers the actual return type of the method, which isn't that way when you write the code.

Unfortunately I couldn't find the actual source, why it is that way.

The document regarding Kotlins name resolution contains some interesting points, but I did not see your actual case there.

What really helped me understand it, was the answer from @Yole to Kotlin type erasure - why are functions differing only in generic type compilable while those only differing in return type are not?, more precisely that the kotlin compiler will not take the type of the variable into account when deciding which method to call.

So, it was a deliberate design decision that specifying the type on a variable will not influence which method is the one to be called but rather the other way around, i.e. the called method (with or without generic information) influences the type to be used.

Applying the rule on the following samples then makes sense:

fun bar(foo: List<String>) = ""    (1)
fun bar(foo: List<Int>) = 2        (2)

val x = bar(listOf("")) --> uses (1), type of x becomes String
val y = bar(listOf(2))  --> uses (2), type of y becomes Int

Or having a method supplying a generic type but not even using it:

fun bar(foo: List<*>) = ""         (3)
fun <T> bar(foo: List<*>) = 2      (4)

val x = bar(listOf(null))          --> uses (3) as no generic type was specified when calling the method, type of x becomes String
val y = bar<String>(listOf(null))  --> uses (4) as the generic type was specified, type of y becomes Int

And that's also the reason why the following will not work:

fun bar(foo: List<*>) = ""
fun bar(foo: List<*>) = 2

This is not compilable as it leads to a conflicting overload as the type of the assigned variable itself is not taken into consideration when trying to identify the method to be called:

val x : String = bar(listOf(null)) // ambiguous, type of x is not relevant

Now regarding that name clash: as soon as you use the same name, the same return type and the same parameters (whose generic types are erased), you will actually get the very same method signature in the byte code. That's why @JvmName becomes necessary. With that you actually ensure that there are no name clashes in the byte code.



标签: kotlin