JavaのgetClass()はジェネリクス(or型変数)との相性が悪い

少し直観に反するJavaの言語仕様に気が付いたのでメモしておきます。
検証環境は Windows 版 OpenJDK 11 (build 28) です。

# 出来ないこと

```java
public static <T> void hoge(@NonNull T value){
    Class<? extends T> type = value.getClass(); // コンパイルできない
    // type を使って作業したい
}
```

value は`T`かそのサブクラスのはずなので一見，問題なさそうな気がしますが，このコードはコンパイル出来ません．

```text:javac
GenericGetClass.java:3: エラー: 不適合な型: Class<CAP#1>をClass<? extends T>に変換できません:
        Class<? extends T> type = value.getClass();
                                                ^
  Tが型変数の場合:
    メソッド <T>hoge(T)で宣言されているT extends Object
  CAP#1が新しい型変数の場合:
    ? extends ObjectのキャプチャからのCAP#1 extends Object
エラー1個
```

何だかよくわからないエラーメッセージ…

# 何がダメなのか？

いくつか試してみます。

```java
// 当然OK
public static void hoge(CharSequence value){
    Class<? extends CharSequence> type = value.getClass();
}
// これもOK
public static <T extends CharSequence> void fuga(T value){
    Class<? extends CharSequence> type = value.getClass();
}
// これはコンパイルできない
public static <T extends CharSequence> void piyo(T value){
    Class<? extends T> type = value.getClass();
}
```

代入先の型に型変数 `T` を使っているとエラーになるようです。ここまで試して、 `Object#getClass()` の型の扱いが特殊だという話を思い出したので改めて調べました。

## `getClass()` の仕様

[API仕様書](https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/lang/Object.html#getClass())によると、

> 実際の結果型はClass<? extends |X|>です。ここで、|X|はgetClassが呼び出される式のstatic型の消去です。

そもそも、 `Object` クラスでは `public final Class<?> getClass()` のように定義されているので、

```java
CharSequence cs = ...;
Class<?> type = cs.getClass()
```

は可能でも、普通のメソッドと同じように扱うと

```java
Class<? extends CharSequence> type = cs.getClass()
```

のように代入することは出来ないことになります。そこで、言語使用で特別扱いしてこの場合`getClass()` の型が `Class<? extends CharSequence>` だとみなすわけですね。具体的には、型の制限がコンパイル時の変数の型(=「static型」)で決まります。

ここで問題になるのが、「static型の消去」になっていること。「消去」は翻訳として微妙な気もしますが、[言語仕様§4.6](https://docs.oracle.com/javase/specs/jls/se11/html/jls-4.html#jls-4.6)のイレイジャ(erasure)のことでしょう。`List<String>` に対する `List` 、 `class Hoge<T extends CharSequence>` と宣言されているときの `T` に対する `CharSequence` 、などを示すようです。

## 今回の例では？

```java
public static <T extends CharSequence> void fuga(T value){
    Class<? extends CharSequence> type = value.getClass(); // OK
}
```

の場合は、`value` は `T` 型で、その`extends`指定[^1] は`CharSequence`なので、`T`のイレイジャは `CharSequence` であり、`getClass()` の返り値の型は `? extends CharSequence` となります。これは `T` やそのサブクラスとは限らないため、`Class<? extends T>` には代入できないんですね。

[^1]: 英語では leftmost bound ですが何か良い日本語訳はないのでしょうか？

最初の例では、

```java
public static <T> void hoge(@NonNull T value){
    Class<? extends T> type = value.getClass(); // コンパイルできない
}
```

`T` のイレイジャは `Object` なので `getClass()` の型は `? extends Object` になるわけです。

# なぜこのような仕様になっているのか

ここからは筆者の推測がかなり含まれるのですが、まずそもそも総称型そのままではなくイレイジャが指定されている理由は、総称型だとこんなことが起こるからでしょう。

```java
ArrayList<String> list = new ArrayList<>();
Class<? extends ArrayList<String>> type = list.getClass(); // 実際にはコンパイルエラー
```

いやな予感がしますね。

`Class`クラスのインスタンスは仕組み上詳細な型情報を持てない[^2]ので、`Class<ArrayList<String>>` 型や `Class<ArrayList<Integer>>` 型のオブジェクトは、実際には同じ`Class<ArrayList>`のインスタンスになります。`Class<ArrayList>`のメソッドは`ArrayList<T>`の型変数`T`が何に設定されているかを知ることができません。

つまり、`Class<ArrayList<String>>` という型の変数は危険です。もし上の例がコンパイルできると、

[^2]: 各クラスごとにClassオブジェクトは一意に存在するので(このため`==`で比較できる)

```java
ArrayList<Integer> another = new ArrayList<>();
another.add(123);
ArrayList<String> casted = type.cast(another); // 実行時には単に ArrayList へのキャスト扱いなので通る
// ...
String str = casted.get(0);// ここで実行時エラー？
```

全体的にraw型を使わずに書いてコンパイルが通っているのに、予期せぬ場所で `ClassCastException` が起こることになりそうです。

総称型を使えない理由は分かりました。だからといって型変数の場合まで消さなくていいだろうと思ってしまったのですが、型変数`T`が呼び出し側で `List<String>` に割り当てられたりしていれば結局同じです。だから冒頭のようなプログラムも許されないということなのでしょう。


# 余談

筆者がこの振る舞いに気が付いたのは、

```java
@SuppressWarnings("unchecked")
public static <K extends Enum<K>> EnumMap<K,String> of(@NonNull K key, String value){
    Class<K> type = (Class<K>) key.getClass(); // キャストしないとコンパイルエラー
    EnumMap<K,String> map = new EnumMap<>(type); // 拡張されたEnum定数を渡すとここで実行時エラーになる
    map.put(key, value);
    return map;
}
```

のようなコードを書いていた時です[^4]。この場合は、`enum`が `final` なので大丈夫な気がしていたのですが、よく考えれば、 `enum` の定数個別にメソッドとかを定義すると別クラスになるのでアウトでした。

[^4]: 実際には EnumMap の独自実装みたいなことをしていました。




## あとがき

初めてQiitaの記事を書いてみましたが、考えて書いていると確かに知識の整理になりますね。至らぬ点があれば是非ご指摘ください。