Heim  >  Artikel  >  Java  >  Detaillierte Einführung in Java-Generika

Detaillierte Einführung in Java-Generika

尚
nach vorne
2019-11-27 14:31:501697Durchsuche

Detaillierte Einführung in Java-Generika

1. Die Einführung des Konzepts der Generika (warum werden Generika benötigt)? (Empfohlen: Java-Video-Tutorial)

Schauen wir uns zunächst den folgenden Kurzcode an:

public class GenericTest {

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("qqyumidi");
        list.add("corn");
        list.add(100);

        for (int i = 0; i < list.size(); i++) {
            String name = (String) list.get(i); // 1
            System.out.println("name:" + name);
        }
    }
}

definiert eine Sammlung vom Typ „Liste“ Zwei Werte ​​vom Typ String werden hinzugefügt, gefolgt von einem Wert vom Typ Integer. Dies ist völlig zulässig, da der Standardtyp der Liste „Objekt“ ist.

In nachfolgenden Schleifen können leicht Fehler ähnlich wie //1 auftreten, weil vergessen wurde, Werte vom Typ Integer vorher zur Liste hinzuzufügen, oder aus anderen Codierungsgründen. Da die Kompilierungsphase normal ist, tritt zur Laufzeit jedoch die Ausnahme „java.lang.ClassCastException“ auf. Daher sind solche Fehler beim Codieren schwer zu erkennen.

Während des obigen Codierungsprozesses haben wir festgestellt, dass es zwei Hauptprobleme gibt:

1 Wenn wir ein Objekt in eine Sammlung einfügen, merkt sich die Sammlung nicht den Typ des Objekts Dieses Objekt wird wieder aus der Sammlung entfernt, der kompilierte Typ des Objekts ändert sich in den Objekttyp, aber sein Laufzeittyp ist immer noch ein eigener Typ.

2. Daher ist beim Herausnehmen der Sammlungselemente bei //1 eine künstliche erzwungene Typkonvertierung in einen bestimmten Zieltyp erforderlich, und es kann zu „java.lang.ClassCastException“-Ausnahmen kommen.

Gibt es also eine Möglichkeit, einer Sammlung zu ermöglichen, sich die Elementtypen in der Sammlung zu merken, sodass während der Laufzeit keine „java.lang.ClassCastException“-Ausnahmen auftreten, solange beim Kompilieren keine Probleme auftreten? ? Die Antwort ist die Verwendung von Generika.

2. Was sind Generika?

Generika, also „parametrisierte Typen“. Wenn es um Parameter geht, ist es am bekanntesten, dass beim Definieren einer Methode formale Parameter vorhanden sind und beim Aufruf dieser Methode die tatsächlichen Parameter übergeben werden.

Wie versteht man also parametrisierte Typen? Wie der Name schon sagt, wird der Typ anhand des ursprünglichen spezifischen Typs parametrisiert, ähnlich wie die variablen Parameter in der Methode. Zu diesem Zeitpunkt wird der Typ auch in Form von Parametern (die als Typparameter bezeichnet werden können) definiert Beim Verwenden/Aufrufen des Typs (Typargument) wird ein bestimmter Typ übergeben.

Es scheint etwas kompliziert zu sein. Schauen wir uns zunächst die allgemeine Schreibweise des obigen Beispiels an.

public class GenericTest {

    public static void main(String[] args) {
        /*
        List list = new ArrayList();
        list.add("qqyumidi");
        list.add("corn");
        list.add(100);
        */

        List<String> list = new ArrayList<String>();
        list.add("qqyumidi");
        list.add("corn");
        //list.add(100);   // 1  提示编译错误

        for (int i = 0; i < list.size(); i++) {
            String name = list.get(i); // 2
            System.out.println("name:" + name);
        }
    }
}

Nach der Verwendung generischen Schreibens tritt ein Kompilierungsfehler auf, wenn versucht wird, ein Objekt vom Typ Integer unter //1 hinzuzufügen. Durch Listf7e83be87db5cd2d9a8a0b8117b38cd4 ist es direkt auf die Listensammlung beschränkt, die nur den Typ String enthalten kann Elemente, daher ist keine erzwungene Typkonvertierung bei //2 erforderlich, da sich die Sammlung zu diesem Zeitpunkt die Typinformationen des Elements merken kann und der Compiler bereits bestätigen kann, dass es sich um einen String-Typ handelt.

In Kombination mit der obigen generischen Definition wissen wir, dass String in Listf7e83be87db5cd2d9a8a0b8117b38cd4 ein tatsächlicher Typparameter ist, das heißt, die entsprechende List-Schnittstelle muss Typparameter enthalten. Und das Rückgabeergebnis der get()-Methode ist auch direkt der Typ dieses formalen Parameters (d. h. der entsprechende tatsächliche Parameter des eingehenden Typs). Werfen wir einen Blick auf die spezifische Definition der List-Schnittstelle:

public interface List<E> extends Collection<E> {

    int size();

    boolean isEmpty();

    boolean contains(Object o);

    Iterator<E> iterator();

    Object[] toArray();

    <T> T[] toArray(T[] a);

    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

    boolean addAll(int index, Collection<? extends E> c);

    boolean removeAll(Collection<?> c);

    boolean retainAll(Collection<?> c);

    void clear();

    boolean equals(Object o);

    int hashCode();

    E get(int index);

    E set(int index, E element);

    void add(int index, E element);

    E remove(int index);

    int indexOf(Object o);

    int lastIndexOf(Object o);

    ListIterator<E> listIterator();

    ListIterator<E> listIterator(int index);

    List<E> subList(int fromIndex, int toIndex);
}

Wir können sehen, dass nach der Übernahme der generischen Definition in die List-Schnittstelle das E in 1a4db2c2c2313771e5742b6debf617a1 den Typparameter darstellt und einen bestimmten Typ empfangen kann Argumente, und in dieser Schnittstellendefinition stellt E überall dort, wo E erscheint, das gleiche Typargument dar, das von außen empfangen wurde.

ArrayList ist natürlich die Implementierungsklasse der List-Schnittstelle und ihre Definitionsform lautet:

public class ArrayList<E> extends AbstractList<E> 
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    public E get(int index) {
        rangeCheck(index);
        checkForComodification();
        return ArrayList.this.elementData(offset + index);
    }
    
    //...省略掉其他具体的定义过程

}

Daraus verstehen wir aus der Quellcode-Perspektive, warum beim Hinzufügen von ein Kompilierungsfehler auftritt Objekt vom Typ Integer bei //1 und der von get() bei //2 erhaltene Typ ist direkt der Typ String.

3. Angepasste generische Schnittstellen, generische Klassen und generische Methoden

Aus dem obigen Inhalt hat jeder den spezifischen Betriebsprozess von Generika verstanden. Wir wissen auch, dass Schnittstellen, Klassen und Methoden auch mithilfe von Generics definiert und entsprechend verwendet werden können. Ja, bei spezifischer Verwendung kann es in generische Schnittstellen, generische Klassen und generische Methoden unterteilt werden.

Benutzerdefinierte generische Schnittstellen, generische Klassen und generische Methoden ähneln List und ArrayList im obigen Java-Quellcode. Im Folgenden betrachten wir die einfachste Definition generischer Klassen und Methoden:

public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        System.out.println("name:" + name.getData());
    }

}

class Box<T> {

    private T data;

    public Box() {

    }

    public Box(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

}

Beim Definieren generischer Schnittstellen, generischer Klassen und generischer Methoden sehen wir häufig T, E, K, V. Parameter derselben form werden häufig zur Darstellung generischer Parameter verwendet, da sie Typargumente empfangen, die von externen Verwendungen übergeben werden. Sind also für verschiedene übergebene Typargumente die Typen der entsprechenden generierten Objektinstanzen gleich?

public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);

        System.out.println("name class:" + name.getClass());      // com.qqyumidi.Box
        System.out.println("age class:" + age.getClass());        // com.qqyumidi.Box
        System.out.println(name.getClass() == age.getClass());    // true

    }

}

Daraus haben wir herausgefunden, dass bei der Verwendung generischer Klassen zwar unterschiedliche generische Argumente übergeben werden, jedoch nicht tatsächlich unterschiedliche Typen generiert werden. Es gibt nur eine generische Klasse im Speicher, nämlich den ursprünglichsten Grundtyp (. Box in diesem Beispiel). Natürlich können wir es logischerweise als mehrere verschiedene generische Typen verstehen.

Der Grund dafür ist, dass der Zweck des Konzepts der Generika in Java darin besteht, dass es nur in der Code-Kompilierungsphase wirkt. Während des Kompilierungsprozesses werden die Generika relevante Informationen sein, nachdem die generischen Ergebnisse korrekt überprüft wurden gelöscht, das heißt, die erfolgreich kompilierte Klassendatei enthält keine generischen Informationen. Allgemeine Informationen gelangen nicht in die Laufzeitphase.

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。 

四.类型通配符

接着上面的结论,我们知道,Boxc8f01a3f8889dcf657849dd45bc0fc4c和Boxc0f559cc8d56b43654fcbe4aa9df7b4a实际上都是Box类型,现在需要继续探讨一个问题,那么在逻辑上,类似于Boxc8f01a3f8889dcf657849dd45bc0fc4c和Boxc0f559cc8d56b43654fcbe4aa9df7b4a是否可以看成具有父子关系的泛型类型呢?

为了弄清这个问题,我们继续看下下面这个例子:

public class GenericTest {

    public static void main(String[] args) {

        Box<Number> name = new Box<Number>(99);
        Box<Integer> age = new Box<Integer>(712);

        getData(name);
        
        //The method getData(Box<Number>) in the type GenericTest is 
        //not applicable for the arguments (Box<Integer>)
        getData(age);   // 1

    }
    
    public static void getData(Box<Number> data){
        System.out.println("data :" + data.getData());
    }

}

我们发现,在代码//1处出现了错误提示信息:The method getData(Boxc8f01a3f8889dcf657849dd45bc0fc4c) in the t ype GenericTest is not applicable for the arguments (Boxc0f559cc8d56b43654fcbe4aa9df7b4a)。显然,通过提示信息,我们知道Boxc8f01a3f8889dcf657849dd45bc0fc4c在逻辑上不能视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类。那么,原因何在呢?

public class GenericTest {

    public static void main(String[] args) {

        Box<Integer> a = new Box<Integer>(712);
        Box<Number> b = a;  // 1
        Box<Float> f = new Box<Float>(3.14f);
        b.setData(f);        // 2

    }

    public static void getData(Box<Number> data) {
        System.out.println("data :" + data.getData());
    }

}

class Box<T> {

    private T data;

    public Box() {

    }

    public Box(T data) {
        setData(data);
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

}

这个例子中,显然//1和//2处肯定会出现错误提示的。在此我们可以使用反证法来进行说明。

假设Boxc8f01a3f8889dcf657849dd45bc0fc4c在逻辑上可以视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类,那么//1和//2处将不会有错误提示了,那么问题就出来了,通过getData()方法取出数据时到底是什么类型呢?Integer? Float? 还是Number?且由于在编程过程中的顺序不可控性,导致在必要的时候必须要进行类型判断,且进行强制类型转换。显然,这与泛型的理念矛盾,因此,在逻辑上Boxc8f01a3f8889dcf657849dd45bc0fc4c不能视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类。

好,那我们回过头来继续看“类型通配符”中的第一个例子,我们知道其具体的错误提示的深层次原因了。那么如何解决呢?总部能再定义一个新的函数吧。

这和Java中的多态理念显然是违背的,因此,我们需要一个在逻辑上可以用来表示同时是Boxc0f559cc8d56b43654fcbe4aa9df7b4a和Boxc8f01a3f8889dcf657849dd45bc0fc4c的父类的一个引用类型,由此,类型通配符应运而生。

类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box6b3d0130bba23ae47fe2b8e8cddf0195在逻辑上是Boxc0f559cc8d56b43654fcbe4aa9df7b4a、Boxc8f01a3f8889dcf657849dd45bc0fc4c...等所有Boxf4669da5dd7ec4cbc793dba728d85fa1的父类。由此,我们依然可以定义泛型方法,来完成此类需求。

public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);
        Box<Number> number = new Box<Number>(314);

        getData(name);
        getData(age);
        getData(number);
    }

    public static void getData(Box<?> data) {
        System.out.println("data :" + data.getData());
    }

}

有时候,我们还可能听到类型通配符上限和类型通配符下限。具体有是怎么样的呢?

在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限。

public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);
        Box<Number> number = new Box<Number>(314);

        getData(name);
        getData(age);
        getData(number);
        
        //getUpperNumberData(name); // 1
        getUpperNumberData(age);    // 2
        getUpperNumberData(number); // 3
    }

    public static void getData(Box<?> data) {
        System.out.println("data :" + data.getData());
    }
    
    public static void getUpperNumberData(Box<? extends Number> data){
        System.out.println("data :" + data.getData());
    }

}

此时,显然,在代码//1处调用将出现错误提示,而//2 //3处调用正常。

类型通配符上限通过形如Boxa2b037db85f4e1df0e812b9647ac55a8形式定义,相对应的,类型通配符下限为Boxda50108ad159903fabe211f1543600e8形式,其含义与类型通配符上限正好相反,在此不作过多阐述了。

更多java知识请关注java基础教程栏目。

Das obige ist der detaillierte Inhalt vonDetaillierte Einführung in Java-Generika. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:cnblogs.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen