Heim  >  Artikel  >  Java  >  Java-Generika

Java-Generika

高洛峰
高洛峰Original
2016-12-19 14:54:421231Durchsuche

1. Die Einführung des Generika-Konzepts (warum werden Generika benötigt)?

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 List, fügt ihr zunächst zwei String-Typ-Werte hinzu und fügt dann einen A-Wert hinzu vom Typ Integer. Dies ist völlig zulässig, da der Standardtyp der Liste „Objekt“ ist. In nachfolgenden Schleifen können leicht Fehler wie //1 auftreten, weil vergessen wurde, Werte vom Typ Integer vorher zur Liste hinzuzufügen, oder weil andere Codierungsgründe vorliegen. 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 das Bekannteste, dass es beim Definieren einer Methode formale Parameter gibt und die tatsächlichen Parameter dann übergeben werden, wenn die Methode aufgerufen wird. Wie versteht man also parametrisierte Typen? Wie der Name schon sagt, wird der Typ aus dem ursprünglichen spezifischen Typ parametrisiert, ähnlich wie die variablen Parameter in der Methode. Zu diesem Zeitpunkt wird der Typ auch in Form eines Parameters (der als Typparameter bezeichnet werden kann) definiert dann wird der spezifische Typ übergeben, wenn der Typ verwendet/aufgerufen wird (Typargument).

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 wird direkt eingeschränkt, dass die Listensammlung nur String enthalten kann Typ.-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 Verwendung der generischen Definition in der List-Schnittstelle das E in 1a4db2c2c2313771e5742b6debf617a1 den Typparameter darstellt, der kann Empfangen Sie tatsächliche Parameter eines bestimmten Typs. In dieser Schnittstellendefinition bedeutet E überall dort, wo E erscheint, dass es sich um tatsächliche Parameter desselben Typs handelt, die von außen empfangen wurden.

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

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);
    }
    
    //...省略掉其他具体的定义过程

}

Daher verstehen wir aus Quellcode-Sicht, warum das Objekt vom Typ Integer ist wird bei //1 hinzugefügt. Kompilierungsfehler, und der von get() bei //2 erhaltene Typ ist direkt der String-Typ.

3. Angepasste generische Schnittstellen, generische Klassen und generische Methoden

Aus dem oben genannten 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- und K-Parameter im Formular of , V usw. werden häufig zur Darstellung generischer Parameter verwendet, da sie Typargumente erhalten, 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, die die ursprünglichste ist Typ (Box in diesem Beispiel). Natürlich können wir ihn logischerweise als mehrere verschiedene generische Typen verstehen.

Der Zweck des Konzepts der Generika in Java besteht darin, 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, d. h. die erfolgreich kompilierte Klassendatei enthält keine generischen Informationen. Allgemeine Informationen gelangen nicht in die Laufzeitphase.

Dies lässt sich in einem Satz zusammenfassen: Generische Typen können logischerweise als mehrere verschiedene Typen betrachtet werden, aber tatsächlich sind sie alle der gleiche Grundtyp.

4. Geben Sie Platzhalter ein

接着上面的结论,我们知道,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泛型相关文章请关注PHP中文网!

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