摘要:唯一的例外是,它是所有引用類型的實例另一方面,程序可以調用方法來返回集合指定索引處的元素,其返回值是一個未知類型,但可以肯定的是,它總是一個。
泛型入門 編譯時不檢查類型的異常
public class ListErr { public static void main(String[] args) { // 創建一個只想保存字符串的List集合 List strList = new ArrayList(); strList.add("布達佩斯"); strList.add("布拉格"); // "不小心"把一個Integer對象"丟進"了集合 strList.add(34); // ① strList.forEach(str -> System.out.println(((String)str).length())); // ② } }
上述程序創建了一個List集合,且該List集合保存字符串對象——但程序不能進行任何限制,如果程序在①處“不小心”把一個Integer對象"丟進"了List集合,這將導致程序在②處引發ClassCastException異常,因為程序試圖把一個Integer對象轉換為String類型
使用泛型參數化類型,允許程序在創建集合時指定集合元素的類型。Java的參數化類型被稱為泛型(Generic)
class GenericList { public static void main(String[] args) { // 創建一個只想保存字符串的List集合 ListstrList = new ArrayList<>(); // ① strList.add("布達佩斯"); strList.add("布拉格"); // 下面代碼將引起編譯錯誤 strList.add(34); // ② strList.forEach(str -> System.out.println(((String)str).length())); // ③ } }
strList集合只能保存字符串對象,不能保存其他類型的對象。創建特殊集合的方法是:在集合接口、類后增加尖括號,尖括號里放一個數據類型,即表明這個集合接口、集合類只能保存特定類型的對象
①類型聲明,在創建這個ArrayList對象時也指定了一個類型參數;②引發編譯異常;③不需要進行強制類型轉換
泛型使程序更加簡潔,集合自動記住所有集合元素的數據類型,從而無須對集合元素進行強制類型轉換
Java7泛型的“菱形”語法Java允許在構造器后不需要帶完整的泛型信息,只要給出一對尖括號(<>)即可,Java可以推斷尖括號里應該是什么泛型信息
public class DiamondTest { public static void main(String[] args) { // Java自動推斷出ArrayList的<>里應該是String List深入泛型countries = new ArrayList<>(); countries.add("法蘭西第五共和國"); countries.add("西班牙王國"); // 遍歷countries集合,集合元素就是String類型 countries.forEach(ele -> System.out.println(ele.length())); // Java自動推斷出HashMap的<>里應該是String , List Map > citiesInfo = new HashMap<>(); // Java自動推斷出ArrayList的<>里應該是String List cities = new ArrayList<>(); cities.add("巴黎"); cities.add("巴塞羅那"); citiesInfo.put("Bienvenue" , cities); // 遍歷Map時,Map的key是String類型,value是List 類型 citiesInfo.forEach((key , value) -> System.out.println(key + "-->" + value)); } }
所謂泛型:就是允許在定義類、接口、方法時指定類型形參,這個類型形參將在聲明變量、創建對象、調用方法時動態地指定(即傳入實際的類型參數,也可稱為類型實參)
定義泛型接口、類List接口、Iterator接口、Map的代碼片段
// 定義接口時指定了一個類型形參,該形參名為E public interface List{ // 在該接口里,E可作為類型使用 // 下面方法可以使用E作為參數類型 void add(E x); Iterator iterator(); // ① } // 定義接口時指定了一個類型形參,該形參為E public interface Iterator { // 在該接口里E完全可以作為類型使用 E next(); boolean hasNext(); } // 定義該接口時指定了兩個類型形參,其形參名為K、V public interface Map { // 在該接口里K、V完全可以作為類型使用 Set keySet(); // ② V put(K key,V value); }
上面代碼說明泛型實質:允許在定義接口、類時聲明類型形參,類型形參在整個接口、類體內可當成類型使用
包含泛型聲明的類型可以在定義變量、創建對象時傳入一個類型實參,從而可以動態地生成無數個邏輯上的子類,但這種子類在物理上并不存在
可以為任何類、接口增加泛型聲明(并不是只有集合類才可以使用泛型聲明,雖然集合類是泛型的重要使用場所)
// 定義Onmyoji類時使用了泛型聲明 public class Onmyoji{ // 使用T類型形參定義實例變量 private T info; public Onmyoji(){} // 下面方法使用T類型形參來定義構造器 public Onmyoji(T info) { this.info = info; } public T getInfo() { return info; } public void setInfo(T info) { this.info = info; } public static void main(String[] args) { //由于傳給T形參的是String,所以構造器參數只能是String Onmyoji a1 = new Onmyoji<>("安倍晴明"); System.out.println(a1.getInfo()); // 由于傳給T形參的是Double,所以構造器參數只能是Double或double Onmyoji a2 = new Onmyoji<>(520.1314); System.out.println(a2.getInfo()); } }
當創建帶泛型聲明的自定義類,為該類定義構造器時,構造器名還是原來的類名,不要增加泛型聲明
從泛型類派生子類當創建了帶泛型聲明的接口、父類之后,可以為該接口創建實現類,或從父類派生子類。當使用這些接口、父類時不能再包含類型形參
// 定義類Shikigami繼承Onmyoji類,Onmyoji類不能跟類型形參 public class Shikigami extends Onmyoji{ } // 錯誤
方法中的形參(或數據形參)代表變量、常量、表達式等數據。定義方法時,可以聲明數據形參;調用方法(使用方法)時,必須為這些數據形參傳入實際的數據;與此類似的是,定義類、接口、方法時可以聲明類型形參,使用類、接口、方法時應為類型形參傳入實際的類型
// 使用Onmyoji類時為T形參傳入String類型 public class Shikigami extends Onmyoji{ } // 正確
調用方法時必須為所有的數據參數傳入參數值,而使用類、接口時也可以不為類型形參傳入實際的類型參數
// 使用Onmyoji類時,沒有為T形參傳入實際的類型參數 public class Shikigami extends Onmyoji{ } // 正確
子類需要重寫父類的Getters和Setters方法
private String info; public String getInfo() { return "子類"+ super.getInfo(); } public void setInfo(String info) { this.info = info; }并不存在泛型類
ArrayList
// 分別創建List對象和List 對象 List l1 = new ArrayList<>(); List l2 = new ArrayList<>(); // 調用getClass()方法來比較l1和l2的類是否相等 System.out.println(l1.getClass() == l2.getClass()); // 輸出true
不管為泛型的類型形參傳入哪一種類型實參,對于Java來說,它們依然被當成同一個類處理,在內存中也只占用一塊內存空間,因此在靜態方法、靜態初始化塊或者靜態變量的聲明和初始化中不允許使用類型形參
public class R{ static T info; // 代碼錯誤,不能在靜態變量聲明中使用類型形參 T age; public void foo(T msg){} public static void bar(T msg){} // 代碼錯誤,不能在靜態方法聲明中使用類型形參 }
由于系統中并不會真正生成泛型類,所以instanceof運算符后不能使用泛型類。
if(cs instanceof List類型通配符) { ... }
如果Foo是Bar的一個子類型(子類或者子接口),而G是具有泛型聲明的類或接口,G
數組和泛型有所不同,假設Foo是Bar的一個子類型(子類或者子接口),那么Foo[]依然是Bar[]的子類型;但G
Java泛型的設計原則是,只要代碼在編譯時沒有出現警告,就不會遇到運行時ClassCastException異常
使用類型通配符為了表示各種泛型List的父類,可以使用類型通配符,類型通配符是一個問號(?),將一個問號作為類型實參傳給List集合,寫作:List>(意思是未知類型元素的List)。這個問號(?)被稱為通配符,它的元素類型可以匹配任何類型
public void test(List> c) { for(int i = 0; i < c.size(); i++) { System.out.println(c.get(i)); } }
現在使用任何類型的List來調用它,程序依然可以訪問集合c中的元素,其類型是Object,這永遠是安全的,因為不管List的真實類型是什么,它包含的都是 Object
這種帶通配符的List僅表示它是各種泛型List的父類,并不能把元素加入到其中
List> c = new ArrayList(); // 下面程序引起編譯錯誤 c.add(new Object());
因為程序無法確認c集合里元素的類型,所以不能向其中添加對象。根據前面的List
另一方面,程序可以調用get()方法來返回List>集合指定索引處的元素,其返回值是一個未知類型,但可以肯定的是,它總是一個Object。因此,把get()的返回值賦值給一個Object類型的變量,或者放在任何希望是Object類型的地方都可以
設定類型通配符的上限List
List extends Shape>是受限通配符的例子,此處的問號(?)代表一個未知的類型,此處的未知類型一定是Shape的子類也可以是Shape,因此可以把shape稱為這個通配符的上限(upper bound)
設定類型形參的上限Java泛型不僅允許在使用通配符形參時設定上限,而且可以在定義類型形參時設定上限,用于表示傳給該類型形參的實際類型要么是該上限類型,要門是該上限類型的子類
在一種更極端的情況下,程序需要為類型形參設定多個上限(至少有一個父類上限,可以有多個接口上限),表明該類型形參必須是其父類的子類(其父類本事也行),并且實現多個上限接口
//表明T類型必須是Number類或其子類,并必須實現java.io.Serializablepublic class Apple{ ... }
與類同時繼承父類、實現接口類似的是:為類型形參指定多個上限,所有的接口上限必須位于類上限之后。也就是說,如果需要為類型形參指定類上限,類上限必須位于第一位
泛型方法 定義泛型方法Java不允許把對象放進一個未知類型的集合中
所謂泛型方法,就是在聲明方法時定義一個或多個類型形參
修飾符返回值類型 方法名(形參列表) { // 方法體... }
把上面方法的格式和普通方法的格式進行對比,不難發現泛型方法的方法簽名比普通方法的方法簽名多了類型形參聲明,類型形參聲明以尖括號括起來,多個類型形參之間以逗號隔開,所有的類型形參聲明放在方法修飾符和方法返回值類型之間
public class GenericMethodTest { // 聲明一個泛型方法,該泛型方法中帶一個T類型形參, staticvoid fromArrayToCollection(T[] a, Collection c) { for (T o : a) { c.add(o); } } public static void main(String[] args) { Object[] oa = new Object[100]; Collection
為了讓編譯器能準確地推斷出泛型方法中類型形參的類型,不要制造迷惑!系統一旦迷惑了,就是你錯了!看如下程序
public class ErrorTest { // 聲明一個泛型方法,該泛型方法中帶一個T類型形參 staticvoid test(Collection from, Collection to) { for (T ele : from) { to.add(ele); } } public static void main(String[] args) { List
上面程序中定義了test方法,該方法用于將前一個集合里的元素復制到下一個集合中,該方法中的兩個形參 from、to 的類型都是 Collection
上面程序中調用test方法傳入了兩個實際參數,其中as的數據類型是List
public class RightTest { // 聲明一個泛型方法,該泛型方法中帶一個T形參 staticvoid test(Collection extends T> from , Collection to) { for (T ele : from) { to.add(ele); } } public static void main(String[] args) { List
上面代碼改變了test方法簽名,將該方法的前一個形參類型改為 Collection extends T>,這種采用類型通配符的表示方式,只要test方法的前一個Collection集合里的元素類型是后一個Collection集合里元素類型的子類即可
泛型方法和類型通配符的區別大多數時候都可以使用泛型方法來代替類型通配符。例如,對于 Java 的 Collection 接口中兩個方法定義:
public interface Collection{ boolean containAll(Collection> c); boolean addAll(Collection extends E> c); ... }
上面集合中兩個方法的形參都采用了類型通配符的形式,也可以采用泛型方法的形式,如下所示
public interface Collection{ boolean containAll(Collection c); boolean addAll(Collection c); ... }
上面方法使用了
泛型方法允許類型形參被用來表示方法的一個或多個參數之間的類型依賴關系,或者方法返回值與參數之間的類型依賴關系。如果沒有這樣的類型依賴關系,就不應該使用泛型方法
如果某個方法中一個形參(a)的類型或返回值的類型依賴于另一個形參(b)的類型,則形參(b)的類型聲明不應該使用通配符----因為形參(a)或返回值的類型依賴于該形參(b)的類型,如果形參(b)的類型無法確定,程序就無法定義形參(a)的類型。在這種情況下,只能考慮使用在方法簽名中聲明類型形參 ——也就是泛型方法
也可以同時使用泛型方法和通配符,如Java的Collections.copy方法
public class Collections { public staticvoid copy(List dest, List extends T> src) { ... } ... }
上面copy方法中的dest和src存在明顯的依賴關系,從源List中復制出來的元素,必須可以“丟進”目標List中,所以源List集合元素的類型只能是目標集合元素的類型的子類型或者它本身。但JDK定義src形參類型時使用的是類型通配符,而不是泛型方法。這是因為:該方法無須向src集合中添加元素,也無須修改src集合里的元素,所以可以使用類型通配符,不使用泛型方法
類型通配符與泛型方法(在方法簽名中顯式聲明類型形參)還有一個顯著的區別:類型通配符既可以在方法簽名中定義形參的類型,也可以用于定義變量的類型;但泛型方法中的類型形參必須在對應方法中顯式聲明
Java7的“菱形”語法與泛型構造器Java允許在構造器簽名中聲明類型形參,這樣就產生了所渭的泛型構造器。一旦定義了泛型構造器,接下來在調用構造器時,就不僅可以讓Java根據數據參數的類型來“推斷”類型形參的類型,而且程序員也可以顯式地為構造器中的類型形參指定實際的類型
class Foo { publicFoo(T t) { System.out.println(t); } } public class GenericConstructor { public static void main(String[] args) { // 泛型構造器中的T參數為String。 new Foo("瘋狂Java講義"); // 泛型構造器中的T參數為Integer。 new Foo(200); // 顯式指定泛型構造器中的T參數為String, // 傳給Foo構造器的實參也是String對象,完全正確。 new Foo("瘋狂Android講義"); // 顯式指定泛型構造器中的T參數為String, // 但傳給Foo構造器的實參是Double對象,下面代碼出錯 new Foo(12.3); } }
前面介紹過 Java 7 新增的“菱形”語法,它允許調用構造器時在構造器后使用一對尖括號來代表泛型信息。但如果程序顯式指定了泛型構造器中聲明的類型形參的實際類型,則不可以使用“菱形”語法
class MyClass{ public MyClass(T t) { System.out.println("t參數的值為:" + t); } } public class GenericDiamondTest { public static void main(String[] args) { // MyClass類聲明中的E形參是String類型。 // 泛型構造器中聲明的T形參是Integer類型 MyClass mc1 = new MyClass<>(5); // 顯式指定泛型構造器中聲明的T形參是Integer類型, MyClass mc2 = new MyClass (5); // MyClass類聲明中的E形參是String類型。 // 如果顯式指定泛型構造器中聲明的T形參是Integer類型 // 此時就不能使用"菱形"語法,下面代碼是錯的。 // MyClass mc3 = new MyClass<>(5); } }
上面程序中最后一行代碼既指定了泛型構造器中的類型形參是 Integer 類型,又想使用“菱形”語法,所以這行代碼無法通過編譯
設定通配符下限實現將src集合里的元素復制到dest集合里的功能,因為dest集合可以保存src集合里的所有元素,所以dest集合元素的類型應該是src集合元素類型的父類,假設該方法需要一個返回值,返回最后一個被復制的元素。為了表示兩個參數之間的類型依賴,考慮同時使用通配符、泛型參數來實現該方法
public staticT copy(Collection dest, Collection extends T> src) { T last = null; for (T ele : src) { last = ele; dest.add(ele); } return last; }
實際上有一個問題:當遍歷src集合的元素時,src元素的類型是不確定的(但可以肯定它是T的子類),程序只能用T來籠統地表示各種src集合的元素類型
Listln = new ArrayList<>; List li = new ArrayList<>; // 下面代碼引起編譯錯誤 Integer last = copy(ln, li);
ln的類型是List
對于上面的copy方法,可以這樣理解兩個集合參數之間的依賴關系:不管src集合元素的類型是什么,只要dest集合元素的類型與前者相同或是前者的父類即可。Java允許設定通配符的下限: super Type>,這個通配符表示它必須是Type本身,或是Type的父類
public class MyUtils { // 下面dest集合元素類型必須與src集合元素類型相同,或是其父類 public staticT copy(Collection super T> dest , Collection src) { T last = null; for (T ele : src) { last = ele; dest.add(ele); } return last; } public static void main(String[] args) { List ln = new ArrayList<>(); List li = new ArrayList<>(); li.add(5); // 此處可準確的知道最后一個被復制的元素是Integer類型 // 與src集合元素的類型相同 Integer last = copy(ln , li); // ① System.out.println(ln); } }
使用這種語句,就可以保證程序的①處調用后推斷出晟后一個被復制的元素類型是 Integer,而不是籠統的 Number 類型
泛型方法與方法重載因為泛型既允許設定通配符的上限,也允許設定通配符的下限,從而允許在一個類里包含如下兩個方法定義
public class MyUtils { public staticvoid copy(Collection dest , Collection extends T> src) {...} // ① public static copy(Collection super T> dest, Collection src) {...} // ② }
MyUtils類中包含兩個copy方法,這兩個方法的參數列表存在一定的區別,但這種區別不是很明確:這兩個方法的兩個參數都是Collection對象,前一個集合里的集合元素類型是后一個集合里集合元素類型的父類。如果這個類僅包含這兩個方法不會有任何錯誤,但只要調用這個方法就會引起編譯錯誤
Listln = new ArrayList<>; List li = new ArrayList<>; copy(ln , li);
上面程序調用copy方法,但這個copy()方法既可以匹配①號copy方法,此時T類型參數的類型是 Number;也可以匹配②號copy()方法,此時T參數的類型是Integer。編譯器無法確定這行代碼想調用哪個copy()方法,所以這行代碼將引起編譯錯誤
Java8改進的類型推斷Java8改進了泛型方法的類型推斷能力,類型推斷主要有如下兩方面
可通過調用方法的上下文來推斷類型參數的目標類型
可在方法調用鏈中,將推斷得到的類型參數傳遞到最后一個方法
class MyUtil擦除和轉換{ public static MyUtil nil() { return null; } public static MyUtil cons(Z head, MyUtil tail) { return null; } E head() { return null; } } public class InferenceTest { public static void main(String[] args) { // 可以通過方法賦值的目標參數來推斷類型參數為String MyUtil ls = MyUtil.nil(); // 無需使用下面語句在調用nil()方法時指定類型參數的類型 MyUtil mu = MyUtil. nil(); // 可調用cons方法所需的參數類型來推斷類型參數為Integer MyUtil.cons(42, MyUtil.nil()); // 無需使用下面語句在調用nil()方法時指定類型參數的類型 MyUtil.cons(42, MyUtil. nil()); // 希望系統能推斷出調用nil()方法類型參數為String類型, // 但實際上Java 8依然推斷不出來,所以下面代碼報錯 // String s = MyUtil.nil().head(); String s = MyUtil. nil().head(); } }
在嚴格的泛型代碼里,帶泛型聲明的類總應該帶著泛型參數。但是為了和古老的java代碼保持一致,也就是說為了向下兼容,也允許在使用帶泛型聲明的類時不指定實際的類型參數。如果沒有為這個泛型指定實際的類型參數,則該類型參數被稱作raw type(原始類型),默認是聲明該類型參數時指定的第一個上限類型
當把一個具體泛型信息的對象賦值給另外一個沒有泛型信息的變量時,所有尖括號之間的類型信息都將被扔掉。比如說將一個List
class Apple{ T size; public Apple() { } public Apple(T size) { this.size = size; } public void setSize(T size) { this.size = size; } public T getSize() { return this.size; } } public class ErasureTest { public static void main(String[] args) { Apple a = new Apple<>(6); // ① // a的getSize()方法返回Integer對象 Integer as = a.getSize(); // 把a對象賦給Apple變量,丟失尖括號里的類型信息 Apple b = a; // ② // b只知道size的類型是Number Number size1 = b.getSize(); // 下面代碼引起編譯錯誤 Integer size2 = b.getSize(); // ③ } }
上面程序定義了一個帶泛型聲明的Apple類,其類型形參的上限是Number,這個類型形參用來定義Apple類的size變量。程序在①處創建了一個Apple對象,該Apple對象傳入了Integer作為類型形參的值,所以調用a的getSize()方法時返回Integer類型的值。當把a賦給一個不帶泛型信息的b變量時,編譯器就會丟失a對象的泛型信息,即所有尖括號里的信息都會丟失——因為Apple的類型形參的上限是Number類,所以編譯器依然知道b的getSize()方法返回Number類型,但具體是Number的哪個子類就不清楚了
從邏輯上來看,List
public class ErasureTest2 { public static void main(String[] args) { Listli = new ArrayList<>(); li.add(34); li.add(59); List list = li; // 下面代碼引起“未經檢查的轉換”的警告,編譯、運行時完全正常 List ls = list; // ① // 但只要訪問ls里的元素,如下面代碼將引起運行時異常 // ClassCastException System.out.println(ls.get(0)); } }
上面程序中定義了一個List
下面代碼與上面代碼的行為完全相似
public class ErasureTest2 { public static void main(String[] args) { Listli = new ArrayList<>(); li.add(34); li.add(59); System.out.println((String)li.get(0)); } }
程序從li中獲取一個元素,并試圖通過強制類型轉換把它轉換成一個String,將引發運行時異常。前面使用泛型代碼時,系統與之存在完全相似的行為,所以引發相同的ClassCastException異常
泛型與數組Java泛型與一個很重要的設計原則——如果一段代碼在編譯時沒有提出“[unchecked] 未經檢查的轉換”警告,則程序在運行時不會引發ClassCastException異常。正是基于這個原因,所以數組元素的類型不能包含類型變量或類型形參,除非是無上限的類型通配符。但可以聲明元素類型包含類型變量或類型形參的數組。也就是說,只能聲明List
Java不支持創建泛型數組
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/66419.html
摘要:引用泛型除了方法因不能使用外部實例參數外,其他繼承實現成員變量,成員方法,方法返回值等都可使用。因此,生成的字節碼僅包含普通的類,接口和方法。 為什么要使用泛型程序設計? 一般的類和方法,只能使用具體的類型:要么是基本類型,要么是自定義類的對應類型;如果要編寫可以應用于多種類型的代碼,這種刻板的限制對代碼的束縛就會很大。----摘自原書Ordinary classes and meth...
摘要:知識點總結泛型知識點總結泛型泛型泛型就是參數化類型適用于多種數據類型執行相同的代碼泛型中的類型在使用時指定泛型歸根到底就是模版優點使用泛型時,在實際使用之前類型就已經確定了,不需要強制類型轉換。 Java知識點總結(Java泛型) @(Java知識點總結)[Java, Java泛型] [toc] 泛型 泛型就是參數化類型 適用于多種數據類型執行相同的代碼 泛型中的類型在使用時指定 泛...
摘要:靜態變量是被泛型類的所有實例所共享的。所以引用能完成泛型類型的檢查。對于這個類型系統,有如下的一些規則相同類型參數的泛型類的關系取決于泛型類自身的繼承體系結構。事實上,泛型類擴展都不合法。 前言 和C++以模板來實現靜多態不同,Java基于運行時支持選擇了泛型,兩者的實現原理大相庭徑。C++可以支持基本類型作為模板參數,Java卻只能接受類作為泛型參數;Java可以在泛型類的方法中取得...
摘要:泛型類在類的申明時指定參數,即構成了泛型類。換句話說,泛型類可以看成普通類的工廠。的作用就是指明泛型的具體類型,而類型的變量,可以用來創建泛型類的對象。只有聲明了的方法才是泛型方法,泛型類中的使用了泛型的成員方法并不是泛型方法。 什么是泛型? 泛型是JDK 1.5的一項新特性,它的本質是參數化類型(Parameterized Type)的應用,也就是說所操作的數據類型被指定為一個參數,...
摘要:可以看到,如果我們給泛型類制定了上限,泛型擦除之后就會被替換成類型的上限。相應的,泛型類中定義的方法的類型也是如此。參考語言類型擦除下界通配符和的區別 本篇博客主要介紹了Java類型擦除的定義,詳細的介紹了類型擦除在Java中所出現的場景。 1. 什么是類型擦除 為了讓你們快速的對類型擦除有一個印象,首先舉一個很簡單也很經典的例子。 // 指定泛型為String List list1 ...
閱讀 3371·2021-11-22 09:34
閱讀 2857·2021-10-09 09:43
閱讀 1445·2021-09-24 09:47
閱讀 2199·2019-08-30 12:53
閱讀 998·2019-08-29 14:00
閱讀 3356·2019-08-29 13:17
閱讀 2269·2019-08-28 18:00
閱讀 1284·2019-08-26 12:00