この記事はチームラボエンジニアリングアドベントカレンダー11日目の記事です!
##はじめに
Javaで開発していると、以下のようなジェネリックスにぶち当たることがあると思います。僕はこのようなコードがあると逃げていました。。。
そうは言っていられず、自分でジェネリックスを扱わなければいけない場面があり、調べて自分なりに理解できたのでそれをここでまとめようと思います。
List<?> hoge;
List<? extends Number> hoge;
List<? super Integer> hoge;
class Hoge<E extends Number> {
private E hoge;
public E get() { return this.hoge; }
public void set(E hoge ) { this.hoge = hoge; }
public <T> T fuga(T hoge) { return hoge; }
public <E> E hoge(E e) {
return e;
}
}
ジェネリックスとは
ジェネリックスとは、List<E>
で書かれている<E>
の部分のことをジェネリックスと言います。
- クラスやメソッドを汎用的に使いたい
- 汎用的に使うクラスやメソッドを制限をかけて安全に使いたい
このようなときに、ジェネリックスを使います。
まず、ここで扱うJavaの世界には以下のWrapperクラス、Animalクラス、Dogクラス、MiniDogクラス、そしてObjectクラスしかないと仮定して話を進めていきます。
Animalクラス、Dogクラス、MiniDogクラス
class Animal {}
class Dog extends Animal {}
class MiniDog extends Dog {}
AnimalクラスをDogクラスが継承し、そのDogクラスをMiniDogクラスが継承していることとします。
Wrapperクラスは以下のように定義します。
Wrapperクラス
// ジェネリックスクラス
class Wrapper<T> {
private T t;
Wrapper(T t) {
this.t = t;
}
public T get() {
return t;
}
public void set(T t) {
this.t = t;
}
// ジェネリックスメソッド(今回は使わない) ここでListインタフェースとArrayListクラスを使っているのですが許してください。。。
public <E> List<E> hoge(E e) {
List<E> list = new ArrayList<>();
list.add(e);
return list;
}
}
ジェネリックスを使用しているWrapperクラスのようなクラスのことをジェネリックスクラスといいます。
class Wrapper<T> { ... }
のように書くことで、クラス内で変数Tという型パラメータを使用することができます。
Wrapperクラスでは。
フィールド、get()メソッド、set()メソッド
に型パラメータTを使用しています。
※しかしここで注意なのですが staticなフィールド、staticなメソッド には使用することができません。
Wrapperクラスの中でジェネリックスメソッド(Wrapperクラスのhogeメソッド)というものがあります。
戻り値の型の定義を書いている手前に<E>
のように書くことで、メソッド内で変数Eという型パラメータを使用することができます。<E>
と書くことで、このメソッド内で型パラメータとしてEを使いますよという宣言をしていることになります。
もし、hoge()メソッド内でT型を使いたい場合には、戻り値の型の定義を書いている手前に<T>
と書く必要はありません。
なぜなら、Wrapperクラスを定義するときにclass Wrapper<T> { ... }
のように、すでにT型は定義されているためです。
変数Tのスコープは、Wrapperクラス内ということになります。
// ジェネリックスクラス
class Wrapper<T> {
// 省略
// ジェネリックスメソッド
public List<T> hoge(T t) {
List<T> list = new ArrayList<>();
list.add(t);
return list;
}
}
ジェネリックスを使うことで、いろんな型を汎用的に扱うことができるようになります。
Wrapperクラスで使われるTのことを仮型パラーメータといい、このTには、基本データ型以外の型を入れることができます。
ちなみに、Tというのは変数です。
基本的には一文字で下記の慣例があるようです。
- E:Element
- K:Key
- V:Value
- T:Type
- N:Number
このジェネリックスを用いたWrapperクラスを使って、AnimalやDogなどを汎用的に扱うことができます。
Wrapper<Animal> animalWrapper = new Wrapper<>(new Animal());
Wrapper<Dog> dogWrapper = new Wrapper<>(new Dog());
Animal animal = animalWrapper.get();
Dog dog = dogWrapper.get();
変性
ジェネリックスは、変性という性質を持っています。変性には、非変、共変、反変の3つがあります。
非変
先程のWrapperクラスを使った例を見てみます。
Animal animal = new Animal();
Dog dog = new Dog();
animal = dog;
Wrapper<Animal> animalWrapper = new Wrapper<>(animal);
Wrapper<Dog> dogWrapper = new Wrapper<>(dog);
animalWrapper = dogWrapper; //コンパイルエラー
このように、Animal
とDog
には継承関係があり、スーパータイプのAnimalインスタンスにサブタイプのdogインスタンスを代入可能です。しかし。Wrapper<Animal>
にWrapper<Dog>
を代入することができません。このような性質のことを非変といいます。
共変
Animal
とDog
には継承関係があります。
このとき、Wrapper<Animal>
にWrapper<Dog>
を代入可能になるような性質のことを共変といいます。
Animal animal = new Dog();
Wrapper<Animal> animalWrapper = new Wrapper<Dog>(new Dog());
反変
反変は共変と逆の性質をもちます。
Animal
とDog
には継承関係があります。
このとき、Wrapper<Dog>
にWrapper<Animal>
を代入可能になるような性質のことを反変といいます。
Animal animal = new Dog();
Wrapper<Dog> = new Wrapper<Animal>(new Animal());
ワイルドカード型
型パラメータに?
が書かれているジェネリックス型のことをワイルドカード型といいます。
ワイルドカード型には、非境界ワイルドカード型と境界ワイルドカード型の2つがあります。
非境界ワイルドカード型
非境界ワイルドカード型は、Wrapper<?> w
のように、?のみの型パラメータがことです。
境界がないということなので、Wrapper<?>
には、どんな型(基本データ型以外)でも代入することができます。
Wrapper<?> animalList = new ArrayList<Animal>();
Wrapper<?> dogList = new ArrayList<Dog>();
animalList = dogList;
非境界ワイルドカード型は、以下の制約があります。
- メソッドの戻り値がObject型となる。
- nullしかsetすることができない
基本的にはget()やset()は使うことができません。
例外として、Object型で値を受け取ったり、nullをsetすることはできます。
Animalクラス、Dogクラス、MiniDogクラス
Wrapperクラス
Wrapper<?> animalWrapper = new Wrapper<>(new Animal());
Wrapper<?> dogWrapper = new Wrapper<>(new Dog());
// getter
Animal animal = animalWrapper.get(); //コンパイルエラー
Dog dog = dogWrapper.get(); //コンパイルエラー
Object objectAnimal = animalWrapper.get();
Object objectDog = dogWrapper.get();
// setter
animalWrapper.set(new Animal()); //コンパイルエラー
dogWrapper.set(new Dog()); //コンパイルエラー
animalWrapper.set(null);
dogWrapper.set(null);
境界ワイルドカード型
境界ワイルドカード型には、上限付きワイルドカード型と下限付きワイルドカード型の2つがあります。
上限付き境界ワイルドカード型(変性:共変)
上限付きワイルドカード型は、Wrapper<? extends Animal>
のような書き方をします。名前の通り、なんでも許容ではなく、制限として、上限があるワイルドカード(この例でいうとスーパータイプがAnimalクラスになるものだけ)のことになります。
Wrapper<? extends Animal>
このように書くことで共変のような性質を持つことができるようになります。
Wrapper<Dog> dogWrapper = new Wrapper<>(new Dog());
Wrapper<? extends Animal> animalEWrapper = dogWrapper;
上限付き境界ワイルドカード型は、基本的には
- 宣言した型パラメータ(
<? extends Animal>
)の戻り値が宣言時の型パラメータ型(Animal)になる - nullのみsetすることができる
実際にコードを見ながら整理していきます。
Animalクラス、Dogクラス、MiniDogクラス Wrapperクラス
Wrapper<? extends Animal> animalEWrapper = new Wrapper<>(new Dog());
Object object = animalEWrapper.get();
Animal animal = animalEWrapper.get();
Dog dog = animalEWrapper.get(); //コンパイルエラー
MiniDog miniDog = animalEWrapper.get(); //コンパイルエラー
animalEWrapper.set(null);
animalEWrapper.set(new Object()); //コンパイルエラー
animalEWrapper.set(new Animal()); //コンパイルエラー
animalEWrapper.set(new Dog()); //コンパイルエラー
animalEWrapper.set(new MiniDog()); //コンパイルエラー
animalEWrapperには、Animalを継承しているクラスを代入することができます。animalEWrapperの型パラメータの宣言を見ると、戻り値はAnimalになります。そのため、Animal型とObject型で受け取ることはできますが、AnimalのサブタイプのDogやMiniDogでは受け取ることはできません。
set()メソッドではnullのみsetすることができます。
下限付き境界ワイルドカード型(変性:反変)
下限付きワイルドカード型は、Wrapper<? super Dog>
のような書き方をします。名前の通り、制限として、下限があるワイルドカード(この例でいうとサブタイプとしてDogクラスを持つクラス)のことになります。
Wrapper<Animal> animalWrapper = new Wrapper<>(new Animal());
Wrapper<? super Dog> dogSWrapper = animalWrapper;
Wrapper<? super Dog>
にWrapper<Animal>
に代入が可能になります。このように書くことで、反変のような性質を持つことができるようになります。
下限付き境界ワイルドカード型は、基本的には
- 宣言した型パラメータ(
<? super Dog>
)の戻り値がObject型になる - Dogを継承したクラスとnullのみsetすることができます。
実際にコードを見ながら整理していきます。
Animalクラス、Dogクラス、MiniDogクラス Wrapperクラス
Wrapper<? super Dog> dogSWrapper = new Wrapper<>(new Animal());
Object object = dogSWrapper.get();
Animal animal = dogSWrapper.get(); //コンパイルエラー
Dog dog = dogSWrapper.get(); //コンパイルエラー
MiniDog miniDog = dogSWrapper.get(); //コンパイルエラー
dogSWrapper.set(null);
dogSWrapper.set(new Object()); //コンパイルエラー
dogSWrapper.set(new Animal()); //コンパイルエラー
dogSWrapper.set(new Dog());
dogSWrapper.set(new MiniDog());
dogSWrapperには、Dogをサブタイプとして持っているクラスを代入することができます。
下限付き境界ワイルドカード型のdogSWrapperの戻り値はObject型になります。そのため、Object型でのみ受け取ることはできます。
set()メソッドでは、Dogを継承したクラスとnullのみsetすることができます。
まとめ
はじめて、技術記事を書いたのですが、自分なりに理解していても文章にまとめるのは難しいなと改めて感じました。わかりづらくなっているところもあるかと思いますが、誰かの役に立てばと願っています。