LoginSignup
0
2

More than 5 years have passed since last update.

Javaで型の整合性が守られる仕組みを調べる

Last updated at Posted at 2018-11-03

  • PHPを数年やった結果、Javaの知識が薄れたので思い出してまとめる(主に型の複雑な部分について)

不変、共変ってなんでしたっけ

  • 前提)Dog extends Animalのとき
  • 配列は共変)Dog[]はAnimal[]のサブタイプ(=代入可能)
  • ジェネリック型は不変)List<Dog>とList<Animal>は別の型(=代入不可能)
  • ピンとこないので、これらのルールの意味するところを考える

クラスやメソッドの宣言のところに現れる?マークとかTとかextendsとかってなんでしたっけ

  • 型パラメータ(Tとか)を宣言にもつクラスはジェネリッククラスという
  • 型パラメータを持つことで、型ごとにクラスを作らなくて良い(Stack<T>を作れば、IntStackクラス、StringStackクラス・・・は作成不要)
  • Box<T>だとTはどのクラスでもよくなるので、Tの型を利用したコードはかけない。書きたいときはBox<T extends Animal>などと制限する
class Animal {
    public int getAge() {
        return 3;
    }
}

class Dog extends Animal {
    @Override
    public int getAge() {
        return 33;
    }
    //Dogクラスにしかないメソッド
    public String getName() {
        return "john";
    }
}

class AnimalBox<T extends Animal> {
    T someAnimal;

    public AnimalBox(T animal) {
        someAnimal = animal;
    }

    public int getAge() {
        return someAnimal.getAge();
    }
}
  • しかしこの場合単純にAnimal型にしてしまえば、Dogを入れたり同じことができるので、使い分けがよくわからない(ここは課題)
  • 上の課題に対して、ジェネリック型の方は、public T get()を実装すれば、Dogでも返せるが、AnimalBox2ではAnimalしか返せないという違いの例をコメントで教えていただきました。
//これでも良いのでは
class AnimalBox2 {
    Animal someAnimal;

    public AnimalBox2(Animal animal) {
        someAnimal = animal;
    }

    public int getAge() {
        return someAnimal.getAge();
    }
}
  • メソッドの宣言に現れる<? extends Animal>や<? super Animal>は、上記ジェネリック型の制限とは別のもので、境界ワイルドカード型という。
  • ジェネリック型の不変性を緩和するもの。といわれても・・・なので後述
class Box<T> {
    T someValue;
    List<T> history = new ArrayList<>();

    public Box(T v) {
        someValue = v;
    }

    public T get() {
        return someValue;
    }

    public void selectOne(Box<? extends T> box) {
        history.add(box.get());
    }
}
  • クラスの宣言で、class Box<? extends T>やclass Box<? extends Animal>、class Box<T super Animal>とはできない。自分でもそれで何をやりたいのか説明できないはず。

ジェネリック型をいったん忘れて、サブクラスなら代入可能ってどういうことだろう

    Dog d = new Dog();
    Animal a = d; //別の型だけどサブクラスだから代入可能

    System.out.println(a.getClass().getName()); //Dog
    System.out.println(a.getAge()); //33
    System.out.println(d.getName()); //john
    System.out.println(a.getName()); //error: cannot find symbol
  • 上記から)
  • Animal型の変数aの参照先の実体はDogであることを忘れていない
  • Animal型の変数aからは、Dog型に固有のgetNameメソッドは使えない
  • ある実体(この場合、生成されたDog)について、継承関係の上位にある様々なクラスとして使用される可能性が存在する(この場合、Object, Animal, Dog)
  • 下位にあるクラスとしては参照させない。(SpaceDog extends Dogとして、宇宙犬に犬を代入することはできない)
  • なぜできないのか?宇宙犬にはダークマターの多い方に進行する機能が実装されているが、犬にはそれが実装されていないので実行時に困ってしまう。
SpaceDog sd = new Dog(); //Dog cannot be converted to SpaceDog

ジェネリック型の不変性の意味すること

  • ジェネリック型が共変だったとしたらどうなるか
        List<SpaceDog> spaceDogList = new ArrayList<>();
        List<Dog> dogList = spaceDogList; //(共変だったら)別の型だけどサブクラスだから代入可能
        dogList.add(new Dog());
  • List<SpaceDog>に、dogList変数経由でDogが入ってしまった。これはspaceDogListを使う時に困る。これを防ぐために不変にしている。
  • 実際はコンパイルエラー:List<SpaceDog> cannot be converted to List<Dog>

なぜ配列は共変でなりたっているのだろう

  • なりたっていない。コンパイルエラーにはならないが、実行時エラーになる。
        SpaceDog[] spaceDogArray = new SpaceDog[1];
        Dog[] dogArray = spaceDogArray;
        dogArray[0] = new Dog(); //実行時エラー:java.lang.ArrayStoreException: Dog
  • 配列は実行時の型安全。ジェネリック型はコンパイル時の型安全
  • サブクラスも入れるような配列はやめた方が良いでしょう

変数が実体への参照を持っていて、そのクラス継承上の解釈が場面によって違うのはジェネリック型でない普通のクラスでも同じでは?

  • ちょっと何を言っているのかわからないかもしれませんが
  • ↓このようなときに、変数aを使って、型の整合成を破壊することができるか?
        Dog d = new Dog();
        Animal a = d;
        //変数aを用いてなんとかして変数dが型解釈の都合上、困るようにしたい
  • 生成して上書きするくらいしか思いつきませんが、上書きされず別の場所に作られるため、dは困りません
        Dog d = new Dog();
        Animal a = d;
        System.out.println(d); //Dog@77468bd9 
        System.out.println(a); //Dog@77468bd9
        a = new Animal();
        System.out.println(a); //Animal@12bb4df8

ジェネリック型と型パラメータの関係の整理

  • これはできるよ(DogやSpaceDog自体はジェネリック型ではないので、Dogを使う場所でSpaceDogを使うことは可能)
        List<Dog> dogList = new ArrayList<>();
        dogList.add(new Dog());
        dogList.add(new SpaceDog());
  • これはできないよ
class Box<T> {
    T someValue;
    List<T> history = new ArrayList<>();

    public Box(T v) {
        someValue = v;
    }

    public void set(T v) {
        someValue = v;
    }

    public T get() {
        return someValue;
    }

    public void selectOne(Box<T> box) {
        history.add(box.get());
    }
}

Box<Dog> dogBox = new Box<>(new Dog());
Box<SpaceDog> spaceDogBox = new Box<>(new SpaceDog());
dogBox.selectOne(spaceDogBox); //コンパイルエラー:Box<Dog>とBox<SpaceDog>は別の型

  • こうすればできるよ
    public void selectOne(Box<? extends T> box) {
        history.add(box.get());
    }
  • なぜ型の整合性がたもたれるのか?
  • setメソッドにnull以外の値を渡すことが禁止されている(DogをsetできるとBox<SpaceDog>のsomeValueがDogになってしまう)

    public void selectOne(Box<? extends T> box) {
        history.add(box.get());
        box.set(someValue); //コンパイルエラー
    }

<? super T>ってなんだっけ

  • 上記<? extends T>が、「変数を、値の取り出しに機能制限することでジェネリック型の不変性を(共変に)緩和した」と考えると、
  • superの方は、「変数を、値を与えることに機能を制限することでジェネリック型の不変性を(反変に)緩和した」ものとなる。(取り出しはできない)
  • いつ何を使えば良いのか?→Put/Get原則

まとめ

  • 通常のクラスが具体的な対象の属性や振る舞いをまとめたもの(動物、犬・・)とすると、ジェネリック型は、そのまとめたものに関して別の文脈で包むものとしてイメージする
  • 型パラメータとなる通常のクラス(Animal,Dog,SpaceDog)の世界と、ジェネリック型が表現しようとしている文脈(List,Queue,Box,Optional・・・)の世界と2つ存在する
  • 「文脈に値を与える」、「文脈から値を取り出す」という行為が発生するが、不変のままだと型パラメータの継承関係を利用できないので制約が大きい
  • 境界型パラメータによって緩和する

 模式図

java.png

 

0
2
2

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
2