Search code examples
javaoopinterfacecovariancesoftware-design

How to use covariant return types with lists in interface methods?


In Object Oriented Programming, "programming to an interface" is often considered a best practice. Since an interface can have many implementations, the idea is that we should be able to swap out one implementation for another with ease. However, I struggle to understand how to do this when the return type for the interface method is a List.

As an example, say I need to get data about NBA (basketball) players. Two providers of this data could be ESPN and Yahoo. Both ESPN and Yahoo will provide certain fields of a player, like their name. However, ESPN has information about the player's college that Yahoo does not. Yahoo might have information about the player's age, which ESPN does not.

public abstract class Player {
  private String name;
}
public class EspnPlayer extends Player {
  private String college;
}
public class YahooPlayer extends Player {
  private int age;
}

I want to be able to swap between the ESPN and Yahoo providers, so I write an interface with implementations.
Client Code:

public class PlayerController {
  
  public List<Player> getPlayers() {
    NbaService nbaService = new EspnServiceImpl(); // or YahooServiceImpl
    return nbaService.getPlayers();
  }
}

Interface and implementations:

public interface NbaService {
  List<Player> getPlayers();
}
public class EspnServiceImpl implements NbaService {

  @Override
  public List<EspnPlayer> getPlayers() {
    List<EspnPlayer> espnPlayers = new ArrayList<>();
    // call ESPN API and get ESPN players
    return espnPlayers;
  }
}

This does not compile. I thought that this would work because of covariant return types. Now if I change the interface method to List<? extends Player> getPlayers();, everything compiles. However, I'm not sure if this is a good practice. Can someone help me understand what concepts I'm missing? Thanks.


Solution

  • This does not compile.

    You have two options:

    • Try List<? extends Player> getPlayers() in your interface, and now you can write List<EspnPlayer> getPlayers() which is a valid implementation if this. Note that it is not possible to call .add() on this list, for proper reasons (after all, adding a non-EspnPlayer to this list would break the list!)
    • If that add thing is required, then there's no way out except to have:
    interface NbaService<T extends Player> {
        public List<T> getPlayers();
    }
    

    In Object Oriented Programming, "programming to an interface" is often considered a best practice.

    Nice use of the passive voice there, but, as wikipedia editors would say, [citation needed].

    There is a grain of truth to this, but as with all programming style suggestions, it's oversimplified. Which isn't a problem, but that's why the following maxim is pretty much always true: If you don't understand the reasons behind a style recommendation, then blindly following it is stupid and will result in worse code. There is no easy out: You must first grok the considerations underlying the recommendation, and until then you just cannot use the recommendation.