So I'm trying to get a reasonably complicated system working. Here's the basics of what I'm attempting.
Rules:
abstract class Rule
{
// stuff
}
class ExampleRule extends Rule
{
// stuff
}
Handlers:
abstract class RuleHandler<T extends Rule>
{
Class<T> clazz;
RuleHandler(Class<T> forClass)
{
this.clazz = forClass;
}
abstract void doStuff(T rule);
}
class ExampleRuleHandler extends RuleHandler<ExampleRule>
{
ExampleRuleHandler()
{
super(ExampleRule.class);
}
void doStuff(ExampleRule rule)
{
// stuff
}
}
And tying them together:
class HandlerDispatcher
{
Map<Class<? extends Rule>, RuleHandler<? extends Rule>> handlers;
void register(RuleHandler<? extends Rule> handler)
{
handlers.put(handler.clazz, handler);
}
void doStuff(List<Rule> rules)
{
for(Rule rule : rules)
{
RuleHandler<? extends Rule> handler = handlers.get(rule.getClass());
handler.doStuff(rule);
}
}
}
class Test
{
void main()
{
HandlerDispatcher hd = new HandlerDispatcher();
hd.register(new ExampleRuleHandler());
}
}
So far I've attempted various combinations of different parameters (wildcarded, restricted, etc.) and have yet to get this compiling without type-related errors. Any insights, solutions or alternative approaches are welcome.
You're trying to use generics in a runtime manner. If you don't know at compile time the types you need to handle, (be it a real type or a type parameter itself), you can't use Generics, plain and simple. It's a compile time only (or mostly) construct.
Here, you're trying to handle things both generically and dynamically. That is generally impossible. Use raw types and live with the type unsafety.
Myself, I'd say you're just asking for trouble with this:
Just make it this:
And then only register that rule handler with types it can handle.
Edit
Based on your comment it seems like your goal is to enforce your design on users of your interface. I stand by the argument that you should separate the concept of filtering and handling.
That said, another option is to leave the class token out of the handler itself and generify your register method.
Here, we suppress a warning when you use the raw type RuleHandler. We know that it is safe through inspection only, by looking at accesses to
map
and seeing that the class always matches theRuleHandler
type parameter. However, this will obviously only be safe if there were no type safety warnings for the client when they invokedregister()
(i.e. they parameterized the call toregister()
).(The inner for-loop was added over
get
such that a handler would be found for subclasses of a given Rule subclasses)Observing that
RuleHandler<T extends Rule>
impliesRuleHandler<?> === RuleHandler<? extends Rule>
the following code is correct (and compiles)
The problem is this line.
The compiler doesn't know that <? extends Rule> is the right class because you looked up the correct handler for the Rule's class. You can replace with
This will produce a warning because the compiler doesn't know that at runtime, you will pick the right type. If this bothers you, you can add to your class.
You cannot avoid unchecked casts here.
Each entry in your
handlers
map relates aClass<T>
to aRuleHandler<T>
, for some classT
that is different for each entry. The methods ofMap
do not express this restriction.What you can do is create a subclass of
Map
that enforces type consistency per entry and perform all the unchecked casts in your new map class.For an example, look Guava's ClassToInstanceMap.