How do I get this system of nested generic paramet

2020-03-28 02:03发布

问题:

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.

回答1:

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:

abstract class RuleHandler<T extends Rule>
{
  abstract void doStuff(T rule);
}

Just make it this:

abstract class RuleHandler
{
  abstract void doStuff(Rule rule);
}

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.

interface RuleHandler<T extends Rule> {
    void doStuff(T rule);
}

//...

public <T> void register(Class<T> type, RuleHandler<? super T> handler) {
   map.put(type, handler);
}

public void process() {
   for ( Rule r : rules ) {
       for(Map.Entry<Class<?>, RuleHandler<?>> entry : map.entrySet() ) {
           if ( entry.getKey().instanceOf(r) ) {
               @SuppressWarnings("unchecked")
               ((RuleHandler)entry.getValue()).doStuff(r);
           }
       }
   }
}

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 the RuleHandler type parameter. However, this will obviously only be safe if there were no type safety warnings for the client when they invoked register() (i.e. they parameterized the call to register()).

(The inner for-loop was added over get such that a handler would be found for subclasses of a given Rule subclasses)



回答2:

You cannot avoid unchecked casts here.

Each entry in your handlers map relates a Class<T> to a RuleHandler<T>, for some class T that is different for each entry. The methods of Map 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.



回答3:

Observing that

  • handlers is private and final
  • handler.clazz is final
  • RuleHandler<T extends Rule> implies RuleHandler<?> === RuleHandler<? extends Rule>

the following code is correct (and compiles)

abstract class RuleHandler<T extends Rule>
{
  final Class<T> clazz;
  // as before
}

class HandlerDispatcher
{
  private final Map<Class<?>, RuleHandler<?>> handlers;
  void register(RuleHandler<?> handler)
  {
    handlers.put(handler.clazz, handler);
  }
  void doStuff(List<Rule> rules)
  {
    for(Rule rule : rules)
    {
      @SuppressWarnings("unchecked")
      RuleHandler<Rule> handler = (RuleHandler<Rule>) handlers.get(rule.getClass());
      handler.doStuff(rule);
    }
  }
}

class Test
{
  void main()
  {
    HandlerDispatcher hd = new HandlerDispatcher();
    hd.register(new ExampleRuleHandler());

    RuleHandler<?> handler = new ExampleRuleHandler();
    hd.register(handler);
  }
}


回答4:

The problem is this line.

RuleHandler<? extends Rule> handler = handlers.get(rule.getClass());

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

RuleHandler handler = handlers.get(rule.getClass());

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.

@SuppressWarnings("unchecked")