4

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?

2
  • A says that users can map anything. But if the implementation is B, they can only map animals. That raises red flags. Commented Sep 19, 2017 at 16:39
  • Thanks for your comment. Okay, so is there a way to provide what the users can map in the children classes? Commented Sep 19, 2017 at 16:42

1 Answer 1

9

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

Sign up to request clarification or add additional context in comments.

4 Comments

I see. Is that true even if in Mapper<A> and Mapper<B>, A is a subtype of B? Is the relationship between A and B irrelevant?
As a side note, it would be really cleaner for me to be able to call toEntity from the parent. What other design could I follow that would allow that? What about passing a Mapper as a parameter?
@pk1914 Yes, if Mapper<T> is invariant, Mapper<A> and Mapper<B> are never subtypes of each other if A and B are different, including even A and B that are subtypes of each other.
@pk1914, I've added a pattern that can be used to improve the usability of such hierarchy. However, it's hard to tell whether it's useful for you without knowing the actual use cases.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.