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.
interface CommandHandler<C extends Command> {
This says: For any given command handler, there is some type C that represents what it can handle.
boolean supportsCommand(Class<C> klazz);
This says: You may only pass instances of java.lang.Class
representing 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 call supportsCommand(CreateCommand.class)
on a DeleteCommandHandler
. After all, DeleteCommandHandler
implements CommandHandler<DeleteCommand>
and therefore its supportsCommand
method's full signature is boolean supportsCommand(Class<DeleteCommand> type) {}
. It's as illegal to pass CreateCommand.class
to that method as it is to pass '5' to the method void foo(InputStream in)
- an InputStream is nothing like an int
. A Class<DeleteCommand>
is nothing like a Class<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, a Class<Command>
and that's not going to be compatible with Class<C>
.
One thing to keep in mind with generics is that generics are invariant. In plain types, Integer extends Number
and 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.class
for example), and generics can represent things classes cannot (List<String>
, for example. j.l.Class can't do that, only List
(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<?>
.
This is tricky. You mostly just cannot do this, generics aren't expressive enough.
List<CommandHandler>
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 CreateCommand
as well as the handler for DeleteCommand
in this list, but then there is no type that covers both CommandHandler<CreateCommand>
as well as CommandHandler<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 a CreateCommand
with a CreateCommandHandler
to 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.
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).
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.