Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin inheritance with generics

I have an abstract class, let's call it A.

abstract class A(private val name: String) {

    fun read(key: String): Entity {
        ...
    }

    fun write(entity: Entity) {
        ...
    }

    abstract val mapper: Mapper<Any>

    ...

    interface Mapper<T> {
        fun toEntity(entry: T): Entity

        fun fromEntity(entity: Entity): T
    }

    ...

It has an abstract mapper. The point of that is that I can map different objects to Entity and use read and write.

My child class, let's call it B, is structured like this:

class B(private val name: String) : A(name) {
    override val mapper = AnimalMapper

    object AnimalMapper: Mapper<Animal> {
        override fun fromEntity(entity: Entity): Animal {
            TODO("not implemented")
        }

        override fun toEntity(animal: Animal): Entity {
            TODO("not implemented")
        }
    }
}

Ideally, I wanted to have an interface in the Mapper generic and not Any, but I'm simplifying this just for the question.

The issue is that I get this error:

Type of 'mapper' is not a subtype of the overridden property 'public abstract val mapper: Mapper<Any> defined in ...

Why is that?

like image 922
pavlos163 Avatar asked Oct 12 '25 10:10

pavlos163


1 Answers

Note the two facts about inheritance and generics:

  • A val property can only be overridden with a subtype of the original property type. That's because all the users of the type expect it to return some instance of the original type. For example, it's okay to override a CharSequence property with String.

    A var property cannot even use a subtype, only the original type, because the users may want to assign an instance of the original type into the property.

  • Kotlin generics are invariant by default. Given Mapper<T>, any two of its instantiations Mapper<A> and Mapper<B> are not subtypes of each other if A and B are different.

Given this, you cannot override a property of type Mapper<Any> with Mapper<SomeType>, because the latter is not a subtype of the former.

You cannot use declaration-site variance to make all Mapper<T> usages covariant (declare the interface as interface Mapper<out T>) because of T used as the type of a parameter in fun toEntity(entry: T): Entity.

You can try to apply use-site variance, declaring the property as

abstract val mapper: Mapper<out Any>

But this way the users of the class A won't be able to call fun toEntity(entry: T): Entity, since they will not know what is the actual type that replaces Any in the child class and thus what they can safely pass as entry. However, if the exact type (e.g. B) is known to the user, they will see the type of mapper as it is declared in the overridden property.


A common pattern that can allow you to use the overridden property in a more flexible way is to parameterize the parent class class A<T> and define the property as val mapper: Mapper<T>.

This way, the subtypes will have to specify which T they use in their declaration: class B(...) : A<Animal>(...), and the users that see A<Animal> (even without knowing it's actually B<Animal> will safely get its mapper as Mapper<Animal>.

like image 141
hotkey Avatar answered Oct 15 '25 00:10

hotkey