ジェネリックメソッドの文脈で正しい型を指定する

2018年9月13日

ジェネリックメソッドでは、その文脈で実際の型が特定されるのだが、これが容易な場合と困難な場合がある。特にラムダ式を使いたい場合に問題になる。

サンプルで使用するクラス

以下のクラスをこの後のサンプルで使用する。

  public static class Foo {
    String name;
  }
  public static class Bar<T> {
    T o;
    Bar(T o) {
      this.o = o;
    }
  }

Barはジェネリッククラスであり、その型パラメータに指定するものとしてFooを用意する。例えば、

Bar<Foo>

という型が可能だ。

特定が容易な場合

返り値のあるジェネリックメソッドで返り値を取得する場合には特定が容易だ。例えば、以下のようなケースである(当然ながら引数と同じ型の場合)。

  <T>T someReturn(Consumer<T>l) {
    return null;
  }

  public void test() {
    Bar<Foo>t = someReturn(b-> System.out.println(b.o.name));
  }

このケースの場合、someReturnの引数で使用される型は、返り値の型と同一であるから、例のように返り値を正しい型として取得する場合(このケースの場合はBar<Foo>型)には、引数の型も特定される。したがって、ラムダ式では、何のキャストも必要なく、エラー無しに「b.o.name」にアクセスすることができる。

ところが、返り値を取得しない場合にどうなるかと言えば、

  <T>T someReturn(Consumer<T>l) {
    return null;
  }

  public void test() {
    someReturn(b-> System.out.println(b.o.name)); <-- コンパイルエラー
  }

この場合にはコンパイルエラーになる。ただし、以下のように書くことはできる。

someReturnがインスタンスメソッドの場合には、

this.<Bar<Foo>>someReturn(b-> System.out.println(b.o.name));

Sampleクラスのクラスメソッドの場合には、

Sample.<Bar<Foo>>someReturn(b-> System.out.println(b.o.name));

返り値を取得しない、あるいは返り値が存在しない場合

前述で返り値を取得しない場合も同じなのだが、そもそも返り値が無い以下のような場合では型の特定ができないので、絶対にラムダ式は使うことはできない。

  <T>void noReturn(Consumer<T>l) {    
  }  

この場合には、おとなしく以下のように書くしかない。

    noReturn(new Consumer<Bar<Foo>>() {
      public void accept(Bar<Foo> t) {
        System.out.println(t.o.name); 
      }      
    });

あるいは、前述と同じように、インスタンスあるいはクラスを指定し、型を明示する。

this.<Bar<Foo>>noReturn(b-> System.out.println(b.o.name));
Sample.<Bar<Foo>>noReturn(b-> System.out.println(b.o.name));

他の引数で型を特定する

他の引数を使い、型を特定する方法も考えられる。例えば、対象とする型のオブジェクトを渡してしまう方法がある。

  <T>void noReturn(T object, Consumer<T>l) {    
  }  

としておいて

   noReturn(new Bar<Foo>(new Foo()), b->System.out.println(b.o.name));

とすれば、完全に型が特定される。しかし、わざわざこのためにだけ無駄なインスタンスを作成していることになる。

クラスインスタンスで特定することはできない

先のメソッドを以下のように変更してみる

  <T>void noReturn(Class<T>clazz, Consumer<T>l) {    
  }  

しかしこれではうまくいかない。これを呼び出すとすれば、以下になるが、

   noReturn(Bar<Foo>.class, b->System.out.println(b.o.name));

もちろん、こんな記述はできない。コンパイルエラーになってしまう。

型を特定する目的のオブジェクトを作成する

これはEventBus 型パラメータ付のイベントタイプのサポートで記述したことで、現在のEventBusの実装がこうなっているのだが、型特定のための特別なクラスを用意することである。

ここではEventBusよりもずっと単純な記述にしてみる。

  public static class TypeDesc<T> {    
  }

  <T>void noReturn(TypeDesc<T>type, Consumer<T>l) {    
  }  

としておけば、

  noReturn(new TypeDesc<Bar<Foo>>(), b->System.out.println(b.o.name));

と呼び出すことができる。

これについての議論のいくつか

この問題についての議論が以下に見られる。