How does erasure work in Kotlin?
Asked Answered
P

2

31

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?

Presidentelect answered 21/3, 2017 at 1:19 Comment(1)
In addition to other answers, return types are not added to signature of a method, in Java.Socle
R
24

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.

Ricoriki answered 21/3, 2017 at 2:12 Comment(5)
But then why does the first Kotlin example compile without this annotation? Does the JVM recognize methods with different return types? According to this answer, the return type is not part of a method's signature.Presidentelect
Ah, I see. Perhaps Java does not allow identical name/parameter methods with different return types during compilation, but the JVM recognizes them?Presidentelect
@Presidentelect well yes, the return type is part of the signature, but in the first case it is different for the two methods, (even though the parameter erased type is the same) so the signatures are different. They're foo(Ljava/util/List;)Ljava/lang/String; and foo(Ljava/util/List;)Ljava/lang/Integer; respectively. Java only considers the parameter types and method name in name clash check so the first example in Java is not compiling. This is more about overloading and anot type erasure and can be easily fixed in the language (like Kotlin did)Ricoriki
I don't think Kotlin considers a method's return type as part of its type signature. For example, it will not allow two methods, @JvmName("barInt") fun bar(foo: List<String>): Int and @JvmName("barString") fun bar(foo: List<String>): String to be defined inside the same class, even though their JVM signatures are different (barInt(Ljava/util/List;)Ljava/lang/Integer; and barString(Ljava/util/List;)Ljava/lang/String; respectively). However, it will allow @JvmName("barInt") fun bar(foo: List<Int>): Int and @JvmName("barString") fun bar(foo: List<String>): String in the same class.Presidentelect
That's because overload resolution could never choose one of these methods over another (it doesn't take return type into account), and so neither method could ever be called.Valenta
K
5

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.

Krug answered 2/8, 2018 at 12:10 Comment(5)
are you saying that overloaded methods by return type only are allowed in kotlin? cause if you do than this fun go(): String = ""; fun go(): Int = 12; must compile, and it does notCreath
No, I didn't want to say that... I rather meant "the constellation shown in the first sample" with 'it is allowed'... I will reformulate that one.... Kotlin really has a special way to deal with generics... I don't get why the following works: fun go() : String = ""; fun <T> go() : Int = 12... I mean... I do not even use T in the second one... on the other hand... if that is valid, why shouldn't the samples you provided work?Krug
that example with <T> is a question I am working on... :)Creath
adapted my answer... just let me know whether it is clearer nowKrug
I don't have an explanation for it, thus working on the question... probably will post it todayCreath

© 2022 - 2024 — McMap. All rights reserved.