Heim  >  Artikel  >  Java  >  11.Java-Grundlagen – Generisches

11.Java-Grundlagen – Generisches

黄舟
黄舟Original
2017-02-27 10:43:58930Durchsuche

Grundkonzepte

Das Wesen von Generika ist die Anwendung von parametrisiertem Typ, das heißt , der Datentyp, mit dem gearbeitet wird, wird als Parameter angegeben, und bei Verwendung wird der spezifische Typ angegeben.

Dieser Parametertyp kann bei der Erstellung von Klassen, Schnittstellen und Methoden verwendet werden, die als generische Klassen, generische Schnittstellen bzw. generische Methoden bezeichnet werden.


1. Entwicklung

Vor JDK 1.5 war nur Object die übergeordnete Klasse aller Typen und Typumwandlungen Merkmale können eine Typverallgemeinerung erreichen.

Daher kann der Compiler während der Kompilierung nicht prüfen, ob die Umwandlung dieses Objekts erfolgreich ist, was zu einer ClassCastException (Umwandlungsausnahme) führen kann.

Sehen wir uns ein Beispiel an, um die Rolle von Generika zu verstehen:

  • Wenn keine Generika verwendet werden (vor 1.5)

ArrayList arrayList = new ArrayList();
arrayList.add(100);
arrayList.add("abc");//因为不知道取出来的值的类型,类型转换的时候容易出错  String str = (String) arrayList.get(0);
  • Bei Verwendung von Generika (nach 1.5)

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

2. Terminologie

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

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

ArrayList<Integer> :参数化的类型

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

ArrayList :原始类型

3. Entdecken

Generische Klassen

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
    }
}

Generische Schnittstellen

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);
    }
}

Generische Methoden

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;
    }
}

Typqualifizierung

Typqualifizierung kann in generischen Klassen, generischen Schnittstellen und generischen Methoden verwendet werden. Beachten Sie jedoch die folgenden Punkte:

  • Unabhängig davon, ob es sich bei der Qualifikation um eine Klasse oder eine Schnittstelle handelt, kann das Schlüsselwort „extends“

  • mit dem &-Symbol angegeben werden. Mehrere Qualifikationen

  • Wenn die Qualifikation sowohl Schnittstellen als auch Klassen hat, darf es nur eine Klasse geben und diese muss an erster Stelle stehen. Zum Beispiel:

public static <T extends Comparable&Serializable> T get(T t1,T t2)

Lassen Sie uns die Rolle der Typqualifizierung analysieren...


1 Legen Sie keine Grenzen für Typparameter fest

Beachten Sie den folgenden Code. Wenn die Typparameter nicht typqualifiziert sind, tritt ein Kompilierungsfehler auf. Der Grund ist wie folgt:

  • Da der Compiler vor der Kompilierung nicht bestätigen kann, um welchen Typ es sich beim generischen Typ (T) handelt

  • , wird er standardmäßig verwendet zu T Ist ein primitiver Typ (Objekt).

  • Sie können also nur die Object-Methode aufrufen, nicht jedoch die CompareTo-Methode.

public static <T> T get(T t1,T t2) {    //编译错误
    if(t1.compareTo(t2)>=0);    return t1;
}

2. Grenzen für Typparameter festlegen

Nachdem Sie Grenzen für Typparameter T festgelegt haben, kompilieren Sie den Fehler nicht mehr auftritt. Denn zu diesem Zeitpunkt verwendet der Compiler standardmäßig den ursprünglichen T-Typ als Comparable.

public static <T extends Comparable> T get(T t1,T t2) {    if(t1.compareTo(t2)>=0);    return t1;
}

Typlöschung

  • Generika in Java werden grundsätzlich auf Compilerebene implementiert.

  • Die Typinformationen in Generics sind nicht im generierten Java-Bytecode enthalten.

  • Die bei der Verwendung von Generika hinzugefügten Typparameter werden vom Compiler während der Kompilierung entfernt. Dieser Vorgang wird als Typlöschung bezeichnet.


Sehen Sie sich das folgende Beispiel an:

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());
    }
}

Beachten Sie den Code, hier sind zwei ArrayList-Arrays definiert:

  • Einer ist der generische Typ ArrayList, der nur Zeichenfolgen speichern kann, und der andere ist der generische Typ ArrayList, der nur Ganzzahlen speichern kann.

  • Durch den Vergleich ihrer Klassenobjekte wird festgestellt, dass das Ergebnis wahr ist.

  • Erklärung, dass die generischen Typen String und Integer während des Kompilierungsprozesses gelöscht werden und nur der ursprüngliche Typ (d. h. Object) übrig bleibt.


Sehen wir uns ein weiteres Beispiel an:

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));  
            }  
    }
}

Beobachten Sie den Code, hier wird ein generisches ArrayList-Typobjekt als Integer instanziiert

  • Wenn Sie die Add-Methode direkt aufrufen, können nur ganzzahlige Daten gespeichert werden.

  • Verwenden Sie Reflection, um die Add-Methode aufzurufen, Sie können jedoch Zeichenfolgen speichern.

  • Erklärung Generische Ganzzahlinstanzen werden nach der Kompilierung gelöscht, sodass nur der ursprüngliche Typ übrig bleibt.


1. Rohtyp

  • Der Rohtyp (Rohtyp) löscht die allgemeinen Informationen und schließlich der wahre Typ der Typvariablen im Bytecode .

  • Jeder generische Typparameter verfügt über eine entsprechende primitive Variable.

  • Sobald eine Typvariable gelöscht wird, wird sie durch ihren qualifizierten Typ ersetzt (nicht qualifizierte Variablen sind Objekt).

// 此时 T 是一个无限定类型,所以原始类型就是 Objectclass Pair<T> { } 

// 类型变量有限定,原始类型就用第一个边界的类型变量来替换,即Comparableclass Pair<T extends Comparable& Serializable> { }  

// 此时原始类型为 Serializable,编译器在必要的时要向 Comparable 插入强制类型转换
// 为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界限定列表的末尾class Pair<T extends Serializable&Comparable>

2. Typ des Typparameters

Im folgenden Beispiel bezieht sich der Typparameter auf T, den Typ von T Es handelt sich um den sogenannten [Typparameter]-Typ.

Wenn wir den Code beobachten, können wir die folgenden Schlussfolgerungen ziehen:

  • Geben Sie den Typ von [Typparameter T] nicht an Der ursprüngliche Typ verwendet dieselbe übergeordnete Klasse. Das Mindestniveau von

  • Bei der Angabe des Typs von [Typparameter T] kann der ursprüngliche Typ nur der angegebene Typ oder eine Unterklasse des Typs

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. Typprüfung

Die Typprüfung von Generika dient der Referenz, nicht dem referenzierten Objekt selbst.

Im folgenden Beispiel ist list ein Referenzobjekt, daher erfolgt die Typprüfung dagegen.

// 没有进行类型检查,等价于 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)!


Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Vorheriger Artikel:10.Java-Grundlagen – AgentNächster Artikel:10.Java-Grundlagen – Agent