はじめに
今回記事にしたジェネリクスは、Java Gold資格勉強をする中で自分が苦戦した項目です。
普通の型をややことなる部分が引っかかるポイントと感じ備忘としてまとめることにしました。
ジェネリクスの非変性
ジェネリクスの非変性について引っかかったポイントを書いていきます
その前に少しだけジェネリクスに付いて補足します。
ジェネリクスとは、型の定義時にダイヤモンド演算子"<>"の中に任意の型を入れることで型の抽象化をする仕組みです。
new ArrayList<String>();
ジェネリクスの概要については、良質な記事が様々ありますのでここでは割愛させていただきます。
スーパークラスへの代入
本題に入ります。
以下のコード、実行するとどうなるでしょうか。
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList;
こちらを実行するとコンパイルエラーになります。
変数であればインタフェースやスーパークラスの型で扱うことはありますが、ジェネリクスだとそれができない。
それはなぜでしょうか。
仮にエラーが出ないならどうなるか、見てみましょう。
ジェネリクス型にサブクラスのジェネリクス型をもし代入ができたら?
以下ののコードは実際にはnumListへの代入時にエラー"incompatible types"となりますが、変数の感覚でクラスを扱ったとして記載しています。
本来サブクラスのジェネリクス型の代入は行えません。
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // ←仮にエラーが出なかった場合、numListの実体はList<Integer>となる。
// numListはList<Number>型なので、NumberのサブクラスであるDoubleもaddできるはず?
numList.add(1.1); // Double型を追加
// numListはList<Number>型なので、NumberのサブクラスであるIntegerを取得できるはず?
Integer value = numList.get(0); // 中身は1.1
"numList"の型はList<Number>ですがその実体はList<Integer>なんてことになってしまうと、リストへの追加や取得時の型安全性がめちゃくちゃになってしまいますね。
ちなみに、上記コードの2行目を削除して本来のList<Number>として扱うと、numList.get(0)の行でやはりコンパイルエラーとなります。
Double型である要素を取得してInteger型に代入しようとしたためです。
List<Number> numList = new ArrayList<>();
// numListはList<Number>型なので、NumberのサブクラスであるDoubleもaddできるはず?
numList.add(1.1); // Double型を追加
// numListはList<Number>型なので、NumberのサブクラスであるIntegerを取得できるはず?
Integer value = numList.get(0); // incompatible types
引っ掛かったポイント
普通の変数と同様に考えてしまうといけませんでした。
Integer integer = new Integer(1);
Number num = integer;
num = new Double(1.1); // OK numに対して、Double型の 1.1を代入
普通の変数とジェネリクスとの本質的な違いは、ジェネリクスが「コンパイル時の型チェックを制御するもの」という点にあります。
通常の変数は、どのようなオブジェクトを代入しても「変数自身の型」に従って処理が行われます。たとえば、Number 型の変数には Integer や Double を代入できますが、実際の処理は「Numberとして」行われるため、安全性が保たれています。つまり、変数の型が処理の基準になるので、サブクラスのインスタンスを入れても問題が起きません。
一方で、ジェネリクスは "<>"(ダイヤモンド演算子)を使って、扱う型そのものを定義します。たとえば
List<Integer>は、「このリストには Integer だけが入る」という契約のもとで作られています。ここで List<Number> に List<Integer> を代入できてしまうと、List<Number> に対して Double を追加することが可能になってしまいます。しかし元の実体は List<Integer> なので、これは矛盾を引き起こします。結果として、型の整合性が崩れてしまい、型安全が保証されなくなってしまいます。
このように、仮に代入できてしまうと型安全性が保てなくなることから、ジェネリクスでは定義したクラスそのものしか代入を許可していません。
通常のクラスシステムはサブクラスをスーパークラスとして利用できますが、それに対してジェネリクスの型を特定してサブクラスやスーパークラスとして扱えないしくみを非変性といいます。
サブクラスをそれでも使いたい
とはいえ、クラスの仕組みとしてサブクラスなどを扱いたい場面は当然あるとおもいます。
(ユーティリティクラスとしてメインクラスで定義して、実際に扱うときはサブクラスで扱うなど・・・)
そういった場合は、ワイルドカードの要素を用いることで、対応可能です。
List<Integer> intList = new ArrayList<>();
intList.add(1);
// 上限境界ワイルドカード
List<? extends Number> numList = intList;
numList.get(0);
// 下限境界ワイルドカード
List<? super Integer> intList = new ArrayList<Number>();
numList.add(1);
ただし、これも完全に自由が効くわけではなく取得(読み込み)のみが可能・または設定(書き込み)のみが可能となっています。
ジェネリクスと同様に型安全性に基づいた制約なのですが、この記事ではそういった機能があることの紹介にとどめさせていただきます。
まとめ
ジェネリクスは制約が通常の変数(型システム)と異なることがわかりました。
型システムのサブクラスを知っているからこそ、学習時に引っかかりやすい節があるかもしれません。
参考
参考サイト
サンプルコードのデバッグに活用させていただきました。
https://codehs.com/explore/sandbox/java
参考文献
以下の書籍およびサイトを参考にしています。
徹底攻略Java SE 11 Gold問題集[1Z0-816]対応
https://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-MJ16-Kolling.pdf
https://qiita.com/ysn/items/66e225004bf656f012c7
生成AI
推敲と誤りの指摘に生成AIを利用しています。