I'm trying to grasp PECS idea in java and don't know, how to apply this idea in following example:
package org.example;
import java.util.List;
import java.util.Optional;
interface Command { String name();}
class DeleteCommand implements Command {
@Override
public String name() {return "Delete command";}
}
interface CommandHandler<C extends Command> {
boolean supportsCommand(Class<C> klazz);
String commandName(C command);
}
class DeleteCommandHandlerImpl implements CommandHandler<DeleteCommand> {
@Override
public boolean supportsCommand(Class<DeleteCommand> klazz) {return DeleteCommand.class.isAssignableFrom(klazz);}
@Override
public String commandName(DeleteCommand command) {return command.name();}
}
public class Main {
public static void main(String[] args) {
Command toBeHandled = new DeleteCommand();
List<CommandHandler> knownCommands = List.of(new DeleteCommandHandlerImpl());
Optional<String> result = knownCommands
.stream()
.filter(commandHandler -> commandHandler.supportsCommand(toBeHandled.getClass()))
.findFirst()
.map(commandHandler -> commandHandler.commandName(toBeHandled));
System.out.println(result);
}
}
I have a problem with line List<CommandHandler> knownCommands = List.of(new DeleteCommandHandlerImpl()); It raises a compilation warning since CommandHandler is missing parametrized class.
According to PECS, I'm consuming from this list, so it should be List<CommandHandler<? extends Command>> knownCommands = List.of(new DeleteCommandHandlerImpl()); but it fails compilation. How should I parametrize this list?
Your entire getup is wrong, probably because you slightly misunderstand what generics do; it's a very common misunderstanding.
This says: For any given command handler, there is some type C that represents what it can handle.
This says: You may only pass instances of
java.lang.Classrepresenting the type I can handle.In other words, the only sane implementation of this is
return true;. In particular, it is not even legal to callsupportsCommand(CreateCommand.class)on aDeleteCommandHandler. After all,DeleteCommandHandlerimplementsCommandHandler<DeleteCommand>and therefore itssupportsCommandmethod's full signature isboolean supportsCommand(Class<DeleteCommand> type) {}. It's as illegal to passCreateCommand.classto that method as it is to pass '5' to the methodvoid foo(InputStream in)- an InputStream is nothing like anint. AClass<DeleteCommand>is nothing like aClass<CreateCommand>. Neither is a supertype of the other, they are utterly incompatible.This is why
.filter(commandHandler -> commandHandler.supportsCommand(toBeHandled.getClass()))is never going to compile:toBeHandled.getClass()is, at best, aClass<Command>and that's not going to be compatible withClass<C>.One thing to keep in mind with generics is that generics are invariant. In plain types,
Integer extends Numberand that means an Integer is good as a number whenever you need a number. Generics are not like that because universe / math, it just doesn't work that way (otherwise, I can make a list of integers, assign to a variable that represents 'list of any numbers', add a double to the other list, which also adds a double to my list because I just copied a reference, not the whole list, and now there's an not-an-integer in my list-of-integers and everything's broken).Surely that's not what you wanted. You wanted that param to be
Class<?>. In general,Class<C>is a pretty big code small; class instances can represent things that generics cannot (int.classfor example), and generics can represent things classes cannot (List<String>, for example. j.l.Class can't do that, onlyList(raw)).Is there a point to even having this method? If not, get rid of it. If you must have it, that param should be
Class<?>.The general idea of a link between a command handler and the command it handles
This is tricky. You mostly just cannot do this, generics aren't expressive enough.
Yes, this is a warning, because it's a list of command handlers of what commands - it doesn't say. And there's nothing you can put here that is going to make sense. Because surely your point is that you want to stick both, say, the handler for
CreateCommandas well as the handler forDeleteCommandin this list, but then there is no type that covers bothCommandHandler<CreateCommand>as well asCommandHandler<DeleteCommand>in a way that is useful (that lets you invoke these handlers properly).(i.e. you can make a
List<CommandHandler<? super DeleteCommand>>but any command handlers you get from such a thing can only be sent delete commands. That'd be a pointless system - at some point you want to add aCreateCommandwith aCreateCommandHandlerto go with it and you can't extend the system to account for more than a single type of command, making the whole exercise pointless).Hence, we have now broken it:
and voila, broken - you have a compile-time-only system whose natures are wiped out at runtime1, that you can only check at runtime, where it is no longer available.
The simple solution
Forget generics. They don't work here. Either just wipe them away from your CommandHandler interface, or keep em but accept that the code where you register handlers and deliver them is going to be riddled with generics warnings and you need to pile on with the hackery (such as a
Class<?> getSupportedCommands()method so that you can at-runtime inspection code and e.g. make a hashmap mapping a type to its handler, none of this would be compile-time typechecked).The complex solution
Involves type tokens and possibly annotation processors. It's rocket science java, I really, really doubt it's appropriate for the basic case here. It's probably inappropriate for any such case.
[1] You can introspect a very limited amount of generics at runtime. But it's always tricky and rarely does what you want - more generally if you must insist on diving down that very deep rabbit hole, you're going to either want a standard SuperTypeToken solution or end up inventing your own, or your docs need a ton of caveats such as 'when writing a command handler, the type of command you handle must be reified, cannot itself have generics, and you can't build up a hierarchy of command handlers - they all have to look like
extends CommandHandler<SpecificCommandType>or stuff just breaks and there is no compile time check available to ensure you're doing it right' - which is the kind of caveat you really, really don't want to write in your docs.