Java汎用ワイルドカード

41229 ワード

この博文は主にJavaプログラミング思想を学ぶいくつかの心得と体得を記録している.この文章の中でいくつかの優秀な博文の内容を引用するかもしれません.私は文章の末尾に博文を引用する住所を明記します.
ワイルドカード
まず、エントリとしてプログラムを提供します.
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class Demo01{
    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10];
        fruit[0] = new Apple();
        fruit[1] = new Jonathan();
        System.out.println(fruit[0]+","+fruit[1]);
        //fruit[0] = new Fruit();
        fruit[1] = new Orange();
        System.out.println(fruit[0]+","+fruit[1]);
    }
} //output:
generics.Apple@4926097b,generics.Jonathan@762efe5d
Exception in thread "main" java.lang.ArrayStoreException: generics.Fruit
    at generics.Demo01.main(Demo01.java:13)

実行結果はすでに与えられました:もちろん、プログラムは実行中にエラーを報告しました.エラーの原因は、Apple配列にAppleタイプではないデータが入っていることです.では、もう一つ問題が出てきました.配列に誤ったデータ型を入れていますが、コンパイラはなぜコンパイル時にエラーを報告しなかったのでしょうか.たとえば、次のプログラムでは、コンパイル期間中にエラーメッセージが表示されます.
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class Demo01{
    public static void main(String[] args) {
        Apple[] apple = new Apple[10];
        apple[0] = new Apple();
        apple[1] = new Jonathan();
        System.out.println(apple[0]+","+apple[1]);
        //error:Type mismatch: cannot convert from Fruit to Apple
        //apple[0] = new Fruit();
        //apple[1] = new Orange();
        System.out.println(apple[0]+","+apple[1]);
    }
} 

このプログラムは、コンパイル時に配列に格納されたデータ型に一致せず、エラーを報告します.
回答:実は第1段のプログラムでは、Fruit[]参照でApple[]を受信し、Fruit配列参照タイプでApple配列を受信し、Fruit配列参照でApple配列にFruitタイプのデータとOrangeタイプのデータを追加すると、コンパイラは通過します.なぜなら、Fruit[]の参照なので、自分のタイプとそのサブタイプのデータを追加しない理由はありません.もちろん、Javaはprivate、static、finalなどの修飾方法以外は動的バインドではなく、他の方法は動的バインドであることを知っています(正しくない場合は、ヒントを求めてください).では、Fruit[]fruit=new Apple[]をまず、AppleはFruitのサブクラスであり、ここでは配列として定義され(我々はそれを特殊な方法と見なすことができる)、次に、Apple配列をFruit配列への参照に戻す(アップグレードに相当する).これにより,コンパイラは,配列にデータを加える際に,コンパイラがコンパイル段階でどの具体的なタイプの配列に入れたのか分からず,プログラムが実行される場合にのみ,配列のタイプを動的に判断することが分かる.
もちろん、逆コンパイルされたコードから結果をより明らかにすることができます.私たちは最初から最後までApple[]オブジェクトを作成しただけです.
 Code:
       0: bipush        10
       2: anewarray     #16                 // class generics/Apple
       5: astore_1
       6: aload_1
       7: iconst_0
       8: new           #16                 // class generics/Apple
      11: dup
      12: invokespecial #18                 // Method generics/Apple."":()V
      15: aastore
      16: aload_1
      17: iconst_1
      18: new           #19                 // class generics/Jonathan
      21: dup
      22: invokespecial #21                 // Method generics/Jonathan."":()V
      25: aastore
      26: getstatic     #22                 // Field java/lang/System.out:Ljava/io/PrintStream;
      29: new           #28                 // class java/lang/StringBuilder
      32: dup
      33: invokespecial #30                 // Method java/lang/StringBuilder."":()V
      36: aload_1
      37: iconst_0
      38: aaload
      39: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      42: ldc           #35                 // String ,
      44: invokevirtual #37                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      47: aload_1
      48: iconst_1
      49: aaload
      50: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      53: invokevirtual #40                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      56: invokevirtual #44                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      59: aload_1
      60: iconst_0
      61: new           #50                 // class generics/Fruit
      64: dup
      65: invokespecial #52                 // Method generics/Fruit."":()V
      68: aastore
      69: aload_1
      70: iconst_1
      71: new           #53                 // class generics/Orange
      74: dup
      75: invokespecial #55                 // Method generics/Orange."":()V
      78: aastore
      79: getstatic     #22                 // Field java/lang/System.out:Ljava/io/PrintStream;
      82: new           #28                 // class java/lang/StringBuilder
      85: dup
      86: invokespecial #30                 // Method java/lang/StringBuilder."":()V
      89: aload_1
      90: iconst_0
      91: aaload
      92: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      95: ldc           #35                 // String ,
      97: invokevirtual #37                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     100: aload_1
     101: iconst_1
     102: aaload
     103: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
     106: invokevirtual #40                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
     109: invokevirtual #44                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     112: return
}

では、上記の問題についていくつかの説明をしました.次に、上記の方法もコンパイル時期に誤報ヒントを通過できるようにするにはどうすればいいかを理解してみましょう.実は、上に保存されている理由は、配列にタイプではないデータが入っているからだと知っています.では、配列に加わるデータのタイプを限定すればいいのです.限定タイプは、ちょうど汎用的な得意なことです.
配列の代わりに汎用容器を選択し、また何か問題が発生するかを見てみましょう.
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class Demo01{
    public static void main(String[] args) {
        //error:Type mismatch: cannot convert from ArrayList to List
        List fruit = new ArrayList();
    }
}

配列の代わりに汎用コンテナを使用して、AppleコンテナをFruitコンテナに割り当てるとコンパイルが通らないのはなぜですか?
まず、第一に、Appleに関する汎用型をFruit汎用型に割り当てることはできません(AppleはFruitのサブタイプですが、汎用型は実行時期に消去されるので、コンパイラは汎用型のクラス情報を知らないので、親子関係ではないと判断するのは言うまでもありません).第二に、汎用型には内蔵のコヒーレントタイプがありません(配列とは異なり、配列には内蔵のコヒーレントタイプがあります.コヒーレント:親容器が子類の容器を持つことができれば、コヒーレントが発生したと言います.)
上の質問に答えた後.私たちは今メインラインに戻りますが、汎用サブコンテナを汎用親コンテナに割り当てる必要があります.つまり、汎用親コンテナが汎用サブクラスを持つことができるコンテナが必要です.どうすればいいのでしょうか?解答:実は上の私たちの需要はワイルドカードが許可しているのです.この場合、プログラムを次のように書き換えることができます.
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class Demo01{
    public static void main(String[] args) {
        List extends Fruit> fruit = new ArrayList();
        //error:The method add(capture#4-of ? extends Fruit) 
        //in the type List is not applicable for the arguments (Object)
        //fruit.add(new Apple());
        //fruit.add(new Orange());
        //fruit.add(new Fruit());
        //fruit.add(new Object());
    }
}

このプログラムでは、汎用親コンテナに汎用子クラスのコンテナを持たせることができます(つまり、現在fruitコンテナにコヒーレントタイプがあります).しかし、この容器に何も追加することはできません.それは一体なぜだろう.
まず、fruitタイプリストについて説明します.
public class Demo01{
    private T value;
    public Demo01() {}
    public Demo01(T val) {
        this.value = val;
    }

    public void set(T val) {
        this.value = val;
    }

    public T get() {
        return value;
    }

    public boolean equals(Object obj) {
        return value.equals(obj);
    }

    public static void main(String[] args) {
        Demo01 apple = new Demo01<>(new Apple());
        Apple a = apple.get();
        System.out.println(a);
        apple.set(a);
        System.out.println(apple.equals(a));

        Demo01 extends Fruit> fruit = apple;
        Fruit f = fruit.get();
        System.out.println(f);
        //   fruit       ,  fruit       Fruit               
        //   set     ,                          ,  ,          
        //fruit.set(a);
        System.out.println(fruit.equals(a));
    }
}//output:
generics.Apple@7637f22
true
generics.Apple@7637f22
true

私たちは一歩一歩上のプログラムを分析します:1.なぜvalueはequals方法を持っていますか:valueは汎用パラメータの中のタイプパラメータなので、コンパイラがプログラムをコンパイルする時に汎用パラメータを彼の第1の境界に消去します.汎用パラメータTの第1の境界を与えていないので、Objectに消去します.自然equals方法はObjectの中の方法です.2.Appleタイプを持つコンテナを使用すると、Appleタイプのデータを持つことができ、そのコンテナに新しいAppleタイプのデータを設定することができます.ただし、Fruitを持つ親コンテナでAppleタイプのコンテナを持つ場合は、親コンテナのタイプを定義する必要があります.
class Fruit{}
class Orange extends Fruit{}
class Jonathan extends Apple{}
class Apple extends Fruit{}
public class Demo01{
    static List apples = new ArrayList<>();
    static List fruits = new ArrayList<>();

    static  void writeExact(List super T> list,T item) {
        list.add(item);
    }

    public static void main(String[] args) {
        writeExact(apples, new Apple());
        writeExact(fruits,new Apple());
        writeExact(fruits,new Fruit());
    }
}

このプログラムは、ワイルドカードを定義することによって?の双曲線コサインを返します.Tタイプが下限の容器を有していると考えられ,この容器にTとそのサブタイプの任意のデータを赤に追加することができる.彼はみな同じ所属だから
class Fruit{}
class Orange extends Fruit{}
class Jonathan extends Apple{}
class Apple extends Fruit{}
public class Demo01{
    static List apples = new ArrayList<>();
    static List fruits = new ArrayList<>();

    static  void writeExact(List list,T item) {
        list.add(item);
    }

    public static void main(String[] args) {
        writeExact(apples, new Apple());
        writeExact(fruits,new Apple());
        writeExact(fruits,new Fruit());
    }
}

このプログラムは通常通り実行でき、何の問題もありません.では、私たちはもう一つの問題があります.この2つのプログラムが同じことをすることができる以上、彼らにはどんな違いがありますか.解答:第一に、下位ワイルドカードを使用するコンテナと、直接ワイルドカードを使用しないコンテナは同じで、パラメータタイプのデータとパラメータタイプの派生タイプのデータを入れることができます.しかし、2つには明らかに異なる意味がある:ワイルドカードを持つ代表コンテナに格納されているデータはいずれも特定のタイプのエクスポートタイプのデータであり、実際にはコンパイラが提示したときに、ワイルドカードを使用するコンテナを見ることができ、addメソッドにはObjectエクスポートタイプのすべてのデータを格納することができる.しかし、下限タイプのデータとエクスポートタイプのデータしか保存できません.ワイルドカードは使用されません.コンテナによって定義されたタイプを表すのは、汎用タイプのデータとエクスポートタイプのデータのみです.
public class Demo01{
    public static void main(String[] args) {
        List super Apple> list = new ArrayList();
        list.add(new Apple());
        list.add(new Jonathan());
        List list2 = new ArrayList();
        list2.add( new Apple());
        list2.add(new Jonathan());
    }
}

第二に、スーパークラスワイルドカードを使用するコンテナからデータを取得し、戻り値のタイプはObjectタイプのみです.一方、汎用パラメータのコンテナを使用して取得したデータ型は、汎用パラメータ型と一致します.
public class Demo01{
    public static void main(String[] args) {
        List super Apple> list = new ArrayList();
        list.add(new Apple());
        Object object = list.get(0);
        System.out.println(object);
        List list2 = new ArrayList();
        list2.add( new Apple());
        Apple apple = list2.get(0);
        System.out.println(apple);
    }
}//output:
generics.Apple@7637f22
generics.Apple@4926097b

以上の説明をしました.より良い拡張のために、ワイルドカードを使用する場合、ワイルドカードの上界と下界のコンテナリストを指定しないという問題を考えることができます.
public class Demo01{
    static List list1;
    static List> list2;
    static List extends Object> list3;

    static void assign1(List list) {
        list1=list;
        list2=list;
        list3=list;
    }

    static void assign2(List> list) {
        list1=list;
        list2=list;
        list3=list;
    }

    static void assign3(List extends Object> list) {
        list1=list;
        list2=list;
        list3=list;
    }

    public static void main(String[] args) {
        assign1(new ArrayList());
        assign2(new ArrayList());
        assign3(new ArrayList());

        assign1(new ArrayList());
        assign2(new ArrayList());
        assign3(new ArrayList());
    }
}

このプログラムにより,コンパイラは無境界ワイルドカードの容器と原生容器の関係に関心を持たないことが分かった.感じて
public class Demo01{
    public static void main(String[] args) {
        List list1 = new ArrayList();
        list1.add(new Object());
        list1.add(new Integer(5));
        list1.add(new String("a"));

        List> list2 = new ArrayList<>();
        //error:The method add(capture#1-of ?) in the type List is not applicable for the arguments (Object)
        //list2.add(new Object());
        //error:The method add(capture#1-of ?) in the type List is not applicable for the arguments (Integer)
        //list2.add(new Integer(5));
        //error:The method add(capture#1-of ?) in the type List is not applicable for the arguments (String)
        //list2.add(new String("a"));

        List extends Object> list3 = new ArrayList<>();
        //error:The method add(capture#1-of ? extends Object) in the type ListObject> is not applicable for the arguments (Object)
        //list3.add(new Object());
        //error:The method add(capture#1-of ? extends Object) in the type ListObject> is not applicable for the arguments (Integer)
        //list3.add(new Integer(5));
        //error:The method add(capture#2-of ? extends Object) in the type ListObject> is not applicable for the arguments (String)
        //list3.add(new String("a"));

        List super Object> list4 = new ArrayList<>();
        list4.add(new Object());
        list4.add(new Integer(5));
        list4.add(new String("a"));
    }
}

このプログラムから,無境界ワイルドカードを用いる場合,List
に質問
汎用とワイルドカードについていろいろ説明しましたが、次に汎用の問題についてお話しします.
1.任意の基本データ型は汎用的なパラメータとして使用できない
この問題については、あまり説明しません.
2.パラメトリックインターフェースの実現
すなわち、1つのクラスは、同じ汎用インタフェースの2つの変異体(すなわち、汎用インタフェースにおける汎用パラメータが異なる)を実現する.実は汎用消去のため、2つのバリエーションでも同じインタフェースになります.
まず,1つのクラスが同じインタフェースを同時に継承できるかを検討する.もちろん否定です.
interface Payable{}
//error:Duplicate interface Payable for the type A
//class A implements Payable,Payable{}

しかし、私たちが次のように書くと、プログラムは間違っていません.
interface Payable{}
class A implements Payable{}
class B extends A implements Payable{}

次に、汎用インタフェースを実装するクラスの2つの変形について説明します.その結果はどうなりますか.
interface Payable<T>{}
class Employee implements Payable<Employee>{}
//The interface Payable cannot be implemented more than once with different arguments: 
//Payable and Payable
//public class Demo01 extends Employee implements Payable{}

結果はコンパイルできません:エラーの原因は、1つのインタフェースで2つの異なるパラメータを実現できないためです.
では、実装するインタフェースは同じパラメータを使用しますか?
interface Payable<T>{}
class Employee implements Payable<Employee>{}
public class Demo01 extends Employee implements Payable<Employee>{}

不思議なことにコンパイルに合格しました!
3.重荷
汎用パラメータでは関数のリロードを区別できません.原因は消去です.
interface Payable{}
public class Demo01{
    //Erasure of method f(Payable) 
    //is the same as another method in type Demo01
    //error:
    public void f(Payable p) {

    }
    //error:
    public void f(Payable p2) {

    }

}

このプログラムはもちろんコンパイルできません!
大まかな汎用型について一定の解釈をした後、次は次の部分に進みます.
自己限定タイプ
自己限定タイプについては、最初は、やはり私には理解しにくいですが、各クラスの関係を詳しく分析することで、後で理解しやすいです.
では、私は直接コードを出して分析しましたよ.
class SelfBounded<T extends SelfBounded<T>>{}

上のコードは、確かに変な感じがします.では、このプログラムを直接分析しましょう.まず、SelfBoundedは汎用クラスであり、汎用パラメータTがあり、この汎用パラメータにはSelfBoundedがあることを知ることができます.そして,パラメータ型Tの上界はSelfBoundedであり,この上界もTである汎用クラスであることを見いだした.すなわち,Tには境界があり,この境界はTをそのパラメータとして持つ自身である.
次に、次のプログラムを見てみましょう.
class SelfBounded<T extends SelfBounded<T>>{
    private T entity;

    public SelfBounded set(T obj){
        this.entity = obj;
        return this;
    }

    public T get() {
        return entity;
    }
}

class A extends SelfBounded<A>{};
class B extends SelfBounded<A>{}

class E{}
//Bound mismatch: The type E is not a 
//valid substitute for the bounded parameter > 
//of the type SelfBounded
class F extends SelfBounded<E>{}

このプログラムについては,EがSelfBoundedのパラメータとして機能しないのはなぜでしょうか.まず、SelfBoundedは汎用消去のため、タイプパラメータをすべてSelfBoundedタイプに置き換えます.EはSelfBoundedのサブクラスではないので、EはSelfBoundedのタイプパラメータとして送信できません.では、なぜAはパラメータタイプとして伝わるのでしょうか.なぜなら,我々はAを定義する際に,AがSelfBoundedのサブクラスであることを明らかにしたからである.では、上記の手順を変更すると、このような解釈に問題があるとどう思いますか?
class SelfBounded<T extends SelfBounded<T>>{
    private T entity;

    public SelfBounded set(T obj){
        this.entity = obj;
        return this;
    }

    public T get() {
        return entity;
    }
}

class A extends SelfBounded<A>{}
class B extends SelfBounded<A>{}

//Bound mismatch: The type B is not a valid substitute 
//for the bounded parameter > of the type SelfBounded
class F extends SelfBounded<B>{}

このプログラムは、Bをパラメータタイプとして、プログラムが依然として間違っていることを発見しました.エラーの原因は上と同じです.では、このエラーの原因はいったい何なのでしょうか.なぜなら,我々は直接継承とサブクラスの関係であるが,SelfBoundedに対しては無視しているからである.