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 List
In Kombination mit der obigen generischen Definition wissen wir, dass String in List
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
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
接着上面的结论,我们知道,Box
为了弄清这个问题,我们继续看下下面这个例子:
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(Box
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处肯定会出现错误提示的。在此我们可以使用反证法来进行说明。
假设Box
好,那我们回过头来继续看“类型通配符”中的第一个例子,我们知道其具体的错误提示的深层次原因了。那么如何解决呢?总部能再定义一个新的函数吧。这和Java中的多态理念显然是违背的,因此,我们需要一个在逻辑上可以用来表示同时是Box
类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box>在逻辑上是Box
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处调用正常。
类型通配符上限通过形如Box extends Number>形式定义,相对应的,类型通配符下限为Box super Number>形式,其含义与类型通配符上限正好相反,在此不作过多阐述了。
五.话外篇
本文中的例子主要是为了阐述泛型中的一些思想而简单举出的,并不一定有着实际的可用性。另外,一提到泛型,相信大家用到最多的就是在集合中,其实,在实际的编程过程中,自己可以使用泛型去简化开发,且能很好的保证代码质量。并且还要注意的一点是,Java中没有所谓的泛型数组一说。
对于泛型,最主要的还是需要理解其背后的思想和目的。
更多Java泛型相关文章请关注PHP中文网!