Javaジェネリクス:型パラメータの関連性を保ったままマップに保持する



正式にはどのような呼称になるのかわからないのだが、以下のような状況がある。

前提条件

public class Sample {

  public static abstract class Action<R extends Response> {}
  public static abstract class Response {}

  public static class FooAction extends Action<FooResponse> {}  
  public static class FooResponse extends Response {}

  public static class BarAction extends Action<BarResponse> {}  
  public static class BarResponse extends Response {}

  public interface Handler<R extends Response, A extends Action<R>> {
    public  R handle(A action);
  }

  public FooResponse handleFoo(FooAction a) {
    return new FooResponse();
  }

  public BarResponse handleBar(BarAction a) {
    return new BarResponse();
  }

アクションとレスポンスのペアとなるクラスがあり、FooAction/Response、BarAction/Responseはそれを継承している。FooActionというアクションには必ずFooResponseという応答があることを示している。

この処理を担当するのがHandlerインターフェースであり、そのインターフェースの唯一のメソッドの型定義通りに、handleFoo、handleBarというメソッドを用意する。

マップにハンドラをまとめたい

当然ながら、以下のようなことはしない。

  Action<?>action = ...
  if (action instanceof FooAction) return handleFoo((FooAction)action);
  if (action instanceof BarAction) return handleBar((BarAction)action);

以下のようにしたいのである。あらかじめ、アクションクラス/ハンドラのマップを作成しておき、アクションのハンドリング時はそのマップを参照するだけとする。

  public Map<Class<?>, Handler>handlerMap = new HashMap<>();
  public void setupHandlers() {    
    handlerMap.put(FooAction.class, this::handleFoo);
    handlerMap.put(BarAction.class,  this::handleBar);
  }
  public Response handle(Action<?>action) {
    return handlerMap.get(action.getClass()).handle(action);
  }

しかし、これはエラーになってしまう。handlerMapの変数宣言を適切なものにできない。

解決策1

以下のようにする。型パラメータの関係性を保持できるメソッドを介すことによって、登録が可能になる。

  public void setupHandlers() {    
    register(FooAction.class, this::handleFoo);
    register(BarAction.class, this::handleBar);
  }

  <R extends Response, A extends Action<R>>void register(Class<A>clazz, Handler<R, A>handler) {
    handlerMap.put(clazz, handler);
  }

解決策2

マップの定義そのものを変更してしまう方法は無いものかと思うのだが、今のところ見つけ出せない。できないのかもしれない。

つまり、以下のようにしたい。

  public static class HandlerMap extends HashMap .... {
  }

  public HandlerMap handlerMap = new HandlerMap();

  public void setupHandlers() {    
    handlerMap.put(FooAction.class, this::handleFoo);
  }

もちろん、以下は可能だ。

  public static class HandlerMap {    
    Map<Class<?>, Handler>map = new HashMap<>();
    public <R extends Response, A extends Action<R>> void put(Class<A>clazz, Handler<R, A>handler) {
      map.put(clazz,  handler);
    }
    public <R extends Response, A extends Action<R>>Handler<R, A>get(Class<A>clazz) {
      return map.get(clazz);
    }
  }

  public HandlerMap handlerMap = new HandlerMap();

  public void setupHandlers() {    
    handlerMap.put(FooAction.class, this::handleFoo);
  }