Mapの置換にみるジェネリクス表現
Javaのジェネリクスについて考察するのに丁度良いテーマを見つけたので忘れないうちに書いておく。
Map
public static <K,V> Map<V,K> swap1(Map<K,V> origin) { // 略 }
単にMapからMapに変換するなら上記のようなメソッドシグネチャでよい。
ここで、Mapの具象型を帰すようにしたいとする。
public static <K,V, R extends Map<V,K>, P extends Map<K,V>> R swap2(P origin, Supplier<R> supplier) { // 略 }
まず単純に引数のMapと戻り値のMapを型変数PとRにとるようにしてみよう。
JavaのジェネリクスはC#のようなnew制約をつけれないのでメソッド内部でインスタンス生成する場合はjava.lang.Classを引数に渡してリフレクションでnewInstance()するのが常道ではあるが、ここではJava8のjava.util.function.Supplierを利用してラムダでインスタンス生成をさせよう。
利用する場合の呼び出し方は以下のようになる。
Map<String, Integer> origin = new LinkedHashMap<>(); Map<Integer, String> swap = MapSwapSample.swap2(origin, ()->new TreeMap<>());
具象型をI/Oで揃えれるか?
今回の場合はMapなのでIn側とOut側の実装型はむしろ自由に変えれるほうが便利なのだが、このテーマの元ネタはBiFunction
ここで、Javaのジェネリクスの表現力ではR extends Map
かつてジェネリクスの型変数を2段階にバインドする試みをしたことがある。これは技法としては型変数の部分適用であった。これが今回のMapのケースでは利用できないところがミソなのだ。
端的に言えば、ジェネリクスの部分適用はフラットで互いに素な型変数であればカリー化して部分適用をすることが可能になるが、今回のようにKとVとそれをバインドするMapという組み合わせではそれが出来ないのである。
無駄な型変数の除去
さて、PとRが同じ具象型であるというような宣言は出来なかった。いや、そこだけを同じであると宣言することは出来るが、そうすると
そうなると残念ながら型変数Pは役に立たないものとなる。
ジェネリクスはスコープを区切って、そこへのInとOutの引数/戻り値などの型の関連性を示すものであることをおさらいしよう。
ジェネリクスの基礎と クラス設計への応用の9ページあたりから参考になるだろう。
どこかひとつだけで用いられている型変数というのはわざわざ型変数でなくてよい。例えばjava.util.Collections#shuffleの引数は型変数を使わず、ワイルドカードでshuffle(List<?> list)と定義されている。
同じように、型変数P extends Map
このようにして不要な型変数を整理すると
public static <V,K, R extends Map<V,K>> R swap3(Map<K,V> origin, Supplier<R> supplier) { // 略 }
といった形になった。もっとも利用側は
Map<String, Integer> origin = new LinkedHashMap<>(); Map<Integer, String> swap = MapSwapSample.swap3(origin, ()->new TreeMap<>());
と特に変わらないのであるが、不要な型変数はジェネリクスヘルへの第一歩なので、後のコードメンテナンスを考えるなら削っておいたほうがいい。
ソースコード
今回掲載のソースコードはGithubにて公開している。