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

55 Views Asked by At
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.

1

There are 1 best solutions below

1
Izruo On BEST ANSWER

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.