Search code examples
javainheritancetypespolymorphism

Java type which accepts a class AND its interfaces (contravariance)


I have a class and an interface like this:

interface Employee {...}

class Developer implements Employee {...}

Then I have classes which implement steps in the onboarding process:

interface OnboardingStep<T extends Employee> {
  void perform(T newEmployee);
}

class GeneralOnboardingStep implements OnboardingStep<Employee> {
  GeneralOnboardingStep() {}

  @Override
  void perform(Employee newEmployee) {...}
}

class DeveloperOnboardingStep implements OnboardingStep<Developer> {
  DeveloperOnboardingStep() {}

  @Override
  void perform(Developer newDeveloper) {...}
}

The full onboarding process consists of the general onboarding plus the specific onboarding for developers.

So what I would like to do is this:

abstract class OnboardingProcess<T extends Employee> {

  // This is where I need help, see below
  private final List<OnboardingStep<T>> onboardingSteps;

  protected OnboardingProcess(List<OnboardingStep<T>> onboardingSteps) {
    this.onboardingSteps = onboardingSteps;
  }

  public void perform(T newEmployee) {
    for (var onboardingStep: onboardingSteps) {
      onboardingStep.perform(newEmployee);
  }
}

class DeveloperOnboardingProcess extends OnboardingProcess<Developer> {

  DeveloperOnboardingProcess() {
    // This does not work
    super(List.of(
      new GeneralOnboardingStep(),
      new DeveloperOnboardingStep()
    ));
  }
}

This does not work, because the List in DeveloperOnboardingProcess may only contain OnboardingStep<Developer> and not OnboardingStep<Employee>.

But it should work because Developer implements Employee. (By "should work" I mean: "I wish it worked").

How can I adapt the list type in the abstract class to also accept onboarding steps for the interface?


Solution

  • What you might want is this (but see below):

    private final List<OnboardingStep<? super T>> onboardingSteps;
    
    protected OnboardingProcess(List<OnboardingStep<? super T>> onboardingSteps) {
        this.onboardingSteps = onboardingSteps;
    }
    

    ? super T means that each list element’s perform method is guaranteed to accept a T instance, since it will be restricting the constructor argument to steps whose type is T or a superclass of T. So, for DeveloperOnboardingProcess, the constructor can take list elements which are an OnboardingStep<Developer> or an OnboardStep whose generic type is any superclass of Developer, including Employee itself.

    However, one thing you cannot do is pass a List<DeveloperOnboardingStep>. List<DeveloperOnboardingStep> is not equivalent to List<OnboardingStep<Developer>> or List<OnboardingStep<Employee>>, because the latter has an add (and addAll, etc.) method which allows an OnboardingStep, or a DeveloperOnboardingStep, or any other future subclass of OnboardingStep, whereas a List<DeveloperOnboardingStep> has an add method which will accept only a DeveloperOnboardingStep argument.

    To allow, say, a List<DeveloperOnboardingStep> argument, use this:

    private final List<? extends OnboardingStep<? super T>> onboardingSteps;
    
    protected OnboardingProcess(List<? extends OnboardingStep<? super T>> onboardingSteps) {
        this.onboardingSteps = onboardingSteps;
    }