FutureTaskのコンストラクタと魔法のバインド

java.util.concurrent.FutureTaskのコンストラクタの定義が

public FutureTask(Callable callable)

http://docs.oracle.com/javase/jp/8/docs/api/java/util/concurrent/FutureTask.html#FutureTask-java.util.concurrent.Callable-

とCallable<V>となっていてCallable<? extends V>じゃないのが不便だという話題。

これにより Thread Safe な汎用オブジェクトCache - がくぞーのメモ のキャッシュ機構のコンストラクタをCallable<? extends V>に出来ないのだとがくぞさん(@gakuzzzz)が嘆いていたので、小手先のテクニックで対応したのが以下のコード。

import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicReference;

public class Cache<T> {
    private Callable<? extends T> originalFactory;
    private AtomicReference<Future<? extends T>> cache = new AtomicReference<Future<? extends T>>();

    public Cache(Callable<? extends T> originalFactory) {
        this.originalFactory = originalFactory;
    }

    public T get() {
        try {
            return getFuture().get();
        } catch (ExecutionException e) {
            // ここの処理は現場のポリシーによって柔軟に
            throw new RuntimeException(e.getCause());
        } catch (InterruptedException e) {
            throw new CancellationException(e.getMessage());
        }
    }

    private Future<? extends T> getFuture() {
        Future<? extends T> result = cache.get();
        if (result != null) return result;
        FutureTask<? extends T> task = getFutureTask(originalFactory);
        if (cache.compareAndSet(null, task)) {
            task.run();
        }
        return cache.get();
    }

    private <X> FutureTask<X> getFutureTask(Callable<X> callable) {
    	return new FutureTask<X>(callable);
    }
}

問題の解説

問題の中心になるのは

public FutureTask(Callable callable)

http://docs.oracle.com/javase/jp/8/docs/api/java/util/concurrent/FutureTask.html#FutureTask-java.util.concurrent.Callable-

というコンストラクタに対して、

Callable<HogeEx> call = ()->new HogeEx();
new FutureTask<Hoge>(call);

という呼び出しが出来ないという点。
Callable<HogeEx>をCallable<Hoge>にキャストすることは出来ない。
このキャストについてはJavaジェネリクス再入門 - プログラマーの脳みそあたりを参考にされたし。ジェネリクスの代入互換性は通常の型の代入互換性とは異なる。
そして

Callable<? extends Hoge> call = ()->new Hoge();
new FutureTask<? extends Hoge>(call);

というようにコンストラクタで? extends Hogeをバインドすることもできない。
もし、Javaの標準APIのFutureTaskのコンストラクタが

public FutureTask(Callable<? extends V> callable)

という形状であれば

Callable<HogeEx> call = ()->new HogeEx();
new FutureTask<Hoge>(call);

とすることが可能になる。が、Javaの標準APIシグネチャ(ようするにメソッドの型)なんて互換性の関連もあってそうそう変わるものでもない。

対応策

new FutureTask<? extends Hoge>を実現できれば良いのに、ということで? extends Hogeロンダリングしてメソッドスコープの型変数にすることで回避。

    private <X> FutureTask<X> getFutureTask(Callable<X> callable) {
    	return new FutureTask<X>(callable);
    }

このgetFutureTaskを呼び出す側だが、メソッドスコープの型変数へのバインドは型が推論できるので

FutureTask<? extends T> task = getFutureTask(originalFactory);

と書くことで呼び出すことが出来る。
逆に推論をさせずに明示的にcaptuer ? extends Tをバインドしようとするとコンパイルエラーになる。

FutureTask<? extends T> task = this.<? extends T>getFutureTask(originalFactory); // NG

ここまで書いてもうひとつ型推論が行われるダイヤモンド演算子を使えばいいことに気づく。

FutureTask<? extends T> task = new FutureTask<>(originalFactory); // OK

まとめ

  • 通常の記法では型変数にcaptuer ? extends Hogeといったものをバインドすることは出来ないが、型推論させるとバインドさせることが出来る。
  • 型推論が効くのはメソッドスコープの型変数の場合と、ダイヤモンド演算子を使った場合