-1

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?

  • you could change `void feed(T pet, int amount);` to `void feed(Pet pet, int amount);` and then do the correct casting on the actual implementation. On the DogService --> `@Override public void feed(Pet pet, int amount) { Dog dog = (Dog) pet; } ` – Anon Jun 22 '23 at 12:19
  • You *could* do ` PetService instantiatePetServiceByType(Class petType)` instead of supplying the pet type as a `String`... – MC Emperor Jun 22 '23 at 16:25
  • Oh wait, now I see (after I refreshed the page) that this exact code was already provided in Zabuzard's answer. Apparently, such is a common solution to the problem you're describing. – MC Emperor Jun 22 '23 at 16:29
  • @Anon That is what i did now... A little bit ugly but hey: It works at least somehow. – Max Bachhuber Jun 23 '23 at 11:14
  • @MCEmperor thank you for the idea but this will still not work for my use case – Max Bachhuber Jun 23 '23 at 11:14

1 Answers1

-1

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.

Zabuzard
  • 25,064
  • 8
  • 58
  • 82