Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin allows the same function signature as property getter with different return type

Tags:

kotlin

Update 2020-12-23

The origin description is a little bit confusing. Kotlin not just allows same signature with getter in sub-class but also in self class too. So this is also allowed:

open class BaseRequest {
    val params = mutableMapOf<String, String>()
    fun getParams(): List<String> {
        return params.values.toList()
    }
}

As @Slaw said, this is the behaviour of kotlin compilor and it works since JVM invoke the correct method using address but not "the signature".


I run into a situation that seems Kotlin allows sub-class create the same signature as super-class’s getter.

Generally, functions has same signature and different return type is not allowed. So I’m confused about this situation. I’m not sure whether this is by designed.

Here is a sample:

open class BaseRequest {
    val params = mutableMapOf<String, String>()

    init {
        params["key1"] = "value1"
    }
}

class SpecificRequest : BaseRequest() {
    init {
        params["key2"] = "value2"
    }

    fun getParams(): List<String> {
        return params.values.toList()
    }
}

MediatorRequest has a function getParams() which has same signature as it’s super-class but has different return type. While using this function, it seems the sub class and super class has different implement of the same declaration.

fun main() {
    val specificRequest = SpecificRequest()
    println("specificRequest.params: ${specificRequest.params}")
    println("specificRequest.getParams(): ${specificRequest.getParams()}")
    println("(specificRequest as BaseRequest).params: ${(specificRequest as BaseRequest).params}")
}

The Output would be like this:

specificRequest.params: {key1=value1, key2=value2}
specificRequest.getParams(): [value1, value2]
(specificRequest as BaseRequest).params: {key1=value1, key2=value2}

If we look at the decompiled Java code, there are two methods has same signature and different return type and this is truly not allowed in Java.

public class BaseRequest {
   @NotNull
   private final Map params;

   @NotNull
   public final Map getParams() {
      return this.params;
   }

   /* ... */
}


public final class SpecificRequest extends BaseRequest {
   @NotNull
   public final List getParams() {
      return CollectionsKt.toList((Iterable)this.getParams().values());
   }
   /* ... */
}

I know the function name is not appropriate but there is a potential risk that if we use the SpecificRequest in .java, we can not visite the Map params until we cast the instance to it’s super class. And this may lead to misunderstanding.

like image 476
Kyle Zhang Avatar asked Dec 23 '20 08:12

Kyle Zhang


Video Answer


1 Answers

There is a difference between Java the language and the JVM. The Java language does not allow two methods with the same name but different return types to be declared in the same class. This is a restriction of the language. The JVM, however, is perfectly capable of distinguishing between the two methods. And since Kotlin is its own language it does not necessarily have to follow the exact same rules as Java—even when targeting the JVM (and thus compiled to byte-code).

Consider the following Kotlin class:

class Foo {
    val bar = mapOf<Any, Any>()
    fun getBar() = listOf<Any>()
}

If you compile the class and then inspect the byte-code with javap you'll see:

Compiled from "Foo.kt"
public final class Foo {
  public final java.util.Map<java.lang.Object, java.lang.Object> getBar();
  public final java.util.List<java.lang.Object> getBar();
  public Foo();
}

So the two functions definitely exist, despite having the same name. But if you access the property and call the function you'll see that:

fun test() {
    val foo = Foo()
    val bar1 = foo.bar
    val bar2 = foo.getBar()
}

Becomes:

 public static final void test();
    descriptor: ()V
    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=3, args_size=0
         0: new           #8                  // class Foo
         3: dup
         4: invokespecial #11                 // Method Foo."<init>":()V
         7: astore_0
         8: aload_0
         9: invokevirtual #15                 // Method Foo.getBar:()Ljava/util/Map;
        12: astore_1
        13: aload_0
        14: invokevirtual #18                 // Method Foo.getBar:()Ljava/util/List;
        17: astore_2
        18: return

Which shows the byte-code knows which function to call. And the JVM can handle this.

But there is a caveat. The following will fail to compile:

class Foo {
    fun getBaz() = mapOf<Any, Any>()
    fun getBaz() = listOf<Any>()
}

Why? I'm not positive, but I believe it has to do with the syntax. The Kotlin compiler can always easily tell which function you meant to invoke based on if you used foo.bar or foo.getBar(). But the syntax is the same for calling the two getBaz() functions, which means the compiler can't easily tell which function you meant to invoke in all cases (and so it disallows the above).

like image 179
Slaw Avatar answered Oct 24 '22 03:10

Slaw