ホームページ  >  記事  >  Java  >  11.Java の基本 - ジェネリックス

11.Java の基本 - ジェネリックス

黄舟
黄舟オリジナル
2017-02-27 10:43:58928ブラウズ

基本概念

ジェネリックの本質は、パラメータ化された型の適用です。つまり、演算対象のデータ型はパラメータとして指定され、詳細は型を使用するときに指定されます。

このパラメータ タイプは、それぞれジェネリック クラス、ジェネリック インターフェイス、ジェネリック メソッドと呼ばれるクラス、インターフェイス、メソッドの作成に使用できます。


1. 開発

JDK 1.5 より前では、型の一般化は、すべての型の親クラスである Object と型キャストの組み合わせによってのみ実現できました。

そのため、コンパイル中に、コンパイラーはこのオブジェクトのキャストが成功したかどうかを確認できず、ClassCastException (キャスト例外) が発生する可能性があります。

ジェネリックの役割を理解するために以下の例を見てみましょう:

  • ジェネリックを使用しない場合(1.5以前)

ArrayList arrayList = new ArrayList();
arrayList.add(100);
arrayList.add("abc");//因为不知道取出来的值的类型,类型转换的时候容易出错  String str = (String) arrayList.get(0);
  • ジェネリックを使用する場合(1.5以降)

ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("abc");//因为限定了类型,所以不能添加整形,编译器会提示出错arrayList.add(100);

2. 用語

// 以 ArrayList<E>,ArrayList<Integer> 为例:ArrayList<E>:泛型类型

E:类型变量(或者类型参数)

ArrayList<Integer> :参数化的类型

Integer:类型参数的实例(或实际类型参数)

ArrayList :原始类型

3. ジェネリッククラス
class Demo<T> {    private T value;

    Demo(T value) {        this.value = value;
    }    public T getValue() {        return value;
    }    public void setValue(T value) {        this.value = value;
    }
}public class Test {
    public static void main(String[] args) {
        Demo<String> demo = new Demo("abc");
        demo.setValue("cba");
        System.out.println(demo.getValue()); // cba
    }
}

ジェネリックインターフェイス

interface Demo<K, V> {    void print(K k, V v);
}

class DemoImpl implements Demo<String, Integer> {    @Override
    public void print(String k, Integer v) {
        System.out.println(k + "-" + v);
    }
}public class Test {
    public static void main(String[] args) {
        Demo<String, Integer> demo = new DemoImpl();
        demo.print("abc", 100);
    }
}

ジェネリックメソッド

public class Test {
    public static void main(String[] args) {        int num = get("abc", 100);
        System.out.println(num);
    }    // 关键 --> 多了 <K, V> ,可以理解为声明此方法为泛型方法
    public static <K, V> V get(K k, V v) {        if (k != null) {            return v;
        }        return null;
    }
}

型を調べる汎用クラスに限定されており、汎用クラスで使用できます。インターフェースとジェネリックメソッドを使用しますが、次の点に注意してください:

修飾がクラスかインターフェースかに関係なく、キーワード extends が使用されます

    & 記号を使用して複数の修飾を与えることができます
  • 資格にインターフェイスとクラスの両方がある場合、クラスは 1 つだけ存在し、それが最初に配置される必要があります。例:
  • public static <T extends Comparable&Serializable> T get(T t1,T t2)
  • 型修飾の役割を分析しましょう...
  • 1. 型パラメータに境界を設定しないでください

型パラメータが type- でない場合、コンパイル エラーが発生することに注意してください。資格のある。その理由は次のとおりです。

コンパイル前に、コンパイラはジェネリック型 (T) がどのような型であるかを確認できないため

    そのため、プリミティブ型 (Object) としてデフォルトの T が設定されます。
  • つまり、呼び出すことができるのは Object メソッドのみであり、compareTo メソッドは呼び出すことができません。
  • public static <T> T get(T t1,T t2) {    //编译错误
        if(t1.compareTo(t2)>=0);    return t1;
    }
  • 2. 型パラメータに境界を設定する

型パラメータ T に境界を設定すると、コンパイル エラーが発生しなくなります。現時点では、コンパイラはデフォルトで T の元の型を Comparable として設定するためです。
public static <T extends Comparable> T get(T t1,T t2) {    if(t1.compareTo(t2)>=0);    return t1;
}

型消去


Java のジェネリックスは、基本的にコンパイラ レベルで実装されます。

    生成された Java バイトコードには、ジェネリックスの型情報が含まれません。
  • ジェネリックを使用するときに追加された型パラメーターは、コンパイル中にコンパイラーによって削除されます。このプロセスは型消去と呼ばれます。
  • 次の例を見てください:

    public class Test {
        public static void main(String[] args) {
            ArrayList<String> arrayList1 =new ArrayList<String>();
            ArrayList<Integer> arrayList2 = new ArrayList<Integer>();        // true
            System.out.println(arrayList1.getClass() == arrayList2.getClass());
        }
    }

    コードを観察すると、ここでは 2 つの ArrayList 配列が定義されています:

1 つは文字列のみを格納できる ArrayList ジェネリック型で、もう 1 つは ArrayList ジェネリック型です, 整数のみを保存できます。

    それらのクラスオブジェクトを比較すると、結果が true であることがわかります。
  • 説明: ジェネリック型 String と Integer はコンパイル プロセス中に消去され、元の型 (つまり Object) だけが残ります。
  • 別の例を見てください:

    public class Test {
        public static void main(String[] args) throws Exception{
            ArrayList<String> arrayList =new ArrayList<String>();
            arrayList.add("abc");
            arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, 100);         for (int i=0;i<arrayList.size();i++) {  
                    System.out.println(arrayList.get(i));  
                }  
        }
    }

    コードを観察してください。ここでは ArrayList ジェネリック型が Integer オブジェクトとして定義され、インスタンス化されています

add メソッドを直接呼び出した場合、整数データのみを格納できます。

    リフレクションを使用して add メソッドを呼び出しますが、文字列を保存することもできます。
  • 説明 整数ジェネリックインスタンスはコンパイル後に消去され、元の型のみが残ります。
  • 1. Raw 型


元の型 (Raw 型) は、汎用情報を消去した後の バイトコード内の型変数の実型です。

  • ジェネリック型パラメーターには、対応するプリミティブ変数があります。

    型変数が消去 (破損) されると、修飾された型に置き換えられます (修飾されていない変数は Object を使用します)。
  • // 此时 T 是一个无限定类型,所以原始类型就是 Objectclass Pair<T> { } 
    
    // 类型变量有限定,原始类型就用第一个边界的类型变量来替换,即Comparableclass Pair<T extends Comparable& Serializable> { }  
    
    // 此时原始类型为 Serializable,编译器在必要的时要向 Comparable 插入强制类型转换
    // 为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界限定列表的末尾class Pair<T extends Serializable&Comparable>

  • 2. 型パラメータの型

  • 次の例では、型パラメータは T を指し、T の型はいわゆる [型パラメータ] の型です。

コードを観察すると、次の結論を導き出すことができます:

[型パラメータ T] の型を指定しないでください。パラメータの型が一致しない場合、元の型は同じ親クラスの最小レベルを取得します

[型パラメータ T] ] 型を指定します。元の型は、指定された型または型のサブクラスのみです
  • public class Test {    // 定义泛型方法
        public static <T> T add(T x, T y) {        return y;
        }    public static void main(String[] args) {        // 1.不指定泛型
    
            // 两个参数都是 Integer,所以 T 为 Integer 类型
            int i = Test.add(1, 2); 
    
            // 两个参数分别是 Integer,Float,取同一父类的最小级,T 为 Number 类型
            Number f = Test.add(1, 1.2);        // T 为 Object
            Object o = Test.add(1, "asd");        // 2.指定泛型
    
            // 指定了Integer,所以只能为 Integer 类型或者其子类
            int a = Test.<Integer> add(1, 2);        //编译错误,指定了 Integer,不能为Float
            int b=Test.<Integer>add(1, 2.2); 
    
             // 指定为Number,所以可以为 Integer,Float
            Number c = Test.<Number> add(1, 2.2);
        }
    
    }

  • 3. ジェネリックスの型チェックは参考用です。参照されるオブジェクト自体に対するものではありません。

    以下の例では、list は参照オブジェクトであるため、型チェックはそれに対して行われます。
  • // 没有进行类型检查,等价于 ArrayList list = new ArrayLis()ArrayList list = new ArrayList<String>();
    list.add(100);
    list.add("hello");// 进行编译检查,等价于 ArrayList<String> list = new ArrayList<String>();ArrayList<String> list = new ArrayList();
    list.add("hello");
    list.add(100);  // 编译错误

    4.类型擦除与多态的冲突

    来看下面的例子,这里定义了一个泛型类 Parent,一个实现它的子类 Son,并在子类中重写了父类的方法。

    class Parent<T> {    private T value;    public T getValue() {        return value;
        }    public void setValue(T value) {        this.value = value;
        }
    }
    
    class Son extends Parent<String>{    @Override
        public void setValue(String value) {         super.setValue(value);
        }    @Override
        public String getValue(){        return super.getValue();}
        }

    在上面提到过泛型的类型参数在编译时会被类型擦除,因此编译后的 Parent 类如下:

    class Parent {    private Object value;    public Object getValue() {        return value;
        }    public void setValue(Object value) {        this.value = value;
        }
    }

    此时对比 Parent 与 Son 的 getValue/setValue 方法,发现方法的参数类型已经改变,从 Object -> String,这也意味着不是重写(overrride) 而是重载(overload)。

    然而调用 Son 的 setValue 方法, 发现添加 Object 对象时编译错误。说明也不是重载。

    public class Test {
        public static void main(String[] args) {
            Son son = new Son();
            son.setValue("hello");        // 关键 -->编译错误
            son.setValue(new Object());
        }
    }

    那么问题来了,通过上面的分析?Son 中定义的方法到底是重写还是重载?答案是:重写。这里 JVM 采用了桥方法(Brige)来解决类型擦除和多态引起的冲突。

    我们对 Son 进行反编译(”Javap -c 类名.class”),得到如下内容:

    Compiled from "Test.java"class Son extends Parent<java.lang.String> {
      Son();
        Code:       0: aload_0       
           1: invokespecial #8                  // Method Parent."<init>":()V
           4: return        
    
      public void setValue(java.lang.String);
        Code:       0: aload_0       
           1: aload_1       
           2: invokespecial #16                 // Method Parent.setValue:(Ljava/lang/Object;)V
           5: return        
    
      public java.lang.String getValue();
        Code:       0: aload_0       
           1: invokespecial #23                 // Method Parent.getValue:()Ljava/lang/Object;
           4: checkcast     #26                 // class java/lang/String
           7: areturn       
    
      public java.lang.Object getValue();
        Code:       0: aload_0       
           1: invokevirtual #28                 // Method getValue:()Ljava/lang/String;
           4: areturn       
    
      public void setValue(java.lang.Object);
        Code:       0: aload_0       
           1: aload_1       
           2: checkcast     #26                 // class java/lang/String
           5: invokevirtual #30                 // Method setValue:(Ljava/lang/String;)V
           8: return        }

    发现这里共有 4 个 setValue/getValue 方法,除了 Son 表面上重写的 String 类型,编译器又自己生成了 Object 类型的方法,也称为桥方法。结果就是,编译器通过桥方法真正实现了重写,只是在访问时又去调用表面的定义的方法。


    注意事项

    • 不能用基本类型实例化类型参数,可以用对应的包装类来实例化类型参数

    // 编译错误ArrayList<int> list = new ArrayList<int>();// 正确写法ArrayList<Integer> list = new ArrayList<Integer>();
    • 参数化类型的数组不合法

    Demo<T >{
    }public class Test {
        public static void main(String[] args) {        // 编译错误 --> 类型擦除导致数组变成 Object [],因此没有意义
            Demo<String>[ ]  demo =new Demo[10];
        }
    }
    • 不能实例化类型变量

    // 编译错误,需要类型参数需要确定类型Demo<T> demo = new Demo<T>
    • 泛型类的静态上下文中不能使用类型变量

    public class Demo<T> {
        public static T name;    public static T getName() {
            ...
        }
    }
    • 不能抛出也不能捕获泛型类的对象

    //异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉。会导致这里捕获的类型一致try{  
    }catch(Problem<Integer> e1){  
        //do Something... }catch(Problem<Number> e2){  
        // do Something ...}

     以上就是11.Java 基础 - 泛型的内容,更多相关内容请关注PHP中文网(www.php.cn)!


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。