Любой способ унаследовать один и тот же общий интерфейс дважды (с отдельными типами) в Котлин?

У меня есть сценарий в моем коде, где я хотел бы, чтобы класс реализовал интерфейс для двух отдельных типов, например, этот пример:

interface Speaker<T> {
    fun talk(value: T)
}

class Multilinguist : Speaker<String>, Speaker<Float> {
    override fun talk(value: String) {
        println("greetings")
    }

    override fun talk(value: Float) {
        // Do something fun like transmit it along a serial port
    }
}

Котлин не доволен этим, ссылаясь на:

Type parameter T of 'Speaker' has inconsistent values: kotlin.String, kotlin.Float
A supertype appears twice

Я знаю, что одним из возможных решений является реализация следующего кода, где я реализую интерфейс с <Any>, а затем сам проверяю типы и делегирую их их функциям.

interface Speaker<T> {
    fun talk(value: T)
}

class Multilinguist : Speaker<Any> {
    override fun talk(value: Any) {
        when (value) {
            is String ->
                internalTalk(value)
            is Float ->
                internalTalk(value)
        } 
    }

    fun internalTalk(value: String) {
        println(value)
    }

    fun internalTalk(value: Float) {
        // Do something fun like transmit it along a serial port
    }
}

Тем не менее, это похоже на то, что я удаляю информацию о типе безопасности и сообщении о том, для чего используется класс, и задает проблемы по линии. Есть ли лучший способ реализовать это в Котлине? Кроме того, какие аргументы позади этого не допускаются, как я указал в первом примере? Не являются ли интерфейсы только контрактом подписей, которые мне требуются для реализации, или есть что-то, чего я не вижу, включая дженериков?

Ответ 1

Да, вам не хватает важной детали реализации дженериков на JVM: стирание типа. В двух словах, скомпилированный байт-код классов фактически не содержит никакой информации об общих типах (за исключением некоторых метаданных о том, что класс или метод является общим). Вся проверка типов происходит во время компиляции, и после этого в коде не сохраняются общие типы, есть только Object.

Чтобы обнаружить проблему в вашем случае, просто посмотрите на байт-код (в IDEA, Tools -> Kotlin -> Show Kotlin Bytecode или любой другой инструмент). Рассмотрим этот простой пример:

interface Converter<T> {
    fun convert(t: T): T
}

class Reverser(): Converter<String> {
    override fun convert(t: String) = t.reversed()
}

В байтовом коде Converter общий тип стирается:

// access flags 0x401
// signature (TT;)TT;
// declaration: T convert(T)
public abstract convert(Ljava/lang/Object;)Ljava/lang/Object;

И вот методы, найденные в байтекоде Reverser:

// access flags 0x1
public convert(Ljava/lang/String;)Ljava/lang/String;
    ...

// access flags 0x1041
public synthetic bridge convert(Ljava/lang/Object;)Ljava/lang/Object;
    ...
    INVOKEVIRTUAL Reverser.convert (Ljava/lang/String;)Ljava/lang/String;
    ...

Чтобы наследовать интерфейс Converter, Reverser должен иметь способ с той же сигнатурой, то есть стираемый тип. Если у фактического метода реализации есть другая подпись, добавляется метод моста. Здесь мы видим, что второй метод в байткоде - это именно метод моста (и он вызывает первый).

Таким образом, несколько универсальных реализаций интерфейса тривиально будут сталкиваться друг с другом, потому что может быть только один метод моста для определенной сигнатуры.

Кроме того, если это было возможно, ни Java, ни Kotlin не имеют перегрузки метода на основе типа возвращаемого значения, а также иногда возникают неоднозначности аргументов, поэтому несколько наследование было бы весьма ограниченным.

Вещи, однако, будут меняться с помощью Project Valhalla (reified generics сохранит фактический тип во время выполнения), но все же я не ожидал множественное наследование интерфейса.