Java Generics Hell - ジェネリックな例外
Java Generics Hell アドベントカレンダー 18日目。
- 前回(16日目) 型変数のバインド
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
throws E
Java のジェネリクスの型変数は例外のthrows宣言でも用いることができる。型変数の宣言時にthrow可能な型であることを型変数の境界で示す必要がある。
public <E extends Exception> void hoge() throws E {}
上記はメソッドスコープの型変数 E を extends Exception としてみた。メソッドスコープの型変数だとthrowsに型変数を用いる意味があまりないが、型システムの挙動を見る分にはコード量が少なくて済むので都合が良い。
バインドの仕方でthrowされうる例外が変わる、例外が変わるのでcatchするべき例外も変わる。
// IOExceptionをバインド try { this.<IOException>hoge(); } catch (IOException e) { // 略 } // SQLExceptionをバインド try { this.<SQLException>hoge(); } catch (SQLException e) { // 略 } // RuntimeExceptionをバインド // catch不要 this.<RuntimeException>hoge();
面白いのはRuntimeExceptionをバインドした場合は、catch不要となることだ。
AutoCloseableの例
ここで、java.lang.AutoCloseableを見てみよう。通常、メソッドのthrowsは具象型が用いられる。
public interface AutoCloseable { void close() throws Exception; }
例えばjava.io.ByteArrayInputStreamでは
public class ByteArrayInputStream extends InputStream { // 略 public void close() throws IOException { } }
となっていて、ByteArrayInputStreamの具象型を扱っていてもclose()を呼ぶ際にIOExceptionが発生する扱いとなる。
byte[] buf = new byte[100]; try (ByteArrayInputStream bais = new ByteArrayInputStream(buf)) { // 略 } catch (IOException e) { // close() が throws IOException のため }
throws Exceptionではなくthrows IOExceptionなのは、例外がメソッドからの出力であるため、より狭い、より具体的な型に絞っても良いからである。このあたりはジェネリクス以前からのJavaの型システムの機能性だが、ジェネリクスのワイルドカードの話にも似ている。
というか、オーバーライドしてthrowsなくしておけばよかったじゃないか、という話もあるんだが、APIの歴史的な後の祭り。Java 1.0 のときに thrwos IOExceptionとしてしまったので、これをなくすとthrowされないIOEexceptionをcatchするコードがコンパイルエラーになってしまうので残されているのである。Javaは到達不能コードをコンパイルエラーにする方針なのでしょうがない。
話がそれた。
ここで言っておきたいのは、AutoCloseable型変数に一度代入してしまえば、そのclose()はthrows Exceptionだということだ。
byte[] buf = new byte[100]; ByteArrayInputStream bais = new ByteArrayInputStream(buf); try (AutoCloseable ac = bais) { // 略 } catch (Exception e) { // IOException ではなく Exception になる }
この点がジェネリクスなしでの不都合ということになる。
例外をインスタンス型変数にした場合
ではここにAutoCloseableとの対比としてinterface Foo を以下のように定めてみよう。
public interface Foo<E extends Exception> { void foo() throws E; }
これをimplementsするclassは以下のようになる。
public class Bar implements Foo<IOException> { @Override public void foo() throws IOException {} }
オーバーライドするにあたって throws IOException となった。
これでパラメタライズドタイプを用いて以下のように書ける。
Bar bar = new Bar(); Foo<IOException> foo = bar; try { foo.foo(); } catch (IOException e) { // IOExceptionで済む }
とはいえ、Fooがパラメタライズドタイプになって鬱陶しい。
なお、Foo<RuntimeException>であればcatch不要となる。
まとめ
例外のthrows宣言に型変数を用いることができるが、正直、あんまり活用できるポイントはない。
Java Generics Hell - 型変数のバインド
Java Generics Hell アドベントカレンダー 16日目。
- 前回(15日目) ワイルドカード落穂ひろい
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
メソッドスコープの型変数
メソッドスコープの型変数へのバインドについては以前も書いたが再掲しておこう。
使用する場合は、通常はバインド型が推論されるのでそのまま呼び出せば済む。
Set<String> set = Collections.singleton("hoge");
型変数にバインドする型を明示する場合はメソッドの手前に山括弧で指定する。
Set<String> set = Collections.<String>singleton("hoge");
メソッドスコープの型変数については明示的にバインドしなくても概ね型が推論される点は知っておくと良い。これはジェネリクスが導入されたJava5当初からある機能だ。「型推論」というと変数宣言の際の型推論ばかりが挙げられがちだが(変数宣言時の型推論についてはJava10 で導入)変数宣言時のみならず、型が推論される部分はいくつかある点は注意されたい。
インスタンススコープの型変数
インスタンススコープの型変数の場合、バインドの仕方は主に2種類ある。ひとつはnewによるバインドで、もうひとつは継承によるバインドだ。
newによるバインドはおなじみであろう。
new ArrayList<String>();
といった場合の<String>の部分である。
インスタンススコープの型変数のバインドもJava7以降では型推論を用いることが出来る。
List<String> list = new ArrayList<>();
<>を書くことで型推論を用いますよ、と明示する。これがないとraw型扱いとなってしまう。この<>をダイヤモンドオペレーターと呼ぶ。
ダイヤモンドオペレーターは、Java5からあるメソッドスコープの型推論の機構をそのまま流用して作られている。おそらく型推論器があるから使い回すとちょっと便利じゃね?ぐらいの機能なのだろう。なお、Java8でStreamAPIを便利に扱うためにラムダ式が導入されたが、このラムダ式を便利に使うために型の推論器が強化され左辺側からの推論に強くなった。この影響で、Java7ではダイヤモンドでうまく推論できずエラーとなる場所でもJava8ならば用いることが出来ることがある。
継承によるバインド
もうひとつのバインド方法は継承の際のバインドである。
public class StringList extends ArrayList<String> { }
継承する際に親クラス(上記例ではArrayList)の型変数に対してバインドすることが出来る。GoFのStrategyパターンのような継承を前提としたデザインパターンなどではこうした継承によるバインドを使うことがままある。
本稿では深入りしないが、再帰ジェネリクスの場合はnewではバインドできず、継承でのバインドしか行えない。
また、これも深入りしないが、継承でのバインドの場合、classファイルに型変数に何がバインドされたのか情報が残るためリフレクションによって型変数に何がバインドされたのか動的に確認することもできる。出来るからってそれを使って何かをやるというシーンは稀だと思うが。
バインド出来るもの
具象型でバインド出来るのは当たり前なのだが、パラメタライズドタイプでバインドすることも出来る。これは意識せずやっていることがあるだろう。
new ArrayList<List<String>>();
この例ではList<String>というパラメタライズドタイプをバインドしているわけだ。
また、スコープ的に有効であれば型変数でバインドすることも出来る。
public class Hoge<T> { public void piyo() { List<?> list = new ArrayList<T>(); } }
上記例ではインスタンススコープの型変数Tをインスタンスメソッド内でArrayListの型変数にバインドしている。
逆にバインド出来ないものとしては共変ワイルドカード、反変ワイルドカードのキャプチャをバインドすることは出来ない。
new ArrayList<? extends X>(); // NG new ArrayList<? super X>(); // NG
これはパラメタライズドタイプでの山括弧には? extends, ? superが現れるため、見た目に紛らわしく思わず書けそうに思えてしまう。しかし、パラメタライズドタイプの山括弧とバインドの山括弧は別物であり、書けるものが異なるので意識して山括弧を見ることが重要だ。
Java Generics Hell - ワイルドカード落穂ひろい
Java Generics Hell アドベントカレンダー 15日目。
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
共変ワイルドカードと反変ワイルドカードについて書いたので、残りの話題を拾っておこう。
Unbounded Wildcard
共変でも反変でもないただのワイルドカードとして<?>を見たことがあるのではないだろうか。
言語仕様上はUnbounded Wildcardと表現される。適当に訳すと無制限ワイルドカード、だろうか。
何が無制限かというと代入が無制限に行える。
List<?> list; list = new ArrayList<String>(); list = new ArrayList<Integer>();
さて、前回、前々回の内容を把握している方であれば整合性を取るにはどうしたらよいか想像できるのではないだろうか。
- Listのメソッドの引数のEに対してはnullしか渡せない
- Listのメソッドの戻り値のEについてはObject型でしか受け取れない
おおよそ、? extendsと? superのツライところ取りみたいな形である。制限はきついが、代入は無制限なので、こうした制限でも構わなければ用いたほうがそのメソッドを利用しやすい。
Javaの標準APIでわかりやすい事例を挙げるとすればjava.util.Collections#disjointだろうか。
public static boolean disjoint(Collection<?> c1, Collection<?> c2)
指定された2つのコレクションに共通の要素が存在しない場合、trueを返す。それぞれのCollection ――これはjava.util.Listの親interfaceにあたる――から要素をObject型で参照できれば済む。
java.util.Collections#reverseは一見するとよい例のように見えるのだが
public static void reverse(List<?> list)
Listの順序を逆にするというものであるが、実は渡したlistを操作して順序を逆転させるのでadd()やset()でnullしか渡せないはずなのにどうしているかというとraw型を用いているということで例としてはあまり適切ではない。
raw型
Java5以降でJavaを学んだ人は基本的にジェネリクスの山括弧<>は省略してはいけない、警告は無視してはいけない、ということを徹底しておくとよい。
しかし、Javaは5以前、つまるところ1.0〜1.4までの間はジェネリクスがなかったわけで、構文上山括弧が用いられていなかった。Java5でそれらの時代のソースコードをコンパイルできるように後方互換性として残されたものがraw型である。
List list = new ArrayList();
現代の標準的なコンパイラ、IDEの設定では警告が出ると思う。分かってて敢えて用いている場合はメソッドにアノテーション@SuppressWarnings("rawtypes")をつけると警告を除去できる。
raw型を用いると基本的に型の安全性が保証されない。明示的にダウンキャストして用いる必要が生じるが、扱いを誤れば実行時に java.lang.ClassCastException が出ることになる。そして、それをコンパイル時点では気づくことができない。
逆に言えば、Java1.4までの時代、java.util.Listなどのコレクションフレームワークを用いたソースコードは常にClassCastExceptionとの戦いであった。現代ではClassCastExceptionに遭遇することは稀であろう。
raw型は歴史的経緯という点が大きいが、変性の制御でどうにもならないときの逃げ道としては時に便利でもある。
Java Generics Hell - 反変ワイルドカード
Java Generics Hell アドベントカレンダー 14日目。
- 前回(13日目) 共変ワイルドカード
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
反変ワイルドカード
さて、前回は共変ワイルドカードについてだった。今回は反変ワイルドカードについてである。
反変ワイルドカードは? superを用いて以下のように記述する。
List<A> listA = new ArrayList<A>(); List<? super B> listSuB = listA; // 代入可能
このlistSuBは次のような性質を持っている。
- listSuB は B型を格納することが出来る
- listSuB から取り出した型はObject型となる
前回のList<? extends A>型は、戻り値がA型であることが保証された。代わりに引数でA型オブジェクトを渡すことができなくなった。
List<? super B>型の場合は逆で、B側を引数として渡すことを保証する代わりに戻り値の型が保証できなくなるのだ。B型の親であるA型であることも保証できない。全ての型の始祖であるjava.lang.Object型としてだけget()することが可能となっている。
反変ワイルドカードの意義
メソッド内部では、共変ワイルドカードで渡されたオブジェクトから値を取ることが出来るのであった。対して、反変ワイルドカードで渡されたオブジェクトには値を渡すことが出来るのである。
考え方としてはサプライヤー、つまり、そのオブジェクトから何か値が供給される場合、? extends を用いる。対してコンシューマー、そのオブジェクトに値を渡して消費させる場合は? superを用いると良い。
事例
ワイルドカードの宝庫であるStreamAPIからjava.util.function.FunctionクラスのandThenを例に挙げよう。
以下ではclass A, B, C はAが親、Bが子、Cが孫である。またclass X, Y, Z ではXが親、Yが子、Zが孫である。
まずFunction<T,R>のインスタンス型変数はTが入力の型(汎用的にTypeの頭文字のTだろう)、Rが結果の型(これはResultの頭文字Rだろう)となっている。ここで、T型を受け取り、B型を返すFunction<T, B>を考えよう。
ここでandThenのメソッドシグニチャは
public <V> Function<T,V> andThen(Function<? super R,? extends V> after) { ... }
であり、V型はメソッドスコープの型変数である。ここでは簡単のためV型はY型としよう。
そしてFunction<T, B>であるので、
public Function<T,Y> andThen(Function<? super B,? extends Y> after) { ... }
と考えて良い。T型を受け取り、B型を返すFunction<T, B>に、B型を受け取りY型を返すFunction<B, Y>を与えると、くっつけてT型を受け取りY型を返すFunction<T, Y>にすることができる、というわけだ。
ここでandThenの実装コードを見てみよう。
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); }
RにBを、VにYをバインドして、実際の型のイメージで書き下ろすと以下のようになる。
default Function<T, Y> andThen(Function<? super B, ? extends Y> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); }
ラムダ式で書かれているが、T型の引数tを受け取ると、Function<T, B>のにtを渡してapply(t)の結果Bをさらにafterのapplyに渡す。今、Function<T, B>であるから、afterはB型を受け取って処理できる必要がある。
しかし、afterはB型だけではなく、親のA型を受け取ることが出来ても良い。しかし、孫のCしか受け取れないようではB型が渡されてくると困る。これが? super Bの効果。
そして、afterはY型を返さなくてはならない。しかし、実はZ型だけを返す実装でも良い。しかし、Yの親のXを返しては困る。これが? extends Yの効果。
このように、andThen(Function<B, Y> after)に対してandThen(Function<? super B, ? extends Y> after)としたほうが、afterの実装がより広く取ることができる。
Java Generics Hell - 共変ワイルドカード
Java Generics Hell アドベントカレンダー 13日目。
- 前回(11日目) 型変数の境界
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
変性
パラメタライズドタイプについては以前に書いたが、ワイルドカードについては後回しということにしていた。
<>の内部については非変性であるということは何度か述べてきたが、特定の制限下で共変性、あるいは反変性をもたせることは出来るだろうか?
共変性
パラメタライズドタイプが共変である場合、何が問題になるのだったか再確認しよう。以下ではclass Aを親とし、class Bが子供という関係とする。パラメタライズドタイプを共変とするとき? extends を用いて以下のように記述する。
List<B> listB = new ArrayList<B>(); List<? exnteds A> listExA = listB; // 代入可能
ここでList<B>型の機能性について確認しておこう。
- listB は B型を格納することが出来る
- listB から B型を取り出すことができる
比較用に List<A>型のlistAがあったとして、この機能性についても確認しよう。
- listA は A型を格納することが出来る
- listA から A型を取り出すことができる
となるのであった。問題となるのはA型を格納の部分で、リスコフの置換原則に従うならList<A>型を継承するならList<A>型の機能すべてを子は満足させないといけないのであった。List<B>はList<A>の機能を満足させることができない。B型しか格納することが出来ず、A型を格納できないのだから。よってList<B>型をList<A>型の子として暗黙に代入可能としてしまうと問題がおきるのであった。
ここで、制約、つまり、格納する機能をそもそも使わないという前提をおいたら整合性が取れるのではないか?つまり
- listExA は格納することが出来ない
- listExA から B型を取り出すことができる
という条件であれば、listBをlistExAに代入できても矛盾は生じない。
格納することが出来ないとはどういうことか?これは引数に型変数が用いられているものを用いないということだ。例えばjava.util.Listであれば
public boolean add(E e) { ... }
は引数に型変数Eが用いられているので listExA.add( ... ) を使わせなければいい。
矛盾するものを使わせなければ矛盾しないということになる。
キャプチャ
さて、おおまかな方針は述べたものの、実は多分に方便が混ざっていて正確ではない。概要を踏まえて詳細に入ろう。
List<? extends A>型では型変数Eは? extends Aとなる。Aというわけではない。この? extends Aはキャプチャ(capture)と呼ばれる。このキャプチャという用語、コンパイルエラーで見かけることがある。
The method add(capture#1-of ? extends A) in the type ArrayList
is not applicable for the arguments (A)
キャプチャ ? extends Aの性質は以下のようなものになる。
- A型 を ? extends A に変換できない
- ? extends A はA型に暗黙に安全に変換できる
このため、List<? extends A>型のadd(E)はEにキャプチャ? extends Aがバインドされ、addの引数に? extends A型に変換可能なものしか受け付けない。しかし、A型は? extends Aに変換できないのでaddの引数にA型は渡せない、というわけだ。ただし、add()に何も渡せないかというと例外がある。
? extends A にキャスト可能なのはnullリテラルだ。前回 nullは全ての型にキャスト可能という話をしたが、キャプチャに対してもキャスト可能である。なのでadd(null)だけは可能となっている。
ここで、? extends A はA型に暗黙に安全に変換できるので、getについては
A a = listExA.get(0);
といった形で使用することができる。
共変ワイルドカードの意義
List<? extends A>型にはList<A>型やList<B>型を代入することが出来る。これをメソッドの引数に用いるとどうなるか。
public void hoge(List<? extends A> list) { ... }
このようにすると、hogeメソッドにはList<A>を渡すこともList<B>を渡すこともできるようになる。
hogeメソッド内部では、get()を用いることはできても、add()などで引数のListに変更を加えることはしない。
メソッド内部では、共変ワイルドカードで渡されたオブジェクトから値を取ることが出来る。
これにより、引数の型を柔軟にしつつ、型の安全性を保つことができる。これが ? extends Aの重要な意義である。
Java Generics Hell - 型変数の境界
Java Generics Hell アドベントカレンダー 11日目。
- 前回(8日目) インスタンススコープのジェネリクス
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
週末サボりました。すいません。
型変数の境界
ここまでは話を簡単にするために型変数の境界については触れずにきた。
しかし、型変数の境界についてはそこまで難しい話ではない。その型変数が何かを継承していることを制約としてつけることができる、というだけである。
public class Hoge<T extends Piyo> { }
ここで、TはPiyo型かもしくは、Piyo型を継承した何かでなくてはならない。この時、class Hogeの中で型変数T型の変数はPiyoを継承していることが保証されるので、Piyoに定義されるメソッドを呼び出すことができる。これを型変数の境界という。なお、指定できるのはextendsだけである。
public class Piyo { public void xxx(); } public class Hoge<T extends Piyo> { T piyo; public void foo() { piyo.xxx(); // ← Piyo#xxx()が使える } }
この場合、Hoge型を外から見た場合は(パラメタライズドタイプの場合のこと)、TはバインドされたPiyoの具象型として見える。
対して、Hoge型の内部としては、Tはあくまで「Piyoを継承した何か」だと思って扱わなければならない。
Tが具体的に何の型であるかinstanceofして分岐したい、と考えたのなら、それは設計がマズいと考えるべきである。端的にはうまく抽象化が出来ていないということだ。
interfaceの多重実装
Javaはclassの単一継承、interfaceの多重実装、という継承ポリシーだが、ジェネリクスで継承階層を考える際はclassかinterfaceかは区別する必要はほぼない。継承に際してclassの宣言時、interfaceの宣言時の書き方が異なるからか、この点で余計な混乱をしている人を見かけたので敢えて書くが、「継承階層」を考える際は一緒くたでいい。一緒くたで考えるとJavaの継承階層というのは、多重継承ということになる。
型変数の境界には、複数のinterfaceを継承していることを制約とすることもできる。
public class Hoge<T extends Cloneable & AutoCloseable> { }
記法は以上の通りで & で複数のinterfaceを並べることができる。
再帰的な宣言
型変数の境界にパラメタライズドタイプを用いることも出来る。
public class Hoge<T extends Comparable<String>> { }
パラメタライズドタイプのバインドに宣言中の型変数を使うことも出来る。
public class Hoge<T extends Comparable<T>> { }
ついでに挙げれば宣言中のcalss 自体も使うことが出来る。
public class Enum<E extends Enum<E>> { }
java.lang.Voidの憂鬱
型変数のバインドに際して、「使用しない」というケースが稀にある。こうした時、一般にはjava.lang.Void型で型変数を潰す。
Map<String, Void> map = new HashMap<String, Void>();
java.lang.Voidはvoidのラッパー型なので、型変数にvoidを指定したいときにも用いられる。しかし、JavaのVoidはあくまでもただのclassでしかなく、特別な型というわけではない。そのため、型変数に境界が指定されている場合、Voidをバインドすることができない。
public class Hoge<T extends Piyo> { } Hoge<Void> hoge = new Hoge<Void>(); // ← NG
これはVoidがextends Piyoではないことによる。こうした型変数に境界があるケースで、かつ、Voidのようなもので潰すことが想定されるような場合は、自力でVoid用のクラスを定義してやる必要がある。Scalaなどでは「全ての子」に相当する型が規定されているのでこうした苦労がない。
ちなみに、Javaのnullは構文的には全ての型にキャスト可能なnull型の唯一のリテラルという扱いで、イメージ的にはnull型は全ての型の子ということになる。しかしこのnull型(null type)というのは言語仕様上現れはするものの、Javaのコードでこのnull型を明示的に扱うことはできない(リフレクションでも扱えない)ので型変数をnull型で潰すような使い方はできない。通常はnull型というのは概念上のもの、ぐらいに思っていて良いだろう。(JVMの中とかでは現れるかもしれない)
Java Generics Hell - インスタンススコープのジェネリクス
Java Generics Hell アドベントカレンダー 8日目。
- 前回(7日目) メソッドスコープのジェネリクス
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
ようやくクラスの型変数の話にたどり着いた。なかなか話題が多くて大変なのである……。
インスタンススコープ
ここのところしつこくJavaのジェネリクスの型変数には2種類のスコープがあることを挙げてきた。
- メソッドの範囲で有効な型変数
- クラスのインスタンスの範囲で有効な型変数
前回、メソッドスコープのジェネリクスについて取り上げた。入門書などではあまり触れられていない構文かもしれないが、機能的にはメソッドスコープのほうが単純だと思うので先にメソッドスコープを取り上げた次第である。
メソッドスコープではメソッドの IN / OUT つまり、引数と戻り値を型変数を用いて関連性を示すことができるということを書いた。インスタンススコープではあるクラスのインスタンスに対しての IN / OUTの範囲で型変数を用いて関連性を示すことができる。つまり、あるインスタンスを外から眺めた場合に
といったところで型変数によって汎化することができる。エンクロージングインスタンスというのは内部クラスのインスタンスのことで、気になる人は拙稿 Javaのクラス宣言5種+αあたりを参照して欲しい。通常はあまり意識することはないだろう。
クラスの内部のコードとしては、ローカル変数宣言などでも型変数を用いることができるし、型変数へのバインドに型変数を用いることもできる。
ジェネリクスの事例
ジェネリックなクラスの「利用」に関してはjava.util.Listなどのコレクション・フレームワークがよく挙げられる。オブジェクトを保持する目的の汎用ライブラリだ。また、Java8で導入されたStream APIでも多用されている。いわゆる関数型プログラミングを行うライブラリといったところだろうか。
ジェネリクスについて学ぶ際、どこを目標にするかという問題がある。
- 型変数のあるメソッド、型変数のあるクラスの既存ライブラリを使うことができる
- 型変数のあるメソッド、型変数のあるクラスを設計することができる
入門レベルだとまずは前者の「利用」が目的となるだろう。しかし、「利用」を確実なものとしようとすれば、メソッド宣言、クラス宣言から型変数がどういう意味合いで宣言されているか読み解ける必要が生じるだろう。そうしたコードリーディングと、自身のプログラミングの試行錯誤でもってジェネリックなメソッド、クラスを設計することができるようになるだろう。
まずは利用、そこから設計に進むのが順当だろうと思う。本稿では軽く、型変数を用いたクラス設計について大雑把な指針を示しておこう。
ジェネリクスで汎化するにあたって、その型変数の個別の具体的な機能に依存することはできない。ここまででまだ取り上げていないが型変数の境界を利用することで、継承階層によってある程度対象となるクラスを具体化することもできるが、境界がない場合は「全てのクラスに対して行える操作」の範囲で型変数の型を取り扱わなければならない。
具体的には
- 正確にはjava.lang.Objectに宣言されるメソッド
- 参照の代入
- 参照の有無 (要するにnullかそうでないか)
ということになる。
ジェネリクスの事例としてコレクションフレームワークが挙げられるのは、まさにこの「参照の代入」「参照の有無」という機能を中心としているのでジェネリクスの題材として具合が良いのである。
データ抽象
言語によっては予め言語に特別な意味を持った型が組み込まれている。Javaでもプリミティブ型や配列といった特別な型が存在する。
プログラマが独自に定義できるデータ型、つまるところJavaでいうクラスのようなものだけでその言語全ての型が取り扱われるならば、それらは同じように抽象的に扱えるわけである。そうした概念を「データ抽象」という。
データ抽象とジェネリクスは関連が深い。抽象化されているからジェネリクスの型変数で抽象的に同じように扱えるというわけである。しかし、Javaの場合はプリミティブ型や配列は特殊な型として言語設計されているため、これらをジェネリクスで一般的なクラスと同列に扱うことができない。
演算子オーバーロード
データ抽象と関連深いテーマに演算子オーバーロードがある。プログラマによって+や-といった「演算子」を型ごとに独自に定義することができる機能である。
C++ではintなどのプリミティブ型に対してもジェネリックなコードを書くことが出来るが、こうした型に対して行える「抽象的な操作」こそ、演算子なのである。つまり、intの値が2つ与えられた時、ふたつを「足す」というコードを書くことが出来るが、同じようにdouble型に対しても「足す」ことが出来る。同じ操作が出来るから、ジェネリクスによって汎化させることができる。
この「同じ操作」を表現するためのインターフェースとして現れるのが「演算子オーバーロード」である。単なるメソッド呼び出しのシンタックスシュガーというわけではない。
Javaの初期に演算子オーバーロードが導入されなかった理由としては、初期のJavaにジェネリクスが間に合わなかったことが挙げられるだろう。ジェネリクスなしでの演算子オーバーロードはコードの見た目を整えるシンタックスシュガー的な意味合いだけになってしまう。初期のJavaの設計段階で掲げられたKeep it simpleという標語を考えれば、データ抽象的な意味合いを持たない演算子オーバーロードなどはまさにKeep it simpleのために捨てられるような機能であったと思う。
今のところ、Javaでもプリミティブ型に対してもジェネリクスで扱えるように拡張しようという計画はあるが、演算子オーバーロードを導入しようという話は聞かない。
まとめ
- インスタンススコープのジェネリクスではインスタンスのメソッド操作、フィールド操作といった広い範囲で型の関連性を示すことが出来る
- 型変数で扱う型といえども、それらに対して「同じ操作」が出来る前提で抽象的な設計をする
- もっと根源にある「同じ操作」とは「代入」であり、他にもnullとの比較やjava.lang.Objectに定義されるequalsやhashCodeなどを用いれる
- このためコレクションフレームワークと相性がよい
- 型変数の境界を用いればより具体的な型で抽象化するようなこともできる(後に取り上げる予定)
- データ抽象という概念と、そのための演算子オーバーロード(Javaでは導入されていない)
次回は型変数の境界あたりを取り上げたい。