0
0

Java 不変性 共変性 反変性

Last updated at Posted at 2024-07-11

Java 11 Gold 取得に向けた学習記録

型推論

型推論はコンパイル時に代入演算子の右辺の値を元に行われ、ローカル変数、メソッドの戻り値や引数に対して利用することができる。

ただしvarによる型推論は、ローカル変数にのみ利用することができる。

また、配列の初期化時にも型推論が利用される事がある。

int[] array = {1, 2, 3};

ジェネリクスにおける型推論

ジェネリクスでも型推論を利用することができる。

型を指定しない記述方法はダイアモンド演算子<>と呼ばれる。

ジェネリクスの型推論
// 型推論あり
List<String> list = new ArrayList<>();

// 型推論なし
List<String> list = new ArrayList<String>();

またジェネリクス自体に型引数を与えなかった場合、Object型を指定したものとして扱われる。

ジェネリクスの型推論
// 型引数を与えない
ArrayList list = new ArrayList<>();

// 型引数を与える
ArrayList<Object> list = new ArrayList<>();

型推論はメソッドの戻り値や引数にも利用できる。

メソッドの戻り値の型推論
// 型推論あり
List<String> getList() {
    return new ArrayList<>();
}

// 型推論なし
List<String> getList() {
    return new ArrayList<String>();
}
メソッド実行時の引数の型推論
void setList(List<String> list) {
        
}

// 型推論あり
setList(new ArrayList<>());

// 型推論なし
setList(new ArrayList<String>());

ワイルドカード

ジェネリクスの型引数を柔軟に指定するための機能。

ワイルドカードを使うことで、特定の型に依存しない汎用的なコードを記述することができる。

class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
非境界ワイルドカード(?)
List<?> list = new ArrayList<>();
上限境界ワイルドカード(? extends T)
List<? extends Animal> animals = new List<Dog>();
List<? extends Animal> animals = new List<Cat>();
下限境界ワイルドカード(? super T)
List<? super Dog> dogs = new List<Animal>();
List<? super Cat> cats = new List<Animal>();

非境界ワイルドカード

非境界ワイルドカードを使用することで、コンパイル時に型が未定だと判断され、実行時にあらゆる型を使用することができるようになる。

一方で、引数に使用した非境界ワイルドカードは、実際には null しか許容されなくなる。また、戻り値に使用した非境界ワイルドカードは、Object 型になるという特徴がある。

class Dog {}
class Cat {}

class Zoo<T> {
    T animal;

    void set(T t) {
        this.animal = animal;
    }

    T get() {
        return animal;
    }
}
非境界ワイルドカード
Dog dog = new Dog();
Cat cat = new Cat();

// あらゆる型を許容する
Zoo<?> dogZoo = new Zoo<>(dog);
Zoo<?> catZoo = new Zoo<>(cat);

// 引数は null しか許容されない
dogZoo.set(null);
catZoo.set(null);

// コンパイルエラー
dogZoo.set(new Dog());
catZoo.set(new Cat());

// 戻り値は Object 型になる
Object obj1 = dogZoo.get();
Object obj2 = catZoo.get();

// コンパイルエラー
Dog dog2 = dogZoo.get();
Cat cat2 = catZoo.get();

不変性・共変性・反変性

ジェネリクスや型引数において、型の代入互換性に関する性質として共変性反変性と呼ばれるものがある。

混同しやすいものにポリモーフィズムの要素の一つである「スーパークラス型の変数にはサブクラス型を代入することができる」という性質があるが、これはこのあと整理するジェネリクスが持つ共変性や反変性とは別である。

class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
ポリモーフィズム
Animal animal = new Animal();

// Animal ◀️ Dog
Animal animal = new Dog();

// Animal ◀️ Cat
Animal animal = new Cat();

// Dog ◀️ Cat (コンパイルエラー)
Dog dog = new Cat();

// Cat ◀️ Dog (コンパイルエラー)
Cat cat = new Dog();

List<Animal> animals = new ArrayList<>();

// Animal ◀️ Animal
animals.add(new Animal());

// Animal ◀️ Dog
animals.add(new Dog());

// Animal ◀️ Cat
animals.add(new Cat());

ジェネリクスには一見クラスの型と同じように扱えるように思えるが、クラスで言うところのスーパージェネリクス、サブジェネリクスといった概念は存在しない。

不変性

Invariance

代入されるジェネリクスが、宣言時に指定された型以外を許容しない性質。ジェネリクスは原則として、不変性の性質を持つ。

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
class Zoo<T> {}

ジェネリクスは完全に一致する型のみを許容する。

不変性
Zoo<Animal> animalZoo;

// Zoo<Animal> ◀️ Zoo<Dog> (コンパイルエラー:不変性により許容されない)
animalZoo = new Zoo<Dog>();

// Zoo<Animal> ◀️ Zoo<Cat> (コンパイルエラー:不変性により許容されない)
animalZoo = new Zoo<Cat>();

個人的にとても混同してしまいがちなのが、ポリモーフィズムの性質。別の性質であることは改めて整理しながら理解したいところ。

これはポリモーフィズム
Animal animal;

// Animal ◀️ Dog 
animal = new Dog();

// Animal ◀️ Cat 
animal = new Cat();

Listでも確かめてみる。

不変性
// List<Animal> ◀️ List<Dog> (コンパイルエラー。やっぱりジェネリクスは不変性)
List<Animal> animals = new List<Dog>();

ジェネリクスは原則、不変性である

extends

ジェネリクスの宣言にextendsキーワードを使用することで、実行時に許容する型引数を特定のスーパークラスのサブクラスに制限することができる。この制限のことを上限境界(upper bound)と言う。

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
class Fox {}
class Monkey {}

// Animal もしくは Animal を継承したクラス型しか許容しない
class Zoo<T extends Animal> {}

Zooクラスのジェネリクスに与える実行時の型引数は、Animalクラスを継承したクラスのみに制限されている。

extends
// OK
Zoo<Animal> animalZoo;

// OK
Zoo<Dog> dogZoo;

// OK
Zoo<Cat> catZoo;

// コンパイルエラー(Fox が Animal を継承していないため、許容されない)
Zoo<Fox> foxZoo;

// コンパイルエラー(Monkey が Animal を継承していないため、許容されない)
Zoo<Monkey> monkeyZoo;

共変性

Covariance

型引数がその型のサブタイプに対して互換性を持つ性質。

ジェネリクスは基本的に不変性だが、extendsキーワードを使用することで共変性を実現することができる

extendsは型引数に制限を課すものだが、「不変性」という観点から考えてみると、本来は完全一致する型しか許容しない性質を、サブクラスも許容できるよう柔軟性を与えていると考えることもできる。

先程のextendsでのコードを共変性という観点でもう一度見てみる。

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

// Animal を継承したサブクラス型も許容できるよう柔軟性が加わる
class Zoo<T extends Animal> {}

Zooクラスに対する実行時の型引数は、Animalクラスに加えて、Animalクラスのサブクラスも許容されるようになる。

共変性
// Animal を継承する Dog も許容される
Zoo<Dog> dogZoo;

// Animal を継承する Cat も許容される
Zoo<Cat> catZoo;

extendsを使用することで、ジェネリクスは共変性を持つことができる

上限境界ワイルドカード

extendsワイルドカードを組み合わせた表現 <? extends T>上限境界ワイルドカードと言う。

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
class Zoo<T> {}
上限境界ワイルドカード
Zoo<Animal> animalZoo;
Zoo<? extends Animal> extendsAnimalZoo;

Zoo<Dog> dogZoo = new Zoo<>();
Zoo<Cat> catZoo = new Zoo<>();

// コンパイルエラー(不変性)
animalZoo = dogZoo;

// コンパイルエラー(不変性)
animalZoo = catZoo;

// 共変性
extendsAnimalZoo = dogZoo;

// 共変性
extendsAnimalZoo = catZoo;

Listでも確かめてみる。

上限境界ワイルドカード
List<Animal> animals;
List<? extends Animal> extendsAnimals;

List<Dog> dogs = new ArrayList<>();
List<Cat> cats = new ArrayList<>();

// コンパイルエラー(不変性)
animals = dogs;

// コンパイルエラー(不変性)
animals = cats;

// 共変性
extendsAnimals = dogs;

// 共変性
extendsAnimals = cats;

上限境界ワイルドカードは主に読み取り専用の操作に使用される。

class Animal {}
class Dog extends Animal {}

class Zoo<T> {
    T animal;

    void set(T animal) {
        this.animal = animal;
    }

    T get() {
        return animal;
    }
}
上限境界ワイルドカード
// 共変性
Zoo<? extends Animal> dogZoo = new Zoo<Dog>();

// 読み取りはできる(Dog ▶️ Animal のアップキャストは暗黙的)
Animal animal = dogZoo.get();

// 書き込みはできない(コンパイルエラー)
dogZoo.set(new Dog());

// 書き込みはできない(コンパイルエラー)
dogZoo.set(new Animal());

上限境界ワイルドカードが読み取り専用なのは、サブクラス型のオブジェクトをスーパークラス型に代入した際に起こるキャスト(アップキャスト)が暗黙的に行われるためである。

Listでも確かめてみる。

上限境界ワイルドカード
// 共変性
List<? extends Animal> animals = new ArrayList<Dog>();

// 読み取りはできる
Animal animal = animals.get(0);

// 書き込みはできない(コンパイルエラー)
animals.add(new Dog());

// 書き込みはできない(コンパイルエラー)
animals.add(new Animal());

共変戻り値

サブクラスでオーバーライドされたメソッドの戻り値型が、親クラスで定義されたメソッドの戻り値型のサブタイプにすることができるという概念のこと。

共変戻り値
class Animal {}
class Dog extends Animal {}

class AnimalZoo {
    Animal animal;
    
    AnimalZoo(Animal animal){
        this.animal = animal;
    }
    
    Animal get() {
        return animal;
    }
}

class DogZoo extends AnimalZoo {
    Dog dog;
    
    DogZoo(Dog dog){
        super(dog);
        this.dog = dog;
    }

    // Animal のサブクラス Dog を返せる
    @Override
    Dog get() {
        return dog;
    }
}

配列

配列も共変性を持つ。

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
配列は共変性を持つ
Animal[] animals = {};
Dog[] dogs = {};
Cat[] cats = {};

// 共変性(Animal[] ◀️ Dog[])
animals = dogs;

// 共変性(Animal[] ◀️ Cat[])
animals = cats;

// コンパイルエラー(Dog[] ◀️ Cat[])
dogs = cats;

配列は共変性である

super / 下限境界ワイルドカード

ジェネリクスの宣言にsuperキーワードを使用することで、実行時に許容する型引数を特定のサブクラスのスーパークラスに制限することができる。この制限のことを下限境界(lower bound)と言う。

superextendsキーワードと異なりクラスの宣言に使用することはできず、必ずワイルドカードと併せて使用する(下限境界ワイルドカード)。

<? super T>で表現する。

class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
class Zoo<T> {}

// class Zoo<T super Dog> {} や、
// class Zoo<T super Cat> {} という使い方はできない
super・下限境界ワイルドカード
Zoo<Animal> animalZoo = new Zoo<>();

// Zoo<? super Dog> ◀️ Zoo<Animal>
Zoo<? super Dog> dogZoo = animalZoo;

// Zoo<? super Cat> ◀️ Zoo<Animal>
Zoo<? super Cat> catZoo = animalZoo;

// コンパイルエラー(不変性)
Zoo<Dog> dogZoo = animalZoo;

// コンパイルエラー(不変性)
Zoo<Cat> catZoo = animalZoo;

Listでも確かめてみる。

List<Animal> animals = new ArrayList<>();

// OK
List<? super Dog> dogs = animals;

// OK
List<? super Cat> cats = animals;

// コンパイルエラー(不変性)
List<Dog> dogs = animals;

// コンパイルエラー(不変性)
List<Cat> cats = animals;

反変性

Contravariance
型引数がその型のスーパータイプに対して互換性を持つ性質。スーパータイプにはインターフェースも含む。

extendsキーワードによって共変性が実現されたように、superキーワードを使用することで反変性を実現することができる。

先程のsuperでのコードを反変性という観点でもう一度見てみる。

class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
class Zoo<T> {}
反変性
Zoo<Animal> animalZoo = new Zoo<>();

// Dog のスーパークラス型も許容できるよう柔軟性が加わる
Zoo<? super Dog> dogZoo = animalZoo;

// Cat のスーパークラス型も許容できるよう柔軟性が加わる
Zoo<? super Cat> catZoo = animalZoo;

下限境界ワイルドカードは主に書き込み専用の操作に使用される。

class Animal {}

class Dog extends Animal {}

class Zoo<T> {
    T animal;

    void set(T animal) {
        this.animal = animal;
    }

    T get() {
        return animal;
    }
}
反変性
// 反変性
Zoo<? super Dog> animalZoo = new Zoo<Animal>();

// 読み取りができない(コンパイルエラー。Animal ▶️ Dog のダウンキャストは暗黙的に行われない。)
Dog dog = animalZoo.get();

// 読み取りができない(コンパイルエラー)
Animal animal = animalZoo.get();

// 型安全が保証されないためObject形で受け取る必要がある
Object obj = animalZoo.get();

// 書き込みはできる
animalZoo.set(new Dog());

反変性において読み取りができないのは、読み取り時にスーパークラスのオブジェクトが取得される可能性があり、仮にスーパークラスのオブジェクトが取得された際のダウンキャストが暗黙的に行われないためである。このような状況を「型安全ではない」と言う。

Listでも確かめてみる。

class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
反変性
// 反変性
List<? super Dog> dogs = new ArrayList<Animal>();

// 読み取りができない(コンパイルエラー)
Animal animal = dogs.get(1);

// 読み取りができない(コンパイルエラー)
Animal dog = dogs.get(1);

// 読み取りができない(コンパイルエラー)
Animal cat = dogs.get(1);

// Object形なら読み取り可
Object obj = dogs.get(1);

// 書き込みはできる
dogs.add(new Dog());   

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0