Generics 勉強中 - しげっと!
妥協案 - しげっと!
Integer.MAX_VALUE で Integer の最大値をとってくるみたいに、
// このソースはコンパイル通らないので参考にしないでね。 class Values <T> { private T max, min; public Values() { this.max = T.MAX_VALUE; this.min = T.MIN_VALUE; } public T getMax() { return this.max; } public T getMin() { return this.min; } } class ValuesTester() { public static void main(String[] args) { Values <Byte> obj = new Values<Byte>(); System.out.println("Byte : " + obj.getMin + "〜" + obj.getMax); Values <Integer> obj2 = new Values<Integer>(); System.out.println("Integer : " + obj2.getMin + "〜" + obj2.getMax); } }みたいなコード書きたいんだけど、これはコンパイル通りませんでした。
こんな感じに実現する方法ないのかな?
Generics 勉強中 - しげっと!
JavaのIntegerやらLongやらのラッパー型には最大値を表現するMAX_VALUEというstaticフィールドがある。んで、ある種の型を内部データとするクラスValueがあってデータ型はジェネリクスで渡すようなイメージ。
このとき、データを保持するフィールドvalueとその値を出し入れするgetter/setterは綺麗に作ることができる。
public class ValueWrapper <T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
じゃぁ、このTに渡したIntegerとかLongとかのMAX_VALUEはどうやれば取れるだろうか?
T.MAX_VALUE
はコンパイルエラーになる。Tは型変数であって、型を表現するものの変数だから new Value
じゃぁ、型が判別できればいいのだろうか。
public class Hoge { public static final String NAME = "this class is Hoge"; } public class Wrapper <T extends Hoge> { public void test() { System.out.println(T.NAME); } }
型TがHogeを継承することをジェネリクス型の境界で保証する。こうなれば、理屈では型TはHoge型が持つstaticなフィールド、メソッドへのアクセスが可能ということになる。しかし、ここで思い出さないといけないのは、Javaのstaticメソッドはオーバーライドされポリモフィックに動作するものではないということ。
public class Hoge { public static final String NAME = "this class is Hoge"; } public class Piyo extends Hoge { public static final String NAME = "this class is Piyo"; } public class Hoge2 extends Hoge {} public class Piyo2 extends Piyo {} public class Main { public static void main(String[] args) { System.out.println(Hoge.NAME); System.out.println(Piyo.NAME); System.out.println(Hoge2.NAME); System.out.println(Piyo2.NAME); } }
this class is Hoge
this class is Piyo
this class is Hoge
this class is Piyo
一見して、PiyoクラスではHogeクラスのNAMEフィールドをオーバーライドしているように見えるが、ここで行われているのはオーバーライドではなくてハイディングだ。コンパイル時に親クラスのstaticフィールド・メソッドを探してくれているが、実行時はあくまで静的に動作する。
Javaでのstaticなフィールド・メソッドは一見してインスタンスフィールドや、インスタンスメソッドのオーバーライドなどと同じように見えるが、動作原理はまるで異なる。Javaのジェネリクスはあくまでインスタンスに対してのものだという制限がある。型変数Tが型を表現する変数だからと言って、型に属する、つまりstaticなフィールドやメソッドへのアクセスはできない。この辺のJavaの言語設計はオブジェクト指向的には綺麗じゃない部分。
さて、話を戻して、Max値やMin値を持つような設計にしたければどうしたらよいか。安直なのはインスタンスにこれらの情報を持たせてそこから値を得る方法だ。
/** 型と値を表現するためのインターフェース */ public interface Value<T> { T getMax(); T getMin(); } /** 値をラップするクラス */ public class ValueWrapper <T extends Value<T>> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } } /** Integer型の値 */ public class IntegerValue implements Value<IntegerValue> { private static final IntegerValue MAX_VALUE = new IntegerValue(Integer.MAX_VALUE); private static final IntegerValue MIN_VALUE = new IntegerValue(Integer.MIN_VALUE); private Integer value; public IntegerValue(Integer value) { this.value = value; } public IntegerValue getMax() { return MAX_VALUE; } public IntegerValue getMin() { return MIN_VALUE; } }
Integer型の値を表現するIntegerValueクラス自身のインスタンスメソッドとしてMAX,MINを返すメソッドを規定したというわけだ。しかしこれではIntegerValueという型の持つべき情報とIntegerValueのインスタンスの表現が混ざる。本来staticなレベルの情報をインスタンスレベルに落としたのだから。
これを嫌がるとすれば型を表現する型と値を表現する型への分離を試みることになる。そう、クラスとインスタンスの関係を表現するように。
/** 値を表現するためのインターフェース */ public interface Value<X, V extends Value<X, V, T>, T extends ValueType<X, V, T>> { T getValueType(); X getValue(); } /** 型を表現するためのインターフェース */ public interface ValueType<X, V extends Value<X, V, T>, T extends ValueType<X, V, T>> { V getMax(); V getMin(); }
これで型を表現するValueType型と値を表現するValue型に分離できる。しかし、ValueTypeとValueがお互いの型を知ろうとするためにジェネリクスの表現が複雑になってしまう。実際に実装型を作る時は以下のようなイメージでそれほど複雑じゃないんだが
/** Integer型を表現するクラス */ public class IntegerValueType implements ValueType<Integer, IntegerValue, IntegerValueType>{ private static final IntegerValue MAX_VALUE = new IntegerValue(Integer.MAX_VALUE); private static final IntegerValue MIN_VALUE = new IntegerValue(Integer.MIN_VALUE); @Override public IntegerValue getMax() { return MAX_VALUE; } @Override public IntegerValue getMin() { return MIN_VALUE; } } /** Integerの値を表現するクラス */ public class IntegerValue implements Value<Integer, IntegerValue, IntegerValueType> { private static IntegerValueType type = new IntegerValueType(); @Override public IntegerValueType getValueType() { return type; } private Integer value; public IntegerValue(Integer value) { this.value = value; } @Override public Integer getValue() { return this.value; } }
この利用イメージを
やりたいことはX型を二つのクラスで共有すること、そして互いの型を知ることだった。Javaのジェネリクスで独立した2つのクラス間でこの型を共有しようとすると先のような複雑な型変数の境界を記述しなければならない。独立した2つのクラスならば。独立じゃない従属関係にあるクラスならば型変数が共有できる。そう、エンクロージング型内部クラスだ。
public abstract class ValueType<X> { public abstract Value getMax(); public abstract Value getMin(); public class Value { public ValueType<X> getValueType() { return ValueType.this; } private X value; public Value(X value) { this.value = value; } public X getValue() { return this.value; } } }
素晴らしい。型とインスタンスの関係を見事に表現しつつ、ジェネリックに好きな値を設定できる。しかし、設計的には綺麗だとしてもInteger型のような既存の型をそのまま利用することができない。結局はInteger型のデータを表すIntegerValueType型を作らないといけない。
public class IntegerValueType extends ValueType<Integer> { private static IntegerValueType INSTANCE = new IntegerValueType(); private static final ValueType<Integer>.Value MAX_VALUE = INSTANCE.new Value(Integer.MAX_VALUE); private static final ValueType<Integer>.Value MIN_VALUE = INSTANCE.new Value(Integer.MIN_VALUE); public static IntegerValueType getInstance() { return INSTANCE; } @Override public ValueType<Integer>.Value getMax() { return MAX_VALUE; } @Override public ValueType<Integer>.Value getMin() { return MIN_VALUE; } }
IntegerValueType型は通常は単一のインスタンスがあれば事足りるのでここではGoF::Singletonパターンで表現している。エンクロージング型の内部クラス側をnewする場合は、newの手前に親となるインスタンスを書かなくてはいけない。*2
IntegerValueType.Value value = IntegerValueType.getInstance().new Value(123);
これは面倒臭い。IntegerValueTypeにValueをnewして返すメソッドでもつければ大分マシになる。エンクロージング型の内部クラスのnewの仕方を知っている人は少ないのでこれを強要するのも辛い。
仮想言語での妄想
細かいことはおいておいて、staticなフィールド、メソッドはclassに属するものなのだから、classオブジェクトから容易に参照できる言語というのは考えられるだろうか。つまり、
public class Hoge { public static final String NAME = "this class is Hoge"; public void hoge() { Class<Hoge> hogeClass = this.getClass(); System.out.println(hogeClass.NAME); } }
といったことが可能な言語を考えよう。ここでHogeクラスをextendsしたクラスPiyoを考える。
public class Piyo extends Hoge { public void piyo() { Class<Piyo> piyoClass = this.getClass(); System.out.println(piyoClass.NAME); } }
Class
public class Piyo extends Hoge { public static final String NAME = "this class is Piyo"; public void piyo() { Class<? extends Hoge> hogeExClass = this.getClass(); System.out.println(hogeExClass.NAME); } }
hogeExClassには実行時にPiyo型が格納されることだろう。コンパイル時の静的な型としては「Hoge型を継承した何か」だからhogeExClass.NAMEはコンパイル時に静的に解決するなら"this class is Hoge"だ。動的に解決できるなら"this class is Piyo"となる。