1)基本概念:
泛型(Generic Type或Generics)是 對Java語言的類型系統的一種擴展,以支持創建可以按類型進行參數化的類。可以把類型參數看做是使用參數化類型時指定的類型的一個佔位符,就像方法的形 式參數是運行時傳遞的佔位符一樣,泛型的體現主要是在集合框架裏面可以看到,JCF裏面應該是1.5裏面使用泛型最多的地方。Java語言引入泛型是一個 較大的功能增強,不僅語言、類型系統和編譯器有了大變化,以支持泛型,而且類庫也進行了大翻修,所以許多重要的類,比如集合框架,都已經成爲了泛型化的 了,使用泛型的優點爲:
* 類型安全:
泛 型的主要目標是提高Java程序的類型安全。通過知道使用泛型定義的變量的類型限制,編譯器可以在一個高得多的程度上驗證類型假設。沒有泛型,這些假設就 只存在於程序員的腦海中。Java程序中的一種流行技術是定義這樣的集合,即它的元素或鍵是功能類型的,比如“_列表”。通過在變量聲明中捕獲這一附加的 類型信息,泛型允許編譯器實施這些附加的約束,類型錯誤現在就可以在編譯時被捕獲了,而不是在運行時纔來進行檢測操作。
* 消除強制類型轉換:
泛型的一個附帶的好處是,消除源代碼中的許多強制類型轉換,這使得代碼更加可讀,而且減少了出錯的機會。比較兩段代碼:
不使用泛型的代碼段:
List li = new ArrayList();
li.add(new Integer(3));
Integer i = (Integer)li.get(0);
使用泛型:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
Integer i = li.get(0);
* 潛在的性能收穫:
泛型爲較大的優化帶來可能。在泛型的初始實現中,編譯器將類型轉換插入到各種字節碼中。但是更多類型信息可用於編譯器這一事實,爲未來版本的JVM也帶來了優化可能。
泛型本質上是提供類型的“類型參數【外露參數】”,它們也被稱爲參數化類型(Parameterized Type)或參量多態(Parametric Polymorphism), 用GJ(Generic Java)編寫的程序看起來和普通的Java基本相同,只不過多了一些參數化的類型同時少了一些類型轉換。實際上,GJ程序也是首先被轉化成一般的不帶泛 型的Java程序後再進行處理的,編譯器自動完成從GenericJava到普通Java的翻譯,具體轉化過程分爲以下幾個階段:
* 將參數化類型中的類型參數“擦除”(erasure)掉;
* 將類型變量用“上限(Upper bound)”取代,通常情況下這些上限是Object。這裏的類型變量是指實例域,本地方法域,方法參數以方法返回值用來標記類型信息的“變量”。
* 添加類型轉換並插入“橋方法”(bridge method),以覆蓋可以正常的工作
轉化後的程序和沒有引入的程序程序員不得不手工完成轉換的程序是非常一致的,下邊針對GJ的特點做一個簡要的總結:
* 類型安全:泛型的一個主要目標是提高Java的類型安全,使用泛型可以使編譯器知道變量的類型限制,進而可以在更高程度上驗證類型假設。如果沒有泛型,那麼類型的安全性主要由程序員來保證,這顯然不如帶有泛型的安全性高。
* 消除強制類型轉換:泛型可以消除源代碼中的許多強制類型轉換,這樣使得代碼更加可讀,並且減少出錯的機會
* 向後兼容:支持泛型的Java編譯器可以用來編譯經過泛型擴充的Java程序(GJ程序),但是現有的沒有使用泛型擴充的Java程序仍然可以用這些編譯器來編譯
* 層次清晰,比較規範:無論被編譯的源程序是否使用泛型擴充,編譯生成的字節碼均可以被虛擬機接受執行。就是說不論編譯器輸入的GJ程序還是一般的Java程序,經過編譯後的字節碼都遵循了Java虛擬機規範裏面針對字節碼的要求,也可以說:泛型對Java虛擬機是透明的
* 性能收益:目前來講GJ編寫的代碼和一般的Java代碼在效率上是很接近的,但是泛型可以進一步優化
API中的類型定義:
K——鍵、比如映射的鍵
V——值,比如_和_的內容,或者 _中的值
E——異常類型
T——泛型
——[1]簡單的泛型例子——
package org.susan.java.enumeration;
class GenClass<T>{
T ob;
GenClass(T o){
this.ob = o;
}
T getOb(){
return this.ob;
}
void showType(){
System.out.println("Type of T is " + ob.getClass().getName());
}
}
public class GenDemo {
public static void main(String args[]){
GenClass<Integer> iObject = new GenClass<Integer>(88);
iObject.showType();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
這段代碼的輸出爲:
Type of T is java.lang.Integer
【*:上邊定義了一個帶泛型的類,在使用的時候T可以替換爲所有我們需要的類型,這種定義方式類似C++裏面的模板定義。】
2)深入理解泛型:
[1]數據類型轉換:
在前邊已經說過了,Java裏面存在類的向下轉型和向上轉型,而且Java語言裏面有時候比較容易因爲類型的檢查引發轉型的問題,因爲很多時候需要不斷地向下轉型,這種方式往往增加了JVM的一部分運行時的開銷。實際上程序中每個向下轉型針對ClassCastException都是潛在的危險, 應當儘量避免它們,但是在寫程序的過程,這種轉型往往沒有辦法避免,即使特別優良的設計也是會存在的。其實JDK 1.4到JDK 1.5的升級過程泛型是一個大的跨度,這種跨度使得編寫程序更加規範,其實在前邊講解集合的時候已經使用了很多泛型的編程格式了,提供的很多代碼Demo 都是泛型的。這裏再提供一段簡單的泛型使用代碼:
——[$]使用泛型的 List——
package org.susan.java.generic;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
*泛型List的使用,結合了JDK 1.4和JDK 1.5的兩種用法,但是在JDK 1.4編譯器中下邊的方式編譯不通過
**/
public class ListGenericDemo {
public static void main(String args[]){
List list = new ArrayList();
List<String> genList = new ArrayList<String>();
list.add("List 1");
list.add("List 2");
genList.add("Generic List 1");
genList.add("Generic List 2");
System.out.println("No Generic:");
Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.print("[" + (String)iterator.next() + "],");
}
System.out.println("\nGeneric:");
Iterator<String> genIterator = genList.iterator();
while(genIterator.hasNext()){
System.out.print("[" + genIterator.next() + "],");
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
上邊這段代碼的輸出爲:
No Generic:
[List 1],[List 2],
Generic:
[Generic List 1],[Generic List 2],
這裏使用了兩種不同的方式【*: 在最原始的定義的List參數裏面,不使用泛型的時候都是直接使用Object類型,在傳入String的時候會進行向下轉型,一般情況不會出現轉型失敗 的情況,但是使用了泛型過後,就上邊的genList代碼段裏面,只能添加String對象,如果使用了其他類型的元素這裏編譯器就會直接報錯,這種做法消除了JVM本身的類型轉換。】
[2]基本類型限制
Tiger【Java 5.0的版本號】中 的類型變量的限制之一就是:必須使用引用類型進行實例化,基本類型不起作用,就是說不能使用:List list = new ArrayList();這種定義方式。也就是說在泛型使用的過程裏面,如果要針對基本類型進行泛型使用,必須要進行包裝,就是 Boxing操作,比如把int包裝成爲Integer。
這裏參考以下泛型的類型限制:JSR-14中:
* 不應在靜態成員中引用封閉類型參數
* 不能用基本類型實例化泛型類型參數
* 不能在數據類型轉換或instanceof操作中使用“外露”類型參數
* 不能在new操作符中使用“外露”的類型參數
* 不能在類定義的implements或extends字句中使用 “外露”類型參數
這裏簡單透露一點JVM的原理:JVM 本身不支持泛型,在編譯器進行泛型代碼的編譯的時候,其實是使用了“擦除”功能,就是JVM在編譯帶泛型的代碼的時候,實際上對帶泛型的代碼進行了類型檢 查,然後“擦除”泛型代碼中的類型支持,轉換爲普通類型進行編譯。這裏有一個新概念成爲“外露”類型——單獨出現而不是位於某個類型中的類型參數如 (List<T>中的T)針對T類型而言,T的上界就是Object。這一項技術的功能極其強大,我們可以使幾乎所有泛型類型的精度增強,但 是與JVM兼容。
——[$]靜態成員中的封閉類型參數——
package org.susan.java.generic;
public class StaticListGenericDemo<T> {
static void metho1(){
//T t;
}
static class StaticClass{
//StaticListGenericDemo<T> t;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
這裏被註釋掉的代碼是不能通過JVM編譯的,因爲編譯器完全禁止在靜態方法和靜態內部類中引用封閉類型參數,下邊幾種情況這裏就不做解釋了,T在整個過程裏面是不應該作爲外露類型來使用。
——[$]提供一個 instanceof操作中的“外露參數”——
package org.susan.java.generic;
import java.util.Hashtable;
interface Registry{
public void register(Object o);
}
class C<T> implements Registry{
int counter = 0;
Hashtable<Integer,T> values;
public C(){
values = new Hashtable<Integer,T>();
}
public void register(Object o){
values.put(new Integer(counter), (T)o);
counter++;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
【*:這段代碼編譯沒有任何問題,但是在(T)o地方會有一個警告,雖然這些警告本身沒有什麼,事實上,它們會使得診斷代碼變得極爲困難。在以前的代碼中,我們認爲如果對實例 C<JFrame> 調用 register("test") ,會發出 ClassCastException 。但並非如此;計算將繼續,就彷彿數據類型轉換成功了一樣,然後在進一步進行計算時發出錯誤,或者更糟:用遭破壞的數據完成計算,但不向外發出任何錯誤信號。同樣,對“外露”類型參數的 instanceof 檢查將在編譯時產生 “unchecked”警告,而且檢查將不會如期在運行時進行。】
[3]泛型的構造函數:
在定義泛型的構造函數的時候,要解決這一個問題,需要一定的操作:
* 要求類型參數的所有實例化都包括不帶參數的構造函數
* 只要泛型類的運行時實例化沒有包括所需要的構造函數,就拋異常
* 修改語言的語法以包括更加詳細的類型參數界限
——[$]定義一個帶泛型構造函數——
package org.susan.java.generic;
class GenGons{
private double val;
<T extends Number> GenGons(T arg){
val = arg.doubleValue();
}
void showVal(){
System.out.println("Val: " + val);
}
}
public class GenGonsDemo {
public static void main(String args[]){
GenGons genOne = new GenGons(100);
GenGons genTwo = new GenGons(123.5F);
genOne.showVal();
genTwo.showVal();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
這段程序輸出爲:
Val: 100.0
Val: 123.5
[4]泛型中通配符的使用:
通配符——使 用一個?標識類型參數,是一種表示未知類型的約束方法。通配符並不包含在最初的泛型設計中,從形成JSR14到發佈其最終版本之間的五年時間內完成了設計 添加到泛型中。通配符在泛型的使用中具有重要的意義,它們爲一個泛型類所指定的類型集合提供了一個有用的類型範圍。對泛型的ArrayList而言,對於 任意類型T,ArrayList<?>類型是ArrayList<T>的超類型,但是這些超類型在執行類型推斷方面是不起作用的。通配符類型List<?>、原始List和具體List<Object>都不相同。如果說變量X具有List<?>類型,標識存在一些T類型,其中x是List<T>類型,x具有相同的結構,儘管我們不知道其元素的具體類型。這並不代表它具有任意內容,而是指我們並不瞭解內容的類型限制是什麼 — 但我們知道存在 某種限制。另一方面,原始類型 List 是異構的,我們不能對其元素有任何類型限制,具體類型 List<Object> 表示我們明確地知道它能包含任何對象(當然,泛型的類型系統沒有 “列表內容” 的概念,但可以從 List 之類的集合類型輕鬆地理解泛型)。 通配符在類型系統中的作用部分來自其不會發生協變(covariant)這一特性。數組是協變的,因爲 Integer 是 Number 的子類型,數組類型 Integer[] 是 Number[] 的子類型,因此在任何需要 Number[] 值的地方都可以提供一個 Integer[] 值。另一方面,泛型不是協變的, List<Integer> 不是 List<Number> 的子類型,試圖在要求 List<Number> 的位置提供 List<Integer> 是一個類型錯誤。這不算很嚴重的問題,也不是所有人都認爲的錯誤,但泛型和數組的不同行爲的確引起了許多混亂。
——[$]通配符的使用 ——
package org.susan.java.generic;
class Status<T extends Number>{
T[] nums;
Status(T[] o){
nums = o;
}
double average(){
double sum = 0.0;
for( int i = 0; i < nums.length; i++)
sum += nums[i].doubleValue();
return sum / nums.length;
}
boolean sameAvg(Status<?> obj){
if( average() == obj.average())
return true;
return false;
}
}
public class WildcardDemo {
public static void main(String args[]){
Integer inums[] = {1,2,3,4,5};
Status<Integer> iobj = new Status<Integer>(inums);
System.out.println("iob average is "+ iobj.average());
Double dnums[] = {1.1,2.2,3.3,4.4,5.5};
Status<Double> dobj = new Status<Double>(dnums);
System.out.println("dob average is "+ dobj.average());
Float fnums[] = {1.1F,2.2F,3.3F,4.4F,5.5F};
Status<Float> fobj = new Status<Float>(fnums);
System.out.println("fob average is "+ fobj.average());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
這段程序的輸出爲:
iob average is 3.0
dob average is 3.3
fob average is 3.300000023841858
——[$]返回泛型值的方法——
package org.susan.java.generic;
import java.io.Serializable;
class Base{}
class SubClass extends Base implements Serializable{}
class SubClassTwo extends Base implements Serializable{}
public class TypeInference {
public static <T extends Base> T Method(T t1,T t2){
return null;
}
public static void main(String args[]){
Base base = Method(new SubClass(), new SubClassTwo());
Serializable run = Method(new SubClass(), new SubClassTwo());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
注意上邊返回值的寫法
——[$]?通配符——
package org.susan.java.generic;
import java.util.ArrayList;
import java.util.List;
/**
*問號通配符
**/
public class QuestionDemo {
private static void testMethod(List<? extends Number> list){}
public static void main(String args[]){
List<Object> oList = new ArrayList<Object>();
List<Integer> iList = new ArrayList<Integer>();
List<Number> nList = new ArrayList<Number>();
//testMethod(oList); //這裏會出現編譯錯誤
testMethod(iList);
testMethod(nList);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
從上邊的的代碼可以知道,?一般和extends以及super關鍵字進行使用,其含義在於傳入泛型的類型定義爲extends後邊的類型的子類,所以上邊的註釋掉的代碼是沒有辦法通過編譯的。
【*:泛型真正在開發過程程序員需要掌握的是用法,上邊講到的四點都比較深入,而且都是討論的與JVM處理泛型原理相關的內容,如果沒有弄懂沒有關係,下邊講“泛型類型捕獲”的時候主要提及應用層的相關內容。】
——[$]定義一個泛型接口——
package org.susan.java.generic;
interface MinMan<T extends Comparable<T>>{
T min();
T max();
}
class MyDemo<T extends Comparable<T>> implements MinMan<T>{
T[] tValues;
MyDemo(T[] o){ tValues = o;}
public T min(){
T vT = tValues[0];
for( int i = 1; i < tValues.length; i++ )
if(tValues[i].compareTo(vT) < 0)
vT = tValues[i];
return vT;
}
public T max(){
T vT = tValues[0];
for( int i = 1; i < tValues.length; i++ )
if(tValues[i].compareTo(vT) > 0)
vT = tValues[i];
return vT;
}
}
public class GenericInterface {
public static void main(String args[]){
Integer inums[] = {3,6,13,11,45,22,33,21};
MyDemo<Integer> iob = new MyDemo<Integer>(inums);
System.out.println("iob Max:" + iob.max());
System.out.println("iob Min:" + iob.min());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
該輸出爲:
iob Max:45
iob Min:3
——[$]泛型的方法重載——
package org.susan.java.generic;
/**
*一段錯誤的代碼段
**/
class MyGenDemo<T,V>{
T ob1;
V ob2;
void set(T o){
this.ob1 = o;
}
void set(V o){
this.ob2 = o;
}
}
class GenDemo<T>{
T ob;
GenDemo(){
this.ob = new T();
}
}
class Wrong<T>{
static T ob;
static T getOb(){
return ob;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
分析上邊的代碼段就可以發現很多問題:
* 首先set方法並不能通過這種方式重載,原因很簡單,雖然這裏使用了通配符T、V,但是這兩個“外露” 類型在編譯器裏面爲默認爲相同的,因爲這種情況下兩個都屬於佔位符類型,按照這樣的方式就會使得set方法的方法簽名對JVM而言是一模一樣的
* this.ob = new T() 有問題,因爲通配符是不能通過這種方式構造的,通配符目前還不能成爲Java類型,所以通配符目前是不能實例化的,哪怕是一個類類型。
* 最後一段代碼問題也很嚴重:因爲通配符屬於非靜態類型,所以不能使用在static的方法或者類定義裏面,這就是上邊代碼段的代碼。
——[$]泛型Java 類——
package org.susan.java.generic;
class GenType<T>{
T ob;
GenType(T ob){
this.ob = ob;
}
T getOb(){
return this.ob;
}
}
class GenStr<T extends Number>{
T str;
GenStr(T o){
this.str = o;
}
T getStr(){
return this.str;
}
}
public class GenTypeDemo {
public static void main(String args[]){
GenType<Integer> iObGenType = new GenType<Integer>(99);
GenStr<Float> fOb = new GenStr<Float>(102.2F);
System.out.println(iObGenType.getClass().getName());
System.out.println(fOb.getClass().getName());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
根據輸出結果可以知道該類型就爲我們定義的類型:
org.susan.java.generic.GenType
org.susan.java.generic.GenStr
3)泛型“類型捕獲”
協變概念【extends 和super】
講了這麼多泛型的內容,相信讀者對泛型有一點點了解了,接下來針對泛型裏面比較費解的地方進行比較通俗的講解,泛型的類型捕獲是從編譯器的級別來說的,當我們定義了泛型的時候,如果使用了? extends T這 種格式,編譯器遇到一個這樣帶有通配符的變量的時候,它如何來進行識別操作呢,它會覺得遇到T了過後,一定會有T定義的變量,而對這些T而言一定有一個 Class<T>類型。但是它不知道T代表什麼,但是JVM會爲T定製一個佔位符來代替T的類型。佔位符被稱爲特殊通配符的捕獲。這種情況 下,編譯器會爲通配符提供一個名字,每個變量聲明中出現的一個通配符都會活得JVM的一個捕獲,,因此在泛型聲明中如果用了public void method(Pointer<?,?> pointer)的話,JVM就會獲取兩個通配符名稱,因爲這個時候?也好,T也好,類型是未知的,任意未知的類型的參數在使用的時候相互之間是沒有任何關係的。
【*:泛型的通配符的使用其實在JDK 1.5無疑是最複雜的部分,而且Java編譯器產生的一些令人困惑的錯誤以及很多消息都可能和通配符有關。】
? extends Number:這種語法的意思就是傳入的泛型的“外露”類型必須是Number的子類,這裏重複一下“外露類型”,其實在遇到泛型的時候往往會遇到兩個類型,直接定義的類型,一般爲Type<T>格式的,然後是外露類型T,這裏T就代表了外露類型,這裏需要區分這種情況下Type<T>和Type不屬於同一個類型。這裏需要了解的是泛型的“協變”。
注意:泛型本身不是協變的
比 如有類型Integer和Number,因爲Integer類是Number類的子類,也就是說Integer是一個Number,Integer[]也 是一個Number[],按照這樣的邏輯,我們也許會覺得泛型也是遵循了這樣原理List<Integer>也會是 List<Number>的子類,但是這種做法在泛型裏面是錯誤的。這種做法在傳入的時候會被編譯器定義爲類型錯誤。看一段程序:
——[$]協變的概念代碼——
package org.susan.java.generic;
import java.util.ArrayList;
import java.util.List;
/**
*關於泛型協變的概念代碼
**/
public class ChangeGenerice {
private static void methodOne(List<Number> number){
System.out.println("One:" + number);
}
private static void methodTwo(List<Integer> integer){
System.out.println("Two:" + integer);
}
private static void methodThree(List<? extends Number> number){
System.out.println("Three:" + number);
}
private static void methodFour(List<? super Number> integer){
System.out.println("Four:" + integer);
}
public static void main(String args[]){
List<Number> nList = new ArrayList<Number>();
List<Integer> iList = new ArrayList<Integer>();
List<Object> oList = new ArrayList<Object>();
methodOne(nList);
//這裏編譯報錯,因爲nList是List<Number>,但是方法接受參數是 List<Integer>,它們不存在繼承關係,這裏也證明了協變的簡單
//methodTwo(nList);
methodThree(nList);
methodFour(nList);
//這裏編譯報錯,iList是List<Integer>,不是 List<Number>
//methodOne(iList);
methodTwo(iList);
methodThree(iList);
//這裏編譯報錯,iList不滿足條件List<? super Number>,因爲List<Integer>中外露類型Integer不滿足Integer super Number
//methodFour(iList);
//最後三個編譯錯誤留給讀者自己去分析
//methodOne(oList);
//methodTwo(oList);
//methodThree(oList);
methodFour(oList);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
如果需要在1.5的JDK環境裏面進行1.4的寫法,如果不去掉類型檢測可能會報警告:
【*:上邊這段概念代碼很好說明了協變過程,同樣也能夠很好理解泛型裏面的extends和super關鍵字的使用了。關於通配符的使用可能還需要寫更多的代碼,最好辦法是直接去參考集合部分的源代碼,是一個不錯的學習途徑。】
針對Java泛型做一個簡單的總結:
* 泛型的“外露”類型可以用在類、接口和方法中,一般稱爲泛型類、泛型接口、泛型方法
* 泛型的“外露”類型只可以是類類型(包括自定義類),不能是簡單類型
* 泛型的“外露”類型可以使用extends和super關鍵字,該關鍵字標識了“外露”類型的界限
* 泛型不支持直接的類型協變,在協變過程注意泛型的“外露”類型滿足的界限的條件
* 泛型還可以使用通配符進行操作,這種情況可以用來作爲類模版
- ').addClass('pre-numbering').hide();