Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin's Generics: Type mismatch in generic map parameter

I have the following code that uses generics:

abstract class Event(val name: String)

interface ValueConverter<E : Event> {

    fun convert(event: E): Float

    fun getEventClass(): Class<E>

}

class ValueConverters {

    private val converters = HashMap<String, ValueConverter<Event>>()

    fun <E : Event> register(converter: ValueConverter<E>) {
        converters.put(converter.getEventClass().name, converter)
    }

    fun unregister(eventClass: Class<Event>) {
        converters.remove(eventClass.name)
    }

    fun <E : Event> convert(event: E): Float {
        return converters[event.javaClass.name]?.convert(event) ?: 0.0f
    }

    fun clear() {
        converters.clear()
    }

}

But on this line:

converters.put(converter.getEventClass().name, converter)

it gives an error:

Type mismatch. Expected ValueConverter<Event>. Found ValueConverter<E>.

I also tried something like this:

class ValueConverters {

    private val converters = HashMap<String, ValueConverter<Event>>()

    fun register(converter: ValueConverter<Event>) {
        converters.put(converter.getEventClass().name, converter)
    }

    fun unregister(eventClass: Class<Event>) {
        converters.remove(eventClass.name)
    }

    fun convert(event: Event): Float {
        return converters[event.javaClass.name]?.convert(event) ?: 0.0f
    }

    fun clear() {
        converters.clear()
    }

}

But the problem is when calling ValueConverters.register() with something like:

class SampleEvent1 : Event(name = SampleEvent1::class.java.name)

class SampleValueConverter1 : ValueConverter<SampleEvent1> {

    override fun convert(event: SampleEvent1): Float = 0.2f

    override fun getEventClass(): Class<SampleEvent1> = SampleEvent1::class.java

}

converters.register(converter = SampleValueConverter1())

It also gives the similar Type mismatch error.

How should I declare the generics so that I could use any class that implements ValueConverter and accepts any class that extends Event?

like image 850
Pablo Espantoso Avatar asked Dec 12 '16 13:12

Pablo Espantoso


1 Answers

The error is on this line:

private val converters = HashMap<String, ValueConverter<Event>>()

The values of this map are limited to ValueConverter<Event>. So if you have a class

class FooEvent : Event

and a value converter:

ValueConverter<FooEvent>,

you couldn't store that value converter in your map. What you would actually want is a * star projection type.

private val converters = HashMap<String, ValueConverter<*>>()

Now you can put whatever value converter in the map.


However, this uncovers another problem: How does

fun <E : Event> convert(event: E): Float

know what the generic type of the returned converter in the map is? After all, the map might contain multiple converters for different event types!

IntelliJ promptly complains:

Out-projected type 'ValueConverter<*>?' prohibits the use of 'public abstract fun convert(event: E): Float defined in ValueConverter'.

But you already know the generic type because your map key is the name of the generic type parameter!

So simply cast the returned value by the map with force:

@Suppress("UNCHECKED_CAST")
fun <E : Event> convert(event: E): Float {
    val converter = converters[event.javaClass.name] ?: return 0.0f
    return (converter as ValueConverter<E>).convert(event)
}

If you are curious why the compiler did not complain about your converter function earlier: Remember how your map could only hold ValueConverter<Event> and only that class? This means that the compiler knew you could pass any subclass of Event into this converter. Once you changed to a star projection type, the compiler doesn't know if this might be a ValueConverter<FooEvent>, or a ValueConverter<BazEvent>, etc. - making the effective function signature of a given converter in your map convert(event: Nothing):

because nothing is a valid input.

like image 76
F. George Avatar answered Sep 23 '22 08:09

F. George