Search code examples
javagenericsinterface

Why can i not use a generic type as parameter in Java?


In Java i have an interface that has a generic type that also implements a different interface.

When i use this generic as a parameter, i cannot pass an instance of the interface because it is of the wrong type.

See this example:

interface Pet {}

interface PetService<T extends Pet> {

  T loadPetById(int petId);

  void feed(T pet, int amount);
}

class Dog implements Pet {}

class DogService implements PetService<Dog> {
  @Override
  public Dog loadPetById(int petId) {
    // load and return dog with this id from database
  }

  @Override
  public void feed(Dog pet, int amount) {
    // feed dog
  } 
}

class RestApi {
  void feedPet(String petType, int petId, int amount) {
    PetService<? extends Pet> petService = instantiatePetServiceByType(petType);
    Pet pet = petService.loadPetById(petId);

    petService.feed(pet, amount);
  }

  PetService<? extends Pet> instantiatePetServiceByType(String petType) {
    // out of scope
  }
}

Now to my problem:

The call to petService.feed(pet, amount) complains in the IDE that pet is not the correct type.

It says:

  • Required type: capture of ?
  • Provided: Ownable

What am i doing wrong?


Solution

  • Problem and PECS

    The problem here ultimately is a consequence of PECS.

    Your instantiatePetServiceByType method returns a service (PetService<? extends Pet>) operating on an unknown type. All you know is that it is some sort of pet, but you do not know which specific pet.

    Now, when you write

    PetService<? extends Pet> petService = instantiatePetServiceByType(petType);
    Pet pet = petService.loadPetById(petId);
    
    petService.feed(pet, amount);
    

    Java has a problem with your insertion of the pet in the feed method, because the method expects from you the exact type that this service operates on. But this type is unknown to you, you can not name it.

    Imagine what would happen if someone writes:

    PetService<? extends Pet> petService = instantiatePetServiceByType(petType);
    Pet pet = petService.loadPetById(petId);
    
    // Malicious
    pet = new Cat();
    
    petService.feed(pet, amount);
    

    Now, pet is a Cat, but the service operates perhaps on Dogs. So if Java would allow your code to compile, users could do above and break the type system.

    As a consequence of this dilemna, PECS just completely disallows you to call the feed method all-together, because it is impossible for you to ensure that you supply the correct type. Or rather, it is impossible for the compiler to ensure that you are not messing with it.


    Solution

    In fact, since you want to find the service dynamically during runtime, it is unfortunately impossible to make the code type-safe. Generics are a compile-time feature and at the point where you know the type of the service (runtime), it is already too late.

    Fortunately, there is a way to give the illusion of type-safety to the user and still providing a sane generic system, by dropping type-safety inside your instantiatePetServiceByType method.

    The idea goes as follows:

    • let the user tell you which type they expect
    • assume it is correct - if not, crash during runtime
    • return the service casted to the expected type
    <T extends Pet> PetService<T> instantiatePetServiceByType(Class<T> petType) {
      PetService<? extends Pet> service = ... // find your service
      return petType.cast(service);
    }
    

    For the user, the generics work and are convenient to use:

    PetService<Dog> dogService = api.instantiatePetServiceByType(Dog.class);
    Dog dog = dogService.loadPetById(petId);
    
    dogService.feed(dog, amount);
    

    Now you just have to make sure that you never save incorrect services. For example registering a PetService<Cat> as a service for Dogs.