Any way to inherit from same generic interface twi

2020-03-01 03:33发布

问题:

I have a scenario in my code where I would like a class to implement an interface for two separate types, like this example:

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
    }
}

Kotlin is not pleased with this, citing:

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

I know that one possible solution is to implement the following code, where I implement the interface with <Any> and then check the types myself and delegate them to their functions.

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
    }
}

However, that feels like I'm removing type safety and communication about what the class is used for, and is asking for trouble down the line. Is there a better way to implement this in Kotlin? Additionally - what's the reasoning behind it not being allowed the way I indicated in the first sample? Aren't interfaces just a contract of signatures I'm required to implement, or is there something I'm missing involving generics here?

回答1:

Yes, you're missing an important detail of generics implementation on JVM: the type erasure. In a nutshell, the compiled bytecode of classes doesn't actually contain any information about generic types (except for some metadata about the fact that a class or a method is generic). All the type checking happens at compile time, and after that no generic types retain in the code, there is just Object.

To discover the problem in your case, just look at the bytecode (in IDEA, Tools -> Kotlin -> Show Kotlin Bytecode, or any other tool). Let's consider this simple example:

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

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

In the bytecode of Converter the generic type is erased:

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

And here are the methods found in the bytecode of 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;
    ...

To inherit the Converter interface, Reverser should in order have a method with the same signature, that is, a type erased one. If the actual implementation method has different signature, a bridge method is added. Here we see that the second method in the bytecode is exactly the bridge method (and it calls the first one).

So, multiple generic interface implementations would trivially clash with each other, because there can be only one bridge method for a certain signature.

Moreover, if it was possible, neither Java nor Kotlin has method overloading based on return value type, and there would also be ambiguity in arguments sometimes, so the multiple inheritance would be quite limited.

Things, however, will change with Project Valhalla (reified generics will retain actual type at runtime), but still I wouldn't expect multiple generic interface inheritance.