I’m building a command dispatcher for my CQRS application.
public interface Command {
}
public interface CommandExecutor<T extends Command> {
Mono<CommandResult> execute(T command);
}
public class CommandDispatcher {
private final Map<String, CommandExecutor<? extends Command>> executors = new HashMap<>();
// Example usage
public static void main(String[] args) {
CommandDispatcher dispatcher = new CommandDispatcher();
dispatcher.register(MakeRequestCommand.class.getName(), new MakeRequestCommandExecutor());
dispatcher
.dispatch(MakeRequestCommand.class.getSimpleName(), new MakeRequestCommand("Any iPhone"))
.subscribe(System.out::println);
}
public <T extends Command> void register(String commandAlias, CommandExecutor<T> executor) {
executors.put(commandAlias, executor);
}
public <T extends Command> Mono<CommandResult> dispatch(String commandAlias, T command) {
if (!executors.containsKey(commandAlias)) {
throw new RuntimeException("No executor registered for command %s".formatted(commandAlias));
}
CommandExecutor<T> executor = (CommandExecutor<T>) executors.get(commandAlias);
return executor.execute(command);
}
}
The following line is producing a warning in IntelliJ.
CommandExecutor<T> executor = (CommandExecutor<T>) executors.get(commandAlias);
Unchecked cast: 'io.freesale.application.command.CommandExecutor<capture<? extends io.freesale.application.command.Command>>' to 'io.freesale.application.command.CommandExecutor'
Is this something I need to be concerned about / is there any way to resolve it (without suppressing warnings 🙂.)
I'm worried it is highlighting a flaw in my design.
An unchecked cast means literally that - Java cannot check whether the cast is valid or not (and this is due to type erasure), and so there will be no ClassCastException
s at the point of the cast, which in general may make it harder to debug.
For example, if you did:
dispatcher.register(SomeTypeOfCommand.class.getName(), new SomeTypeOfCommandExecutor());
dispatcher
// we're getting the SomeTypeOfCommandExecutor, but giving it AnotherTypeOfCommand
.dispatch(SomeTypeOfCommand.class.getName(), new AnotherTypeOfCommand())
.subscribe(System.out::println);
SomeTypeOfCommandExecutor
is a CommandExecutor<SomeTypeOfCommand>
. If the cast were checked, Java would see that casting AnotherTypeOfCommand
to CommandExecutor<SomeTypeOfCommand>
is an invalid cast, and throw an ClassCastException
at the line:
CommandExecutor<T> executor = (CommandExecutor<T>) executors.get(commandAlias);
However, the cast is actually unchecked, so a ClassCastException
is only thrown later down the line, when SomeTypeOfCommandExecutor.execute
is called. At that point, Java can see that the passed in parameter is not a SomeTypeOfCommand
. This may lead to confusing stack traces.
That is what the warning is warning you about. IMO, this isn't too much to worry about, since executor.execute(command);
is not too far after the unchecked cast (it is immediately after), so you are basically "checking" the cast immediately on the next line anyway. If you want to be extra clear that you are checking the cast, you can add a method in CommandExecutor
that looks like:
boolean canExecute(Object command);
And in implementations, you would just use instanceof
to check command
.
Side note: I would recommend that you use a Class<?>
as the map's key type, if there can only be one command executor for each type of command. This way, in register
and dispatch
, you can require that class type and the command's type must be the same, which prevents things like this:
.dispatch(SomeTypeOfCommand.class, new AnotherTypeOfCommand())
You would do:
private final Map<Class<? extends Command>, CommandExecutor<? extends Command>> executors = new HashMap<>();
public <T extends Command> void register(Class<T> commandAlias, CommandExecutor<T> executor) {
executors.put(commandAlias, executor);
}
public <T extends Command> Mono<CommandResult> dispatch(Class<T> commandAlias, T command) {
CommandExecutor<T> executor = (CommandExecutor<T>) executors.get(commandAlias);
if (executor == null) {
throw new RuntimeException("No executor registered for command %s".formatted(commandAlias));
}
return executor.execute(command);
}