Обновление 2020-12-23
Описание происхождения немного сбивает с толку. Kotlin не только допускает одну и ту же подпись с геттером в подклассе, но и в собственном классе. Итак, это также разрешено:
open class BaseRequest {
val params = mutableMapOf<String, String>()
fun getParams(): List<String> {
return params.values.toList()
}
}
Как сказал @Slaw, это поведение компилятора kotlin, и оно работает, поскольку JVM вызывает правильный метод, используя адрес, но не «подпись».
Я столкнулся с ситуацией, когда кажется, что Kotlin позволяет подклассу создавать ту же подпись, что и геттер суперкласса.
Как правило, функции имеют одинаковую сигнатуру, и разные типы возврата не допускаются. Так что я в недоумении от этой ситуации. Я не уверен, что это задумано.
Вот пример:
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 есть функция getParams()
, которая имеет ту же сигнатуру, что и суперкласс, но имеет другой тип возвращаемого значения. При использовании этой функции кажется, что подкласс и суперкласс имеют разные реализации одного и того же объявления.
fun main() {
val specificRequest = SpecificRequest()
println("specificRequest.params: ${specificRequest.params}")
println("specificRequest.getParams(): ${specificRequest.getParams()}")
println("(specificRequest as BaseRequest).params: ${(specificRequest as BaseRequest).params}")
}
Вывод будет таким:
specificRequest.params: {key1=value1, key2=value2}
specificRequest.getParams(): [value1, value2]
(specificRequest as BaseRequest).params: {key1=value1, key2=value2}
Если мы посмотрим на декомпилированный код Java, то увидим, что два метода имеют одинаковую сигнатуру и разные типы возвращаемого значения, и это действительно не разрешено в 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());
}
/* ... */
}
Я знаю, что имя функции не подходит, но существует потенциальный риск того, что если мы используем SpecificRequest в .java, мы не сможем посетить параметры карты, пока не приведем экземпляр к его суперклассу. И это может привести к непониманию.
Чтобы продолжить, Kotlin — это язык, отличный от Java. Это означает, что он не обязательно должен следовать одним и тем же правилам, даже если он компилируется в байтовый код при работе с JVM. Обратите внимание, что формат байт-кода определяется спецификацией JVM, а не спецификацией Java.
@Slaw Спасибо за ответ. Это кратко, но очень полезно.
Есть разница между языком Java и JVM. Язык Java не позволяет объявлять в одном классе два метода с одинаковыми именами, но разными типами возвращаемого значения. Это ограничение языка. Однако JVM прекрасно различает эти два метода. А поскольку Kotlin — это отдельный язык, он не обязательно должен следовать тем же правилам, что и Java — даже при работе с JVM (и, следовательно, при компиляции в байт-код).
Рассмотрим следующий класс Kotlin:
class Foo {
val bar = mapOf<Any, Any>()
fun getBar() = listOf<Any>()
}
Если вы скомпилируете класс, а затем проверите байт-код с помощью javap
, вы увидите:
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();
}
Таким образом, две функции определенно существуют, несмотря на то, что они имеют одно и то же имя. Но если вы получите доступ к свойству и вызовете функцию, вы увидите, что:
fun test() {
val foo = Foo()
val bar1 = foo.bar
val bar2 = foo.getBar()
}
Становится:
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
Что показывает, что байт-код знает, какую функцию вызывать. И JVM может справиться с этим.
Но есть предостережение. Следующее не будет компилироваться:
class Foo {
fun getBaz() = mapOf<Any, Any>()
fun getBaz() = listOf<Any>()
}
Почему? Я не уверен, но я считаю, что это связано с синтаксисом. Компилятор Kotlin всегда может легко сказать, какую функцию вы хотели вызвать, основываясь на том, использовали ли вы foo.bar
или foo.getBar()
. Но синтаксис один и тот же для вызова двух функций getBaz()
, а это означает, что компилятор не может легко определить, какую функцию вы хотели вызвать во всех случаях (и поэтому он запрещает вышеперечисленное).
Спасибо за Ваш ответ. Я думал, что вы правы, это зависит от компилятора. И даже kotlin позволяет, кроме fun getBar(): List<Any>
, var bar = mapOf<Any, Any>()
, мы не можем вызывать Foo.getBar()
в Java-коде (вызов метода Got Ambiguous.). Так что это различие между компилятором Java и kotlin, а не проблема.
Обратите внимание, что ограничение, согласно которому два метода с одинаковыми именами, но разными типами возвращаемых значений не могут существовать одновременно, является ограничением языка Java. JVM прекрасно способна различать их (фактически, реализация ковариантных возвращаемых типов в Java использует это).