はじめに
ここ数日改めてGenericsについて調べていたので、Genericsの概要について簡単に述べた後、以下2つの内容についてメモ程度にまとめることにした。
- Genericsのスコープ
- 型変数周りの話し
Generics概要
なにはともあれGenericsを使用することによるメリットを改めて整理したい。
それは誤った型のオブジェクトを挿入しようとするとコンパイル時に知らせてくれることである(EffectiveJava第3版第5章ジェネリックスより引用)。これにより型安全性を保証できる範囲の様々な型で同様の処理を実現することができる。
さて、具体例をコードで示していきたい。ここでは配列とジェネリックスを比較してジェネリックスで書くことの良さを認識したい。
ジェネリクスを使用せずにリストの定義、リストへの追加、取り出しを以下の通り書く。
ArrayList list = new ArrayList();
list.add("hoge");
String s = (String) list.get(0);
Object型で取り出すため明示的にキャストが必要、誤りがある場合(intで代入してStringで取り出すなど)動かしてみないと誤りに気づかない。←これ、とてもつらい。
ジェネリクスを使用したリストの定義、リストへの追加、取り出しを以下の通り書く。
ArrayList<String> list = new ArrayList<String>();
list.add("hoge");
String s = list.get(0);
コンパイル時にエラーが出る。(実行する前の段階で誤りに気づくことができる←これ、とてもうれしい)
ジェネリクスのスコープについて
ジェネリクスには二つのスコープがある。メソッドスコープとインスタンススコープである。概要をそれぞれ示す。
メソッドスコープ
まず構文を以下に示す。
public class SetInt{
public <E> Set<E> union(Set<E> s1,Set<E> s2){
// (例)s1とs2をたす処理
}
}
まず目につくのはアクセス修飾子publicの後の<E>であろう。これは型変数宣言である。この宣言により、そのメソッドの範囲内において引数と戻り値は同じ型であるという対応づけを定義することができる。メソッドスコープでのジェネリクス宣言はインスタンスメソッドはもちろん、staticメソッド、コンストラクタでも用いることができる。
呼び出し時はそのまま呼び出す場合と型を明示的に指定して呼び出す場合がある。
#####そのまま呼び出す場合
Set<Integer> result = SetInt.union(Set<Integer>,Set<Integer>);
#####型を明示的に指定して呼び出す場合
Set<Integer> result = SetInt.<Integer>union(Set<Integer>,Set<Integer>);
また、型を明示的に指定して以下のように例外処理を呼び分けることができる。
public class Hoge{
public <E extends Exception> void example() throws E {};
public void test() {
try {
this.<IOException>example();
} catch (IOException e) {
// IOException例外catch時の例外処理
}
try {
this.<SQLException>example();
} catch (SQLException e) {
// SQLException例外catch時の例外処理
}
}
}
インスタンススコープ
メソッドスコープよりも見かけることが多く、入門書等で扱うのも大抵はこちらな気がする。一例を以下に示す。
public class Stack<E> {
private E elements;
public void push(E e) {
// elementsにpushする処理
}
public E pop() {
// elementsからEを取り出す処理
}
}
宣言されている複数のインスタンスメソッド(上記では2つ)の引数、戻り値、及びインスタンスフィールドの型が同じであるという対応づけを定義することができる。
型変数周りの話
「共変、反変、不変」、及び「リスコフの置換原則」についての知識があることが前提である。前者についてはこちらの記事が分かりやすい(https://www.thekingsmuseum.info/entry/2016/02/07/235454)
以下のメモでは例として、それぞれA、B、Cというクラスがあり、それらはC extends B, B extends Aという継承関係があることとして検証する。
共変
以下のように定義することができる。
List<? extends B> extendsBList = new ArrayList<C>();
上記extendsAListは以下の性質を持つ。
- extendsBListからB型、C型の変数を取り出すことができる。
- extendsBListにnull型以外格納することはできない。
性質1はともかく、性質2には疑問が残る。以下、性質2が導き出される過程をまとめる。
extendsBListにnull型以外格納することはできないのは何故か
たとえばextendsBListに格納可能でextendsBList.add(new B());成立するものとする。しかし今一度extendsBListの初期化コードを参照していただきたい。ArrayList型で 初期化している。もし、Bをadd()できるとしたら、 ArrayList型に B型がadd()されてしまうことになり、矛盾が生じる。
そこで、List extends B>では 型の安全性が破壊されないように、add()できるのは List extends B>に代入可能な List<B>、List<C>それぞれにadd()可能なものだけしかadd()できないように制約が掛けられる。それはnull型しかないため性質2が成立する。
反変
以下のように定義することができる。
List<? super B> superBList = new ArrayList<A>();
上記superBListは以下の性質を持つ。
- superBListはB型やA型の変数を格納することが出来る。
- superBListから取り出した型はObject型である。
性質2には疑問が残る。しかし性質1が成り立つということは、get()で取り出されるオブジェクトは、B型よりも上位のオブジェクト型である可能性がある。 このことから、 super B>とした場合は 全ての型のトップに位置するObject型でしかget()したオブジェクトを受け取ることができない。格納を受け入れる代わりに返す型を特定することが出来なくなるのである。
PECS原則について
EffectiveJava第2版項目28にPECS原則について書かれている。以下引用。
関数内において、ジェネリックス型の引数の役割が「プロデューサー(Producer)」 であれば extends を、「コンシューマー(Consumer)」であれば super を用います。プロデューサーとは関数内で、何らかの値を生成(提供)する引数のことです。一方、コンシューマーとは関数内で、何らかの値を消費(利用)する引数です。この原則は Producer-Extends and Consumer-Super を略して「PECS」とも呼ばれます。
共変、反変の議論をもとにすれば、何故そうするのか理解することが出来るだろう。
Hogeクラスを例にする。
- 関数側からすれば値を生成(提供)することは、関数を使う側からすれば値を取得すること、すなわち値を取り出す場合は extends Hoge>とする。
- 関数側からすれば値を消費(利用)することは、関数を使う側からすれば値を設定すること、すなわち値を格納する場合は super Hoge>とする。
あるメソッドにおいて、そのメソッドの引数にextends、そのメソッドの戻り値にsuperを用いると、そのメソッドの実装をより広く取ることが出来る。
さいごに
記事の内容は随時見直していきたい。あと、Generics書けるようになってきたら、Genericsを用いた設計が出来るようになりたい。
参考
(https://www.amazon.co.jp/exec/obidos/ASIN/B078H61SCH/xray2000-22/)