參考文獻---Java 基礎(一)| 使用泛型的正確姿勢
前言
為跳槽面試做準備,今天開始進入 Java 基礎的復習。希望基礎不好的同學看完這篇文章,能掌握泛型,而基礎好的同學權當復習,希望看完這篇文章能夠起一點你的青澀記憶。
一、什么是泛型
泛型,即“參數化類型”。一提到參數,最熟悉的就是定義方法時有形參,然后調用此方法時傳遞實參。那么參數化類型怎么理解呢?
顧名思義,就是將類型由原來的具體的類型參數化(動詞),類似于方法中的變量參數,此時類型也定義成參數形式(可以稱之為類型形參),
然后在使用/調用時傳入具體的類型(類型實參)。
泛型的本質是為了參數化類型(在不創建新的類型的情況下,通過泛型指定的不同類型來控制形參具體限制的類型)。也就是說在泛型使用過程中。
操作的數據類型被指定為一個參數,這種參數類型可以用在類、接口和方法中,分別被稱為泛型類、泛型接口、泛型方法。
參考:https://www.cnblogs.com/coprince/p/8603492.html
1.1常見的泛型類型變量:
E:元素(Element),多用于 java 集合框架
K:關鍵字(Key)
N:數字(Number)
T:類型(Type)
V:值(Value)
二、為什么要使用泛型
回答這個問題前,首先舉兩個栗子,我想打印字符串到控制臺,如下代碼:
package com.nasus.generic;
import java.util.ArrayList;
import java.util.List;
/**
* Project Name:review_java <br/>
* Package Name:com.nasus.generic <br/>
* Date:2019/12/28 20:58 <br/>
*
* @author <a href="turodog@foxmail.com">chenzy</a><br/>
*/
public class Show {
public static void main(String[] args) {
List list=new ArrayList();
list.add("一個優秀的廢人");
list.add("java 工程師");
list.add(666);
for (int i = 0; i < list.size(); i++) {
String value= (String) list.get(i);
System.out.println(value);
}
}
}
本身我的 list 是打算裝載 String 去打印的,但是大家發現沒有?我傳入 int 型時(編譯期),Java 是沒有任何提醒的(頂多是 IDEA 警告)。直到我循環調用(運行期)打印方法,打印 int 型時,Java 才報錯:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
一個優秀的廢人
at com.nasus.generic.Show.main(Show.java:23)
java 工程師
第二栗子,我想實現一個可以操作各種類型的加法,如下代碼:
package com.nasus.generic.why;
/**
* Project Name:review_java <br/>
* Package Name:com.nasus.generic <br/>
* Date:2019/12/28 21:18 <br/>
*
* @author <a href="turodog@foxmail.com">chenzy</a><br/>
*/
public class Add {
private static int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static float add(float a, float b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static double add(double a, double b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
// 一個泛型方法
private static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
public static void main(String[] args) {
Add.add(1, 2);
Add.add(1f, 2f);
Add.add(1d, 2d);
System.out.println("--------------------------");
// 以下三個都是調用泛型方法
Add.add(Integer.valueOf(1), Integer.valueOf(2));
Add.add(Float.valueOf(1), Float.valueOf(2));
Add.add(Double.valueOf(1), Double.valueOf(2));
}
}
這個加法可以操作 int、float、double 類型,但相應的也必須重寫對應的加法,而此時我其實可以就用一個泛型方法就實現了上面三個重載方法的功能。
1+2=3
1.0+2.0=3.0
1.0+2.0=3.0
--------------------------
1+2=3.0
1.0+2.0=3.0
1.0+2.0=3.0
所以使用泛型原因有三個:
提高可讀性
使 ClassCastException 這種錯誤在編譯期就檢測出來
適用于多種數據類型執行相同的代碼(代碼復用)
參考:http://www.lxweimin.com/p/986f732ed2f1
三、泛型詳解
3.1泛型類
由我們指定想要傳入泛型類中的類型,把泛型定義在類上,用戶使用該類的時候,才把類型明確下來,比如:定義一個萬能的實體數據暫存工具類。
注意:泛型類在初始化時就把類型確定了
package com.nasus.generic.how;
/**
* Project Name:review_java <br/>
* Package Name:com.nasus.generic.how <br/>
* Date:2019/12/28 21:35 <br/>
*
* @author <a href="turodog@foxmail.com">chenzy</a><br/>
*/
public class EntityTool<T> {
private T entity;
public T getEntity() {
return entity;
}
public void setEntity(T entity) {
this.entity = entity;
}
public static void main(String[] args) {
// 創建對象并指定元素類型
EntityTool<String> stringTool = new EntityTool<>();
stringTool.setEntity("一個優秀的廢人");
String s = stringTool.getEntity();
System.out.println(s);
// 創建對象并指定元素類型
EntityTool<Integer> integerTool = new EntityTool<>();
// 此時,如果這里傳入的還是 String 類型,那就會在編譯期報錯
integerTool.setEntity(10);
int i = integerTool.getEntity();
System.out.println(i);
}
}
3.2泛型方法
有時候我們只想在方法中使用泛型,可以這么定義:
值得注意的是:
與泛型類不同,泛型方法在調用時才確定最終類型
若有返回值,返回值不需要強轉
package com.nasus.generic.how;
/**
* Project Name:review_java <br/>
* Package Name:com.nasus.generic.how <br/>
* Date:2019/12/28 21:46 <br/>
*
* @author <a href="turodog@foxmail.com">chenzy</a><br/>
*/
public class Show {
public static <T> T show(T t) {
System.out.println(t);
return t;
}
public static void main(String[] args) {
// 返回值不用強轉,傳進去是什么,返回就是什么
String s = show("一個優秀的廢人");
int num1 = show(666);
double num2 = show(666.666);
System.out.println("------------------------");
System.out.println(s);
System.out.println(num1);
System.out.println(num2);
}
}
3.3泛型接口
泛型接口分兩種實現方法:
一是實現類不明確泛型接口的類型參數變量,這時實現類也必須定義類型參數變量(比如下面 Showimpl)
接口:
public interface Show<T> {
void show(T t);
}
public class ShowImpl<T> implements Show<T>{
@Override
public void show(T t) {
System.out.println(t);
}
public static void main(String[] args) {
ShowImpl<String> stringShow = new ShowImpl<>();
stringShow.show("一個優秀的廢人");
}
}
二是明確泛型接口的類型參數變量
public class ShowImpl2 implements Show<String>{
@Override
public void show(String s) {
System.out.println("一個優秀的廢人");
}
}
3.5 限定泛型類型變量
限定泛型類型上限
其實就是相當于指定了泛型類的父類
聲明類:類名<泛型標識 extends 類>{}
在類中使用:
// 用在類上
public class Show<T extends Number> {
private T show(T t){
System.out.println(t);
return t;
}
public static void main(String[] args) {
// 初始化時指定類型
Show<Integer> show = new Show<>();
show.show(6666666);
// 報錯,該類只接受繼承于 Number 的泛型參數
// Show<String> stringShow = new Show<>();
}
}
方法中使用:
定義對象:類名<泛型標識 extends 類> 對象名稱
public class Info<T> {
// 定義泛型變量
private T var;
public void setVar(T var) {
this.var = var;
}
public T getVar() {
return this.var;
}
public String toString() {
return this.var.toString();
}
}
public class ShowInfo {
// 用在方法上,只能接收 Number 及其子類
public static void showInfo(Info<? extends Number> t) {
System.out.print(t);
}
public static void main(String args[]) {
Info<Integer> i1 = new Info<>();
Info<Float> i2 = new Info<>();
i1.setVar(666666666);
i2.setVar(666666.66f);
showInfo(i1);
showInfo(i2);
}
}
限定泛型類型下限
定義對象:類名<泛型標識 extends 類> 對象名稱
與指定上限相反,指定下限定很簡單,就是相當于指定了泛型類的子類,不再贅述。
public class ShowInfo {
// 只接受 String 的父類
public static void showInfo(Info<? super String> t) {
System.out.println(t);
}
public static void main(String args[]) {
Info<String> stringInfo = new Info<>();
Info<Object> objectInfo = new Info<>();
stringInfo.setVar("一個優秀的廢人");
objectInfo.setVar(new Object());
showInfo(stringInfo);
showInfo(objectInfo);
}
}
3.6 通配符類型
<? extends Parent> 指定了泛型類型的上限
<? super Child> 指定了泛型類型的下屆
<?> 指定了沒有限制的泛型類型
3.7 泛型擦除
泛型是提供給 javac 編譯器使用的,它用于限定集合的輸入類型,讓編譯器在源代碼級別上,即擋住向集合中插入非法數據。但編譯器編譯完帶有泛形的 java 程序后,生成的 class 文件中將不再帶有泛形信息,以此使程序運行效率不受到影響,這個過程稱之為 “擦除”。
3.8 泛型的使用規范
1、不能實例化泛型類
2、靜態變量或方法不能引用泛型類型變量,但是靜態泛型方法是可以的
3、基本類型無法作為泛型類型
4、無法使用 instanceof 關鍵字或 == 判斷泛型類的類型
5、泛型類的原生類型與所傳遞的泛型無關,無論傳遞什么類型,原生類是一樣的
6、泛型數組可以聲明但無法實例化
7、泛型類不能繼承 Exception 或者 Throwable
8、不能捕獲泛型類型限定的異常但可以將泛型限定的異常拋出
3.9 虛擬機是如何實現泛型的 (JAVA:類型擦除 C++:類型膨脹)
泛型思想早在C++語言的模板(Template)中就開始生根發芽,在Java語言處于還沒有出現泛型的版本時,只能通過Object是所有類型的父類和類型強制轉換兩個特點的配合來實現類型泛化。,由于Java語言里面所有的類型都繼承于java.lang.Object,所以Object轉型成任何對象都是有可能的。但是也因為有無限的可能性,就只有程序員和運行期的虛擬機才知道這個Object到底是個什么類型的對象。在編譯期間,編譯器無法檢查這個Object的強制轉型是否成功,如果僅僅依賴程序員去保障這項操作的正確性,許多ClassCastException的風險就會轉嫁到程序運行期之中。
泛型技術在C#和Java之中的使用方式看似相同,但實現上卻有著根本性的分歧,C#里面泛型無論在程序源碼中、編譯后的IL中(Intermediate Language,中間語言,這時候泛型是一個占位符),或是運行期的CLR中,都是切實存在的,List<int>與List<String>就是兩個不同的類型,它們在系統運行期生成,有自己的虛方法表和類型數據,這種實現稱為類型膨脹,基于這種方法實現的泛型稱為真實泛型。
Java語言中的泛型則不一樣,它只在程序源碼中存在,在編譯后的字節碼文件中,就已經替換為原來的原生類型(Raw Type,也稱為裸類型)了,并且在相應的地方插入了強制轉型代碼,因此,對于運行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個類,所以泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱為類型擦除,基于這種方法實現的泛型稱為偽泛型。
將一段Java代碼編譯成Class文件,然后再用字節碼反編譯工具進行反編譯后,將會發現泛型都不見了,程序又變回了Java泛型出現之前的寫法,泛型類型都變回了原生類型
上面這段代碼是不能被編譯的,因為參數List<Integer>和List<String>編譯之后都被擦除了,變成了一樣的原生類型List<E>,擦除動作導致這兩種方法的特征簽名變得一模一樣。
由于Java泛型的引入,各種場景(虛擬機解析、反射等)下的方法調用都可能對原有的基礎產生影響和新的需求,如在泛型類中如何獲取傳入的參數化類型等。因此,JCP組織對虛擬機規范做出了相應的修改,引入了諸如Signature、LocalVariableTypeTable等新的屬性用于解決伴隨泛型而來的參數類型的識別問題,Signature是其中最重要的一項屬性,它的作用就是存儲一個方法在字節碼層面的特征簽名[3],這個屬性中保存的參數類型并不是原生類型,而是包括了參數化類型的信息。修改后的虛擬機規范要求所有能識別49.0以上版本的Class文件的虛擬機都要能正確地識別Signature參數。
另外,從Signature屬性的出現我們還可以得出結論,擦除法所謂的擦除,僅僅是對方法的Code屬性中的字節碼進行擦除,實際上元數據中還是保留了泛型信息,這也是我們能通過反射手段取得參數化類型的根本依據。