何度か書籍で読んで、何度か作成したこともあって分かった気になっていたが、全然細かいところまで理解できていなかったので、自分でまとめてみる
総論
単語
単語 | 説明 | 例 |
Generic Type | Genericな型 | List<E> |
Type Parameter | Genericクラス宣言式における型情報 | List<E>のE |
Type Argument | Generic TypeをParameterizeする型情報 | List<String>におけるString |
Parameterized Type | Type Argumentを与えられたGeneric Type | List<String> |
Unbounded wildcard | 不明型 | ? |
Upper bounded wildcard | 上限境界ワイルドカード | ? extends E |
Lower bounded wildcard | 下限境界ワイルドカード | ? super E |
Unbounded wildcard type | unbounded wildcardを使用したParameterized Type | List<?> |
Xxxx bounded wildcard type | Xxxx bounded wildcardを使用したParameterized Type | List<? extends Number>、List<? super Integer> |
raw type | ParametereizeされていないGeneric Type | List |
命名慣習
E – Element (used extensively by the Java Collections Framework)
https://docs.oracle.com/javase/tutorial/java/generics/types.html
K – Key
N – Number
T – Type
V – Value
S,U,V etc. – 2nd, 3rd, 4th types
代表的な性質
Genericsの性質には以下の大きな2つの性質があります。
- erasure(型消去)
- Parameterized Typeはinvariant(不変)
Erasure(型消去)
GenericsはCompile時に型安全性を保障し、Compile後には型情報を消去したバイトコードを生成します。
例えば、以下のようにGenericsを用いて宣言されたクラスがあったとします。
public class GenericsClass<T> { private T field; public void setField1(T newField){ field = newField; } }
これはコンパイル後には以下のようなバイトコードにコンパイルされます。
public class GenericsClass { private Object field; public void setField1(Object newField){ field = newField; } }
したがって、実行時には何も特殊なクラスではなく、どの型でも受け入れるObject型として定義されているだけです。Compile時に型安全については保証したので、もはや具象的な方情報は必要ないからです。
加えてこのようなバイトコードとすることで、Genericsが存在する前のバージョンと互換性を保っています。
Invariant(不変)
Parameterized TypeはCovariant(共変)ではありません。すなわち、以下のようにParameterized Typeが親子関係を有していたとしても、ParameterizedされたGenericsClassに親子関係はなく、別の型として認識されます。
public class GenericsClass<T> { private T field; public void setField1(T newField){ field = newField; } } GenericsClass<Child> child = new GenericsClass<>(); GenericsClass<Parent> parent = child; //NG!!
Q&A
raw typeとunbounded wildcard typeの違い(ListとList<?>の違い)
ListはList<Object>に等しい。したがって、List<Object>にはあらゆる型を渡すことが可能です。一方で、List<?>は型が不明なため特定の型を渡すことができません。渡すことが可能なのはnullのみです。以下例です。
List<?> unboundList = new ArrayList<>(); unboundList.add(new Object()); //NG unboundList.add(null); //OK List rawList = new ArrayList(); rawList.add(new Parent()); //OK rawList.add(""); //OK
なお、raw typeは過去のバージョンとの互換性のために残されているだけで、本来使用すべきではありません。理由としては上記のようにrawListには何でも突っ込めるため、ParentのListだけと想定して取り出して、ParentにCastするとClassCastExceptionが発生するためです。
これはRuntime時に発生するためCompile時に型安全を保障してくれないのでロスに繋がります。
ListとList<Object>の違い
上記項目に加えて、それじゃあListとList<Object>は何が違うのか?と問われれば
意味上は何も変わりません。
将来的にraw typeの互換性がなくなった場合に動かなくなる程度です。ただし、List<Object>と明示的にObjectとすることで、意図したObject型であることが分かり、ユーザーはObject以下にダウンキャストしようとは思わなくなります。これもRuntime時のエラーを防止する効果が見込めます。
List<?>とList<Object>の違い
Invariantの章で記載しましたが、JavaのGenericsはinvariantのため、AとBに親子関係があったとしても、List<A>とList<B>に親子関係はありません。しかし、型を意識しなくてもよいからとりあえず値を受けたいときにそのままでは不便です。wildcardを使用することでJavaのGenericsにおいてもcovariantを実現できます。
List<Parent> parentList = new ArrayList<>(); List<?> unboundedList = parentList; //OK List<Object> objectList = parentList; //NG
結局Parameterized Typeに対して何を渡せるのか
以下になります。
1 List<Parent> parentList = new ArrayList<>(); parentList.add(new Parent()); //OK parentList.add(new Child()); //OK 2 List<Child> childList = new ArrayList<>(); List<Parent> parentList = childList; //NG 3 List<? extends Parent> upperBoundList = new ArrayList<>(); upperBoundList.add(new Parent()); //NG upperBoundList.add(new Child()); //NG upperBoundList.add(null); //OK upperBoundList.get(0).parentMethod(); //OK 4 List<? super Parent> lowerBoundList = new ArrayList<>(); lowerBoundList.add(new Parent()); //OK lowerBoundList.add(new Child()); //OK lowerBoundList.add(null); //OK lowerBoundList.get(0).parentMethod(); //NG
1(一般的なGenerics)
List<Parent> parentList = new ArrayList<>(); parentList.add(new Parent()); //OK parentList.add(new Child()); //OK
EはParentととしてParameterizedされます。したがって、Parentに対してParentやChildを渡すことは勿論可能です。
2 (Parameterized TypeのInvariant性)
List<Child> childList = new ArrayList<>(); List<Parent> parentList = childList; //NG List<?> unboundedList = parentList; //OK
invariantの章でも説明を行いましたが、Parameterized TypeはInvariantなため、別々の型として認識されるので、たとえType Argumentに親子関係があったとしても受け取ることができません。
ただし、wildcardを用いればcovariantとして取り扱われますので、アサインすることが可能です。
3 (upper bounded wildcard typeについて)
List<? extends Parent> upperBoundList = new ArrayList<>(); upperBoundList.add(new Parent()); //NG-1 upperBoundList.add(new Child()); //NG-2 upperBoundList.add(null); //OK -3 upperBoundList.get(0).parentMethod(); //OK -4
upper bounded wildcardを使用しているので、List<? extends Parent>のParameter Typeの実態はParentを継承している、あるいは、Parentであれば何でもよいことになります。
1. Parentを渡せないことに違和感があるかもしれませんが、例えば実態がList<Child>である可能性もあります。そのため、Parentをダウンキャストする必要があり型安全が保障されないのでコンパイルエラーとなります。
2.Childを渡せないのも同様です。例えば実態がList<GrandChild>の場合もあるので。上記と同様です。
3.nullはどのような型でも問題ないので渡せます。
4.返り値として提供されるParameter TypeはすべてParentを継承しているので、Parentに実装されているmethodにはアクセスできます。
4(lower bounded wildcard typeについて)
List<? super Parent> lowerBoundList = new ArrayList<>(); lowerBoundList.add(new Parent()); //OK lowerBoundList.add(new Child()); //OK lowerBoundList.add(null); //OK lowerBoundList.get(0).parentMethod(); //NG
3とは逆です。List<? super Parent>のParameter TypeはすべてParentの親、あるいは、Parentなので、Parent、あるいはParentの子はすべて渡すことが可能です。
一方で、実態がParentの親である可能性があるので、Parentの関数にはアクセスできません。この場合、最も上位のObjectの関数にのみアクセスが可能です。
Wildcardの使い所とは?
- Generic Typeを引数で使用するとき
- 返り値では使用しない
あが原則として提示されています。引数で使用することでflexibilityが上がるため積極的に用いるべきですが、返り値で使用した場合ユーザーにwildcard bounded typeを使用することを強制するため、使い勝手の悪いAPIとなります。
引数でのWildcardを使用することで、Parameterized TypeがInvariantであっても、Type Argumentに親子関係があれば値を渡すことができるので、汎用性が広がります。
public void apply(List<Parent> aList){} --1 public void apply(List<? extends Parent> aList){} --2
上記の例だと、1はList<Parent>しか引数にとれないですが、2はList<Child>でも引数に渡すことができます。Invariantな性質をここで吸収できるの積極的に使うべきと考えられます。
ただし、wildcard bounded typeを使用して、そのwildcard bounded typeのGenericなメソッドに値を渡すことはそのままではできません。何が言いたいかというと以下はNGです。(関数的には初めの要素を最後に足すだけで意味のない関数です。)
public void apply(List<? extends Parent> aList){ aList.add(aList.get(0)); //NG }
なぜなら、aList.addはGeneric methodであり、aListが<? extends Parent>である以上、aListのParameter Typeの実態はChildでもGrandChildでもよいので、この一文からはコンパイラーは型安全を保障できません。
しかし、実現したい操作内容は型安全が保障されているのでこのコンパイルエラーは回避したいところです。その場合、以下のようにHelperメソッドを定義します。
public void apply(List<? extends Parent> aList){ applyHelper(aList); } private static <E> void applyHelper(List<E> aList){ aList.add(aList.get(0)); }
一度Generic methodでwildcardを吸収することで、型安全を保障できます。
Genericとbounded wildcard typeのどちらがいい?
以下のように一つのType Parameterのみを用いて表現できるメソッドはGenericsを用いてもbounded wildcard typeを用いても表現できます。
public static <E> void doSomething(List<E> aList){} public static void doSomething(List<?> aList){} <-- better
一応、シンプルであるためbounded wildcard typeを使用するのがベターとされている様子。
(正直どちらでもいいと思います。)
Generic型を要素として持つListを作りたいんだけど?
以下のようにあるGeneric型を要素として持つListは以下のようにwildcardを用いて作成します。(Type Argumentが決まっているのであればbounded wildcardを使用する)
List<GenericClass<?>> list = new ArrayList<>(); list.add(new GenericClass<Parent>()); list.add(new GenericClass<Child>());
これはwildcardを用いることで、GenericClass<?>がGenericClass<Parent>やGenericClass<Child>の親となっているから実現できています。したがって、逆にlistから取り出した値についてParameterized TypeをCastなしに利用することはできません。
GenericClass<?> genericClass = list.get(0); //OK GenericClass<Parent> parentClass = list.get(0); //Castが必要
Type Parameterをインスタンス化したいんだが?
new T()はできないので、以下のようにClass<T>を使用してインスタンス化する必要があります。
public T createNewInstance(){ return new T(); return aClazz.getDeclaredConstructor(null).newInstance(); }
instanceof T
instanceof Tはできません。理由としてはTはerasureで実行時には型情報を保持していないので、常にinstanceof Tの結果はnullとなります。
代替手段としては以下のように、Classを合わせて渡すことでチェックすることが可能です。
private <T> void NGmethod(List<T> list){ Object obj = getSomething(); if(obj instanceof T){ //NG list.add((T)obj); } } private <T> void OKmethod(List<T> list, Class<T> aClazz){ Object obj = getSomething(); if(aClazz.isInstance(obj)){ list.add((T)obj); } if(aClazz.isAssignableFrom(obj.getClass())){ list.add((T)obj); } }
lass#isInstanceとClass#isAssignableFromの両方で可能ですが、公式にinstanceofの動的バージョンとして定義されているのはisInstanceの方ですので、特に理由がなければisInstanceを使用しましょう。
isInstanceとisAssignableFromの違い
色々議論されていますが、基本的には同じです。Objectが得られるのであればinInstanceを使用し、Class同士の比較をしたいのであればisAssignableFromを使用する程度の違いで、utility的に両者が定義されていると考えられます。
- isInstanceはnullを許容する(falseを返す)が、isAssignableFromはNullPointerExceptionを投げる
- primitive型を検証したい場合にはisAssignableFromを使用する。classがprimitive型の場合、isInstanceは常にfalseを返します(引数がObjectで指定されている以上、autoboxingされてObjectになるので常にfalseになるのは仕方ないですね)。isAssignableFromでは、同一のprimitive型の場合はtrueを返してくれます。ただこのprimitive型のクラスが正確に一致することを確認したい場合、ってあるんでしょうかね?ユースケースが思いつかない。実行結果は以下です。
Class<Integer> integerClass = Integer.class; System.out.println("integerClass.isInstance(1):" + integerClass.isInstance(1)); System.out.println("integerClass.isAssignableFrom(int.class):" + integerClass.isAssignableFrom(int.class)); Class<?> intClass = int.class; System.out.println("intClass.isInstance(1):" + intClass.isInstance(1)); System.out.println("intClass.isAssignableFrom(int.class):" + intClass.isAssignableFrom(int.class)); integerClass.isInstance(1):true integerClass.isAssignableFrom(int.class):false intClass.isInstance(1):false intClass.isAssignableFrom(int.class):true
Reference
Java公式
Effective Java
読むなら英語版の方がいいと思います。