Kotlin допускает ту же сигнатуру функции, что и получатель свойств, но с другим типом возврата

Обновление 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, мы не сможем посетить параметры карты, пока не приведем экземпляр к его суперклассу. И это может привести к непониманию.

Обратите внимание, что ограничение, согласно которому два метода с одинаковыми именами, но разными типами возвращаемых значений не могут существовать одновременно, является ограничением языка Java. JVM прекрасно способна различать их (фактически, реализация ковариантных возвращаемых типов в Java использует это).

Slaw 23.12.2020 10:14

Чтобы продолжить, Kotlin — это язык, отличный от Java. Это означает, что он не обязательно должен следовать одним и тем же правилам, даже если он компилируется в байтовый код при работе с JVM. Обратите внимание, что формат байт-кода определяется спецификацией JVM, а не спецификацией Java.

Slaw 23.12.2020 10:33

@Slaw Спасибо за ответ. Это кратко, но очень полезно.

Kyle Zhang 23.12.2020 10:49
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
3
703
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Есть разница между языком 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, а не проблема.

Kyle Zhang 23.12.2020 11:07

Другие вопросы по теме