javainheritanceoverridingcovariancecovariant-return-types

Using covariant return type while overriding method from granparent interface results in complier error


public interface EntityId {
...
    EntityId cloneWithNewId(long id);
}
public interface Ticket extends EntityId {
/// cloneWithNewId - is not mentioned in this interface
}
public record TicketImpl(...)
@Override
    public Ticket cloneWithNewId(long id) {...}

The compiler gives the error, when I write in my unit test the line with "cloneWithNewId" call:

    @Test
    void shouldBookTicket() {
        Ticket ticket = new TicketImpl(7L, 8L, Ticket.Category.PREMIUM, 21);
        Ticket expectedTicket = ticket.cloneWithNewId(1L); // compiler error in this line
    //...
    }

"EntityId cannot be converted to Ticket. Required type: Ticket. Provided: EntityId"

Any ideas why? Seems to be not much different from classic examples for covariant return type.

It works if I make EnityId interface generic

public interface EntityId<? extends T> {
...
    T cloneWithNewId(long id);
}
public interface Ticket extends EntityId<Ticket> {
/// cloneWithNewId - is not mentioned in this interface
}

It also works if I add the method declaration to Ticket interface:

public interface Ticket extends EntityId {
    Ticket cloneWithNewId(long id);
}

But I do not understand, why it does not work when I override method from extended interface.


Solution

  • In the unit test, you are not invoking the method with the narrowed return type.

    The compiler raises an error here, because the return type of Ticket#cloneWithNewId(long) is EntityId, as it is inherited from EntityId. Note that, when confronted with a compiler error, it does not make sense to argue about a specific TicketImpl instance, as you are not in a runtime. Instead only the declared type of each local variable is relevant.

    This is also why

    public interface Ticket {
        @Override
        public Ticket cloneWithNewId(long id);
    }
    

    will fix the compiler error.

    Another possible fix is to change the unit test to

    @Test
    void shouldBookTicket() {
        TicketImpl ticket = new TicketImpl(7L, 8L, Ticket.Category.PREMIUM, 21);
        Ticket expectedTicket = ticket.cloneWithNewId(1L);
    //...
    }
    

    as that will "point" the compiler to the method declaration in TicketImpl, whose return type is narrowed to Ticket.