0.先に結論
- 型パラメータT付きでクラス継承する
- コンストラクタにT型可変長引数を持たせることでTの実際の型が取得できる
1.課題
配列の要素の型は array.getClass().getComponentType()
で後から分かるけど、
ArrayList、LinkedList等のListで同じことをやるのは難しい!
2.対象読者
- javaで
Class
クラスを利用したことのある人 - 型パラメータに興味のある人
3.ダメな例
準備
class Hoge
{
public static void main(String[] args)
{
ArrayList<Number> inteList = new ArrayList<>();
System.out.println(new ParamGetter(inteList).get());
//ParamGetterクラスのget()メソッドで
//「Number」などと表示させたい
}
}
3.1 T.classはできない
class ParamGetter
{
Class paramClass;
<T> ParamGetter(List<T> list)
{
this.paramClass = T.class;
//×これはNG
}
Class get(){return paramClass;}
}
3.2 リフレクションしても失敗[1]
//(リフレクションしたことある人向け)
class ParamGetter
{
Type paramType;
<T> ParamGetter(List<T> list)
{
this.paramType = ((ParameterizedType)new ArrayList<T>(){}.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
Type get(){return paramType;}
}
結果:「T」と表示されてしまう
Field
クラスの getGenericType()
メソッドを使う方法[2]でも同じ。
3.3 toArray()
で配列化しても失敗
class ParamGetter
{
Object array;
<T>ParamGetter(List<T> list)
{
this.array = (T[])list.toArray();
//☆System.out.println((T[])list.toArray());
}
Type get() throws NoSuchFieldException, SecurityException
{
return this.array.getClass().getComponentType();
}
}
結果(main):「class java.lang.Object」と表示されてしまう。
4. 根本的な原因
メソッドに渡す段階で型情報が消えている
3.3はまずList<T>
型仮引数にArrayList<Number>
型実引数を渡している。
次にT
型配列に変換して Object
型のフィールドに格納している。
そして、get()
が呼ばれると、「実行時の型」(配列型)を求め、その配列の要素の型を調べる。
その結果が Object
と出たのだから、
T
= Number
ではなく T
= Object
となってしまったことになる。
事実、☆のコードを実行すると、Object[]
型である旨が表示される。
→やはり T[]
= Object[]
System.out.println((T[])list.toArray());//Object[]型と出る。
ここから「型パラメータの情報が、引数として渡す間で消えてしまった」ということがわかる。
実際にはコンパイル時に消される(受け取り可能な最も抽象的な型になる)[3]
5. 解決策
実行時に消えるものは例えリフレクションしても取れない(リフレクションは実行時の技術[4])
→
- 呼び出すとき、消えないところに書く
- クラス自体に型パラメータを持たせ、コンストラクタでそれを利用
5.1 型パラメータを引数に置換え
class Hoge<T> | Fuga生存 | Fuga as |
---|---|---|
new Hoge(); | × | インスタンスメソッドが持つ型パラメータ |
new Hoge<Fuga>(); | 〇 | クラスが持つ型パラメータ[5] |
new Hoge(Fuga.class) | △ | Class型引数 |
△・・・不可能ではないが、冗長な使用になってしまう |
5.2 ArrayList<T>
→ ArrayList_<T>
やりたいこと:
new ArrayList_<Number>(Object...);
が
new ArrayList<Number>(Object...);
と
ほぼ同じ意味を持つようにし、
かつ Number
を実行時に取得可能にする
ほぼ同じ意味を持つ→継承で実現可能
本記事のsaka1029氏のコメントを参考に書く
public class ArrayList_<T> extends ArrayList<T> {
public final Class<?> clazz;
public ArrayList_(T... dummy) {
if (dummy.length > 0)
throw new IllegalArgumentException(
"dummy引数を指定してはいけません");
clazz = dummy.getClass().getComponentType();
}
public Class getComponentClass() {
return clazz;
}
}
6 (おまけ) new Hoge<t.getClass()>
的なことを可能にする
ジェネリクス型Hoge<T>
のインスタンスはClass<T>
型の変数からは作れない。
要は new Hoge<t.getClass()>
はできない。
メソッド内に書いておくなら new Hoge<?>
としてある程度可能だけど、
Hoge<t.getClass()>.getClass().getDeclaredConstructor().newInstance()
として任意の場所でジェネリクス型インスタンスを作ることはできない。
これをしたい場合は、引数にClass<?>
型変数を渡せばよい。
public class ArrayList_<T> extends ArrayList<T> {
public final Class<?> clazz;
public ArrayList_(T... dummy) {
if (dummy.length > 0)
throw new IllegalArgumentException(
"dummy引数を指定してはいけません");
clazz = dummy.getClass().getComponentType();
}
public ArrayList_(Class<T> clazz)//これを追加
{
this.clazz = clazz;
}
public Class getComponentClass() {
return clazz;
}
}
参考
[1]http://unageanu.hatenablog.com/entry/20071105/1194264963
[2]https://codeday.me/jp/qa/20181216/16785.html
[3]https://qiita.com/pebblip/items/1206f866980f2ff91e77
本文中の「受け取り可能な最も抽象的な型」は[3]の「上限境界」を意味する。
[4]https://java.keicode.com/lang/reflection.php
[5]本記事にお寄せいただいた、saka1029氏のご指摘
その他:https://java-beginner.com/thread-thread-and-runnable/