Java の型変数に慣れるための練習。
例えば、変数を 『箱』 に見たて、「箱の中の値」 と 「別の何らかの値」 を引数として与えると、二つをくっつけて返す takeOut メソッド があるとする。
takeOut (箱の中の値, 別の何らかの値) => 2 つをくっつけた値
ただし、この関数は次の制約を満さなければならないという条件付きで。
takeOut メソッドが返す値の型は、箱の中身の型ではなく、もう一方の「別の何らかの値」の型と一致する。
箱を int 型として、もう一方の値を int 型としたときのイメージは以下の通り。
同じく箱を int 型として、もう一つの値を String 型としたときのイメージ。
実装。
public class Main00 { public static void main(String[] args) { int box = 100; System.out.println(box); // 100 System.out.println(takeOut(box, 200)); // 300 System.out.println(takeOut(box, "円")); // 100円 } static int takeOut(int box, int val) { return box + val; } static String takeOut(int box, String str) { return box + str; } }
特定の型に依存している箇所
上記の実装は特定の型に依存している。型に依存している部分は次の 3 つ。
-
箱の中身 … (A)
-
別の何らかの値 (takeOut メソッドの第2引数) … (B)
-
上記 (A), (B) をくっつける演算子(関数)
これらが可変になるようにしたい。
クラスで型変数を宣言
まずは、箱をクラスとして実装し、中身を保持するための型変数を導入。型変数により箱はどんな型でも受け入れることができるようになり、入れた中身の型に応じたメソッドを作成可能。
「箱に入れた中身を取り出す」だけの takeOut メソッドから実装する。
クラスで型変数を宣言し、その型変数を「箱の中身」を保持するためのプライベート変数の型として宣言し、takeOut メソッドはその型変数を返すようにする。中身は箱を生成するときに渡すとするなら、
public class Box<A> { private A contents; Box(A contents) { this.contents = contents; } A takeOut() { return this.contents; } }
メソッドで型変数を宣言
先ほどの takeOut メソッドを見ると、「箱の中身」と「別の何らかの値」をくっつける関数は 2 項演算子 `+’ だった。これを、与えられる値の型に応じて、適切な 2 項演算子を適用できるように変更したい。ただし、先ほどの制約は守ることにする。
takeOut メソッドが返す値の型は、箱の中身の型ではなく、もう一方の「別の何らかの値」の型と一致する。
これをイメージしたのが下図。「箱の中身」を型変数 A で、「もう一つの値」に対応したものを型変数 B とし、二項演算子を f で表現した。
ところで、Java は関数を直接メソッドに渡せないので、Strategy パターン でオブジェクトを渡す必要がある。このとき、2項演算子を適用するメソッドを apply とし、インターフェイスを IBinaryOp とする。 apply メソッドの第1引数は「箱の中身」、第2引数は「別のもう一つの値」が渡されるとするなら、
public interface IBinaryOp<A, B> { public B apply(A a, B b); }
第2引数と返り値は、型変数 B で一致させることにより型チェックができる。
これで Box クラスの takeOut メソッドを次のように定義できる。
<B> B takeOut(IBinaryOp<A, B> f, B b) { return f.apply(this.contents, b); }
型変数 B の具体的な型は、Box クラスの生成時に決まるのではなく、このメソッドを使うときに決まる。よって、メソッドにおいて型変数を宣言。もし、これをクラスの型変数としてしまうと、Box の生成時に型を指定しなければならず、また、そのときに指定した型に固定されてしまう。
ここまでのコード。
- Box.java : gist: 316379 – GitHub
- IBinaryOp.java : gist: 316378 - GitHub
型変数に具体的な型を指定して使う
Box クラスを作成できたので、Main クラスの main メソッドで使ってみる。はじめは箱に入れた値を取り出すだけ。
Box<Integer> box1 = new Box<Integer>(100); System.out.println(box1.takeOut()); // 100
Box クラスをインスタンス化する文において、型宣言と new の双方において具体的な型 <Integer> を指定することを忘れずに。
匿名クラスでインターフェイスを実装したクラスを作成
次に、「箱から取り出した値」と「別のもう一つの値」を引数に取るメソッドを作成する。これは匿名クラスを利用した。
System.out.println(box1.takeOut( new IBinaryOp<Integer, Integer>() { public Integer apply(Integer a, Integer b) { return a + b; } }, 200)); // 300
先ほどと同じく、IBinaryOp インターフェイスを実装するクラスをインスタンス化するときに具体的な型の指定を忘れずに。これによりコンパイル時に型チェックがされる。
ところで、takeOut 関数の引数と返り値の型は以下の通りだった。
<B> B takeOut(IBinaryOp<A, B> f, B b) {
上例の場合、型変数 B に Integer 型が対応する。よって、takeOut の第2引数の値を文字列にすると、「型が違うよ」とコンパイラが間違いを指摘してくれる。具体的な型を指定してないと型チェックはされず、メソッドでキャストをしなければならない。
もし、 takeOut の第2引数の値を文字列にしたいなら、この定義では以下のように IBinaryOp インターフェイスを実装したクラスのオブジェクトを生成するときに具体的な型として String を指定。
System.out.println(box1.takeOut( new IBinaryOp<Integer, String>() { public String apply(Integer a, String b) { return a + b; } }, "円")); // 100円
型変数のメリット
当然ながら、Box クラスで型変数を利用しているので、箱の中身の型が違っていても問題ない。main メソッドにおいて、中身を文字列に変更して使うなら、
Box<String> box2 = new Box<String>("百"); System.out.println(box2.takeOut()); // 百 System.out.println(box2.takeOut( new IBinaryOp<String, String>() { public String apply(String a, String b) { return a + b; } }, "円")); // 百円
NetBeans でメソッドの引数と返り値の型を補完
ちなみに、Netbeans を使っている場合、IBinaryOp インターフェイスを実装するときに、
new IBinaryOp<Integer, Integer>()
を入力した後、
メニューより「ソース > コードを修正...」を選択するか、コードの左に出るランプのようなアイコンをクリックすると、「すべての抽象メソッドを実装」がポップアップし、
選択すると型に応じた空のメソッドが生成される。
コード全体
ここまでのコード。
- Main.java : gist: 316380 – GitHub
- Box.java : gist: 316379 – GitHub
- IBinaryOp.java : gist: 316378 - GitHub
型のイレイジャに注意
ところで、型変数を利用したクラスを使うのは、特定の型に依存した実装ではなく、色々な型に対応でき、かつ、コンパイル時に型のチェックができるからだった。ただし、型変数があるにも関わらず、型を指定せずにコードを書くことも可能。これは、Java総称型メモ(Hishidama's Java Generics Memo) によると、
総称型として定義されているクラスから型パラメータを除去したクラスを「型のイレイジャ(型消去:type erasure)」と呼ぶ。…
つまり「List<T>」に対し「List」、「Map<K, V>」に対し「Map」が“イレイジャ”。
例えば、先ほどの例で Box クラスのオブジェクトを生成するとき、型宣言で具体的な型を指定しなかったとする。
Box box1 = new Box<Integer>(100);
Box クラスの takeOut メソッドは IBinaryOp の 型変数 B が決まると、第2引数と返り値の型が決まるのだった。
<B> B takeOut(IBinaryOp<A, B> f, B b) {
しかし、この場合 box1 の型変数が指定されてないためなのか(?)、コンパイラが型チェックをしてくれない。(イレイジャは関係している型変数のチェックを無効にしてしまうのかな?)
System.out.println(box1.takeOut( new IBinaryOp<Integer, Integer>() { public Integer apply(Integer a, Integer b) { return a + b; } }, "hoge"));
上記は実行した段階で「キャストできません」と例外が投げられる。
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
具体的な型の指定を忘れるとコンパイル時にチェックされない。ジェネリクスの恩恵を受けるために忘れないように。
Haskell の場合
同じ意味合いのコードをHaskell で実装するなら、
data Box a = Box a deriving Show takeOut :: Box a -> a takeOut (Box x) = x takeOutWith :: (a -> b -> b) -> b -> Box a -> b takeOutWith f z (Box x) = x `f` z main = do let box1 = Box 100 box2 = Box "Hyaku" print $ takeOut box1 -- 100 print $ takeOutWith (+) 200 box1 -- 300 print $ takeOutWith (\a b -> show a ++ b) "En" box1 -- "100En" print $ takeOutWith ((++).show) "En" box1 -- "100En" print $ takeOut box2 -- "Hyaku" print $ takeOutWith (++) "En" box2 -- "HyakuEn"
う~ん、シンプル。
Java の方は本当にあれでいいのかなぁ。。 (+_+) 実はここで書いた例は、Java で Cons クラスをベースにリストを作り、その上で foldr, foldl に相当するメソッドを実装したらどうなるか試していたとき、型チェックがされなくておかしなぁ~と思い、よりシンプルな例にして試したもの。上記の不自然に見える制約は、foldr, foldl に与える 2項演算子における型と同じようにしたかったので、そのようにした。