ジェネリクスの基礎とクラス設計への応用
DESCRIPTION
JJUG CCC 2013 Fall #ccc_h1TRANSCRIPT
ジェネリクスの基礎とクラス設計への応用
Twetter : @nagiseはてな : Nagise
所属 java-ja 北陸エンジニアグループ
2013年版
導入
Java 1.4までのコード
ArrayList list = new ArrayList();
list.add("hoge");
String s = (String) list.get(0);
ジェネリクスのない世界
Object型からのダウンキャストが必要キャスト失敗は実行時例外 ClassCastException
動かしてみないと間違いに気づかないときに動かしても気づかないドキュメントなどで型を明示
導入
Java 1.5からのコード
ArrayList<String> list =
new ArrayList<String>();
list.add("hoge");
String s = list.get(0);
ジェネリクス以後の世界
コレクション APIでダウンキャストが不要に
コンパイル時点で型の間違いに気づく IDEによってはコンパイル前に気づく型システムが型を把握してくれる必修事項が増えました
今日のキーワード
Generics Hellじぇねりくす へる
日本風に言えば「まったく Javaのジェネリクスは地獄だぜ!」
略して「ヘル -い」などと表現したりする
入門編
ジェネリックな APIを利用するのに必要な知識を身に着ける
2種類のスコープ3種の山括弧代入について
ジェネリクスの2種類のスコープメソッドの中でのみ有効なジェネリクス
public static <T> void hoge(T t) { … }
インスタンスの中でのみ有効なジェネリク
ス
class Hoge<T> {
T t ;
...
}
ジェネリックメソッド
メソッドの引数と戻り値の型の関係を表すメソッドの複数の引
数の型の関係を表す
ジェネリックメソッドの例
java.util.Collections クラス
リスト内に出現する指定された値をすべてほかの値に置き換えます。
public static <T> boolean replaceAll(List<T> list, T oldVal, T
newVal)
3つの引数が List<T> 型、 T 型、 T型という関係性
ジェネリックメソッドの例
java.util.Collections クラス
指定されたオブジェクトだけを格納している不変のセットを返します
public static <T> Set<T> singleton(T o)
引数が T型で、戻り値が Set<T>型という関係性
ジェネリックメソッドの例
java.util.Collections クラス
デフォルトの乱数発生の元を使用して、指定されたリストの順序を無作為に入れ替えます。
public static void shuffle(List<?> list)
引数1つだけなので関連性を示す必要がない。無駄に型変数を定義せず、ワイルドカードを利用してList<?>型として宣言するほうが使い勝手が良い
ジェネリックメソッドの呼び出し方
List<String> list = new ArrayList<String>();list.add("hoge");Collections.<String>replaceAll(list, "hoge", "piyo");
List<Integer> intList = new ArrayList<Integer>();intList.add(42);Collections.<Integer>replaceAll(intList, 42, 41);
staticの場合はクラス名 .<バインド型 > メソッド名 (…)インスタンスメソッドの場合は変数名 .<バインド型 > メソッド名(…)thisに対するメソッド呼び出し時、明示的にバインドしたい場合はthisを省略できない
インスタンスの I/O
複数のメソッド間の引数・戻り値の型の関係性公開されてい
るフィールド内部クラス
ジェネリックなインスタンスの例
java.util.ArrayListの例
public boolean add(E e)public E get(int index)
複数のメソッド間で型の関連性を表現している
ジェネリクスと構造化
ジェネリクスメソッド、ジェネリッククラスともにある囲いを作って、その内外のやりとりをする場合のデータ型を型変数で抽象化する。
そのため、ジェネリックな設計をするためには前提としてきれいな構造化をする必要がある。
文法のはなし
public class Syntax<T>implements Iterable<String> {
public <X> void hoge(X x) {List<X> list = new ArrayList<X>();list.add(x);
}@Overridepublic Iterator<String> iterator() {
return null;}
}似て非なる<>を色分けしてみました
3種類の<>型変数の宣言class Hoge<T> {}public <T> void hoge() {}
型変数のバインドnew ArrayList<String>();class Hoge extends ArrayList<String> {}Collections.<String>replaceAll(
list, "hoge", "piyo");
パラメータ化された型( parameterized type)
List<String> list;
型のバインド
ジェネリックメソッドの例
宣言側 仮型引数( type-parameter)
public static <T> boolean replaceAll(List<T> list, T oldVal, T
newVal)
利用側 実型引数( type-argument)
Collections.<String>replaceAll(list, "hoge", "piyo");
型のバインド
ジェネリッククラスの例
宣言側 仮型引数( type-parameter)
public class ArrayList<E> {...}
利用側 実型引数( type-argument)
List<String> list = new ArrayList<String>();
(参考 ) 仮引数と実引数
メソッドの仮引数と実引数との対比
宣言側 仮引数( parameter)
public void hoge(int index){...}
利用側 実引数( argument)
hoge(123);
型推論
ジェネリックメソッド
正書法
Collections.<String>replaceAll(list, "hoge", "piyo");
型推論
Collections.replaceAll(list, "hoge", "piyo");
ダイヤモンド演算子
ジェネリッククラス
正書法
List<String> list = new ArrayList<String>();
ダイヤモンド演算子
List<String> list = new ArrayList<>();
推論器
ジェネリックメソッドの型推論とダイヤモンド演算子は同じ機構を利用している
Java7では推論が弱いが、 Java8ではラムダのために型推論が強化された
継承によるバインド
public class StringList extends ArrayList<String> {}
というクラスを作ると
StringList list = new StringList();list.add("hoge");String str = list.get(0);
といったように、型変数のないクラスになる
複雑になる前に
Map<String, Map<Integer, List<Hoge>>>
みたいにジェネリクスが複雑化するようなら適度なレベルでした継承クラスを作ることでシンプルでわかりやすくなる
クラスを作るのをサボらない
サンプルのクラス
パラメータ化された型の代入互換性B[] arrayB = new B[1];
A[] arrayA = arrayB;arrayA[0] = new B2();
→ ArrayStoreException が発生
List<B> listB = new ArrayList<B>();List<A> listA = listB;listA.add(new B2());
→ コンパイルエラー
異なる代入互換性
B の集合型 ArrayList<B>は
A の集合型 ArrayList<A>の
代理をできない
ワイルドカードの使用
ArrayList<? extends B> の範囲
ArrayList<? super B> の範囲
<? extends ~ >の入力制約List<? extends B>には add()できない List<C>型を代入 B型を add()とする List<C>に B型が add() ←される 矛盾
get()は B型の戻り値を返す
<? super ~ >の出力制約
ArrayList<? super B> にはB型を add()できる
ただし get()は Object型としてしか返せない
まとめ
型変数でメソッドやインスタンスの I/Oの型の関連性を表す→ まずは綺麗なオブジェクト指向の設計を
文法をマスターするには3種類の<>を意識する
代入互換性は訓練あるのみ
中級編
ジェネリックな APIを設計するのに必要な知識を身に着ける
型変数の宣言再帰的ジェネリクス内部クラスリフレクション
型変数の境界
class Hoge<T extends B>
型変数の宣言時には境界を指定できる
new Hoge<A>(); ← NGnew Hoge<B>(); ← OKnew Hoge<B2>(); ← NGnew Hoge<C>(); ← OK
型変数の境界
class Hoge <T extends A & Serializable>
& で繋いでインタフェースを境界に加えることができる
再帰ジェネリクス
public abstract class Hoge<T extends Hoge<T>> {
public abstract T getConcreteObject();}
Hoge型の型変数 Tは Hoge型を継承していなくてはならない
再帰する型変数へのバインド
new でバインドできない
Hoge<?> hoge = new Hoge<Hoge<...>>();
再帰ジェネリクスは継承で扱う
public class Piyo extends Hoge<Piyo> {...}
再帰ジェネリクスの効能
public class Piyo extends Hoge<Piyo> {@Overridepublic Piyo getConcreteObject() {
return this;}
}
このようにすると
Piyo piyo = new Piyo();Piyo piyo2 = piyo.getConcreteObject();
再帰ジェネリクスの効能
public abstract class Hoge<T extends Hoge<T>> {
public abstract T getConcreteObject();}
親クラスで定義されたメソッドなのに…子クラスの具象型を扱うことができる !
再帰ジェネリクスの使用例
java.lang.Enum クラスの例
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
public final int compareTo(E o) {…}}
再帰ジェネリクスの使用例
enum SampleEnum {ONE,TWO,
}
に対して
SampleEnum.ONE.compareTo(SampleEnum.TWO);
は安全に compareToできる。他の enumと比較しようとするとコンパイルエラー
内部クラスのジェネリクス
内部クラスは外部クラスの型変数を利用できる
public class Outer<T> {public class Inner {
T genericField;}
}
内部クラスの newの仕方
内部クラスの newの仕方知ってますか?
Outer<String> outer = new Outer<String>();Outer<String>.Inner inner = outer.new Inner();
内部クラスは外部クラスのインスタンスに紐づく。ジェネリクスは「インスタンスの I/Oで型の関連性を示す」の I/Oには内部クラスも含まれます
内部クラスの利用例
public class SampleList<T> extends ArrayList<T> {
@Overridepublic Iterator<T> iterator() {
return super.iterator();}
public class SampleIteratorimplements Iterator<T> {// 略
}}
そもそも内部クラスの使いどころが難しいですが。
リフレクション
• 型変数に格納されるインスタンスの型情報は欠落する(イレイジャ方式)
• メソッド引数やフィールド宣言に用いられるパラメタライズド・タイプの型情報は欠落しない
http://d.hatena.ne.jp/Nagise/20121226/1356531878Javaのジェネリクスとリフレクション
リフレクション
java.lang.reflect.Type を利用
実装クラス /サブ interface•Class – いつもの•GenericArrayType – ジェネリックな配列。きもい•ParameterizedType – パラメタライズドタイプ•TypeVariable<D> - 型変数•WildcardType – ワイルドカード
ダウンキャストして使う
リフレクション
java.lang.reflect.Type を返すメソッドを利用する
•Class # getGenericSuperclass() : Type•Class # getGenericInterfaces() : Type[]•Method # getGenericParameterTypes() : Type[]•Method # getGenericReturnType() : Type•Field # getGenericType() : Type
などなど
まとめ
ジェネリクスが複雑になりすぎないように継承でのバインドの利用を検討する
再帰ジェネリクスを応用すればサブクラスで具象型を扱える
内部クラスもスコープ範囲内
リフレクションでフィールドやメソッド引数・戻り値のパラメタライズドタイプの型をとれる
上級編
ジェネリックな APIの設計例利用側のコードとともによい設計を考える
new T()したい
動機
DBのマッパーやWebシステムのControllerのようなフレームワークを作った場合、取り扱うオブジェクトの型をジェネリクスで定義し、インスタンスを生成してデータをマッピングして渡したい
コンストラクタ
Javaのオブジェクト指向の一般論おさらい
interfaceや親クラスとして振る舞うことが要求される
具象型の特有の情報を押し込める場所はコンストラクタ
コンストラクタ
コンストラクタの引数の形は継承で制約できません
やりたければ Factoryクラス作れ
一般にインスタンス生成は抽象化しにくい
Factoryの実装例
interface HogeFactory<T extends A> {/** デフォルトコンストラクタ的な */T newInstance();
}
インスタンスの生成に必要なデータを Factoryで制約
public class HogeTemplate {public <T> T template(HogeFactory<T>
factory) {return factory.newInstance();
}}
こうすればインスタンスの生成は可能になる、が面倒
妥協例
public <T extends A> T template(T obj) {try {
return (T)obj.getClass().newInstance();} catch (InstantiationException |
IllegalAccessException e) {throw new RuntimeException(e);
}}public <T extends A> T template(Class<T> clazz) {
try {return (T)clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);}
}デフォルトコンストラクタがあることが前提
C#の例
class MyGenericClass<T> where T : new() {}
T型にデフォルトコンストラクタがあることという制約。デフォルトコンストラクタがあることを保証させることでインスタンス生成を可能とする。
この制約はダサい制約ではあるが妥当な妥協点か。Javaでもリフレクションで生成する場合にデフォルトコンストラクタがあることという制限をつけがち。
継承によるバインドの場合Class # getGenericSuperclass() という福音
継承によるバインドであれば、型情報から型変数に何がバインドされたかを知ることができる
Type を ParameterizedType にキャストgetActualTypeArguments() でバインドされた実体
実体の Typeを Classにキャストして newInstance() ただし、デフォルトコンストラクタがあるものとする
http://d.hatena.ne.jp/Nagise/20130815
ジェネリックなデザインパターン
Template Methodパターン
虫食いのテンプレートを作る。左図の処理 ABCは abstractメソッド。サブクラスではオーバーライドして実装を書く。
ここに先の継承によるバインドとバインドされた型のリフレクションでのnewInstance()を複合すると便利
処理 A
処理 B
処理 C
まとめ
New T()したくなるシチュエーションには継承によるバインド+リフレクションを使えないか検討する
Template nethodパターンと複合させるとフレームワークに応用できるかも
変態編
何かに使えるかもしれない
再帰での相互参照
二つの型の具象型が、相互に相手の具象型を知っている
class Hoge<H extends Hoge<H, P>, P extends Piyo<H, P>>
class Piyo<H extends Hoge<H, P>, P extends Piyo<H, P>>
実装は
class HogeImpl extends Hoge<HogeImpl, PiyoImpl>class PiyoImpl extends Piyo<HogeImpl, PiyoImpl>
相互再帰+1
汎用型変数 Tを追加してみるclass Hoge<T, H extends Hoge<T, H, P>,
P extends Piyo<T, H, P>>class Piyo<T, H extends Hoge<T, H, P>,
P extends Piyo<T, H, P>>
実装クラスclass HogeImpl<T> extends
Hoge<T, HogeImpl<T>, PiyoImpl<T>>class PiyoImpl<T> extends
Piyo<T, HogeImpl<T>, PiyoImpl<T>>
やりすぎです
内部クラスでグルーピング
二つのクラスを囲うクラスを作ってHogeと Piyo …を内部クラスにすれば !
public abstract class Outer<H extends Outer<H, P>.Hoge, P extends Outer<H, P>.Piyo> {
public abstract class Hoge {public abstract P getConcretePiyo();
}public abstract class Piyo {
public abstract H getConcreteHoge();}
}やりすぎです
型変数の部分適用
複数の型変数がある型を2段階に分けてバインド
public class Outer<K> {
public class Inner<V> extends HashMap<K, V> {}
}
Outer<String> o = new Outer<String> ();
HashMap<String, Integer> inner =
o. new Inner<Integer>();
型変数の部分適用応用例
http://d.hatena.ne.jp/Nagise/20110124/1295874192Javaによる高階型変数の実装
public class Hoge extends KeyGroup<Hoge> {private static final Hoge singleton = new Hoge();public static final Hoge.Key<String>
HOGE_STRING = singleton.new Key<String>();}
public class KeyValue<KG extends KeyGroup<KG>> { public <T> void put(KeyGroup<KG>.Key<T> key, T value){}
}
KeyValue<Hoge> tm = new KeyValue<Hoge>(); tm.put(Hoge.HOGE_STRING, "hoge");
当セッションは主に blogに記述した事項を再編集してまとめたものである。
Blog プログラマーの脳みそ
カテゴリー Generics を参照
http://d.hatena.ne.jp/Nagise/searchdiary?word=%2A%5BGenerics%5D
Generics Hell に遭遇したら Twitterで
@nagiseにご一報ください。
より深く知りたい場合の資料