什麼是泛型,有什麼用? 先運行下麵的代碼: 上面的代碼稍微修改下: 對比上面的代碼,沒加入泛型的時候,在程式運行期才發現問題,而加入了泛型則在程式編譯期就發現了,這就是泛型的優勢所在。 在第二段代碼中,泛型就好象是在告訴編譯器:這裡聲明的變數c只跟Date類型進行比較,如果跟別的類型比較,那麼就不能 ...
什麼是泛型,有什麼用?
先運行下麵的代碼:
public class Test {
public static void main(String[] args) {
Comparable c=new Date(); //Date實現了Comparable介面的
System.out.println(c.compareTo("red")); //這裡可以通過編譯,但在運行的時候會拋出異常。ClassCastException: java.lang.String cannot be cast to java.util.Date
}
}
上面的代碼稍微修改下:
public class Test {
public static void main(String[] args) {
Comparable<Date> c=new Date(); //這行修改了,加入了泛型
System.out.println(c.compareTo("red")); //這行會提示錯誤:compareTo (java.util.Date) in Comparable cannot be applied to (java.lang.String)
}
}
對比上面的代碼,沒加入泛型的時候,在程式運行期才發現問題,而加入了泛型則在程式編譯期就發現了,這就是泛型的優勢所在。
在第二段代碼中,泛型就好象是在告訴編譯器:這裡聲明的變數c只跟Date類型進行比較,如果跟別的類型比較,那麼就不能通過編譯。
再用ArrayList的例子看下
public class Test {
public static void main(String[] args) {
List list=new ArrayList(); //沒有泛型
list.add("HTTP"); //添加的是String類型
list.add("FTP"); //添加的是String類型
list.add("SMTP"); //添加的是String類型
list.add(1024); //不小心添加了個Integer類型,編譯和運行期都不會報錯
Object http = list.get(0); //取出第一個String元素,類型變成了Object
String ftp = list.get(1); //編譯錯誤:取出第二個String元素,卻不能直接賦值給String類型變數
String smtp = (String) list.get(2); //取出第三個String元素,經強制類型轉換賦值給String類型
}
}
上面的代碼就是在Java1.5泛型加入前的辦法,list中可以加入的是Object類型,同時加入String和Integer都沒問題,然後不管加進去的是什麼類型,取出來的時候都成了Object,還得強制轉換成自己的類型。但在實際編碼中往往只是添加同類型的元素,得小心翼翼的保證不會加入其他類型,否則在取出來的時候會出意外。泛型加入後,如果不小心添加了非指定類型元素,那根本不能通過編譯,在取出來的時候,直接就是指定的類型,而不再是Object。
也就是說對類型的保證由程式員轉到了編譯器,將可能的錯誤從運行期轉到了編譯期。其實泛型只是一層皮,功能的實現還是Object+強制轉換。
泛型就是Java的語法糖,所謂語法糖就是:這種語法對功能沒有影響,但更方便程式員使用,虛擬機並不支持這些語法,在編譯階段就被還原成了簡單語法結構。Java中的其他語法糖還有變長參數、自動拆裝箱、內部類等。
自定義泛型類
設想有這樣的一個類:用來盛裝兩個對象,但其類型不確定,可能是String,可能是File,可能是自定義的。我們引入一個類型參數T來代替它可能盛裝的類型:
public class Pair<T> { //這裡的<T>就是類型參數,寫在類名的後面
private T first;
private T second;
public Pair() {
first = null;
second = null;
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
public void setFirst(T first) {
this.first = first;
}
public void setSecond(T second) {
this.second=second;
}
public String toString(){
return first.toString()+" "+second.toString(); //甚至可以調用T的方法
}
}
類型參數:
寫在類名後面
可以有多個,比如:
T一般代表任意類型,E一般用於集合類型中,KV一般用於鍵值類型
類型參數T可以用在實例變數、局部變數、方法返回值中
可以調用類型參數T的方法。既然不知道T具體是什麼類型,那咋能調用其方法呢?後面再說
使用Pair類:
public class PairTest1 {
public static void main(String[] args) {
String[] words = {"Mary", "had", "a", "little", "las"};
Pair<String> mm = ArrayAlg.minmax(words); //指定mm中裝的是String類型
System.out.println("min= " + mm.getFirst());
System.out.println("max= " + mm.getSecond());
}
}
class ArrayAlg{
public static Pair<String> minmax(String[] a) { //計算一個字元串數組中的最大、最小值
if (a == null || a.length == 0) {
return null;
}
String min = a[0];
String max = a[0];
int len = a.length;
for (int i = 1; i < len; i++) {
if (min.compareTo(a[i]) > 0) {
min = a[i];
}
if (max.compareTo(a[i]) < 0) {
max = a[i];
}
}
return new Pair(min, max);
}
}
自定義泛型方法
定義泛型介面跟定義泛型類是一樣,另外還可以在一個非泛型類中定義泛型方法,當然也可以定義在泛型類中
看代碼:
public class ArrayAlg { //類中沒有泛型參數,這不是個泛型類
public static <T> T getMiddle(T[] a) { //類型參數在方法中,這是個泛型方法,類型參數放在返回值前修飾符後。該方法返回一個數組中間那個元素
return a[a.length/2];
}
}
使用該泛型方法
public class Test {
public static void main(String[] args) {
Integer[] a = {54, 42, 94, 23, 34};
Integer middle = ArrayAlg.<Integer>getMiddle(a); //使用的時候在方法調用前指定類型實參,其實也可以省略,編譯器能通過方法中的實參類型自動判定類型實參
System.out.println(middle);
}
}
對類型參數進行限定
看下麵的代碼,計算一個數組中的最小元素:
public class ArrayAlg {
public static <T> T min(T[] a) {
if (a == null || a.length == 0) {
return null;
}
T smallest = a[0];
for (int i=1;i<a.length;i++) {
if (smallest.compareTo(a[i]) > 0) { //這裡會有編譯錯誤,提示smallest沒有compareTo()方法
smallest = a[i];
}
}
return smallest;
}
}
上面代碼中,需要調用類型參數T的compareTo()方法,但T有沒有這個方法呢?不知道。但是如果能明確告知T實現了Comparable介面,那T類型就一定是具有compareTo()方法的,這就是類型限定。
在聲明類型參數處改為這樣:
public static <T extends Comparable> T min(T[] a) {
類型限定:
形式:
,關鍵字就是extends,沒有別的比如implements<T extends BoundingType>
可以進行多個限定:比如:
.註意這是且的關係<T extends Comparable & Serializable>
可以限定多個介面,但最多只能限定一個類,且這個類得排在第一位
,這裡指定了T的上限是Comparable,那麼<T extends Comparable>
的上限是什麼呢?是Object<T>
正是因為有了類型限定,才能調用T的方法。
泛型的擦除
前面說了,Java的泛型只是語法糖,僅僅存在於源碼層面。
編譯後的位元組碼中是沒有泛型的。
虛擬機中更沒有泛型類,所有的類都是普通類。
編譯器在編譯的時候,會將泛型信息擦除,轉換為原始類
,用限定類型替換泛型T,再插入必要的強制類型轉換或者橋方法。
如果有多個限定類型,那就用第一個替換,因此標記式介面要放到泛型列表的最後。
比如:
public class Pair<T>{
private T first;
public T getFirst(){
return first;
}
}
擦除泛型變成這樣了:
public class Pair{
private Object first;
public Object getFirst(){
return first;
}
}
下麵使用這個getFirst()方法,代碼片段:
Pair<Employee> buddies=....;
Employee buddy=buddies.getFirst();
上面這個代碼片段中,buddies.getFirst()取出來的為啥直接就是Employee呢,不是Object呢?因為在編譯的時候,編譯器自動插入了強制類型轉換,大概是這樣子Employee buddy=(Employee) buddies.getFirst();
。
橋方法--與多態的衝突
前面的Pair類類型擦除後,大概是這樣的:
public class Pair{
...
public void setSecond(Object second){
this.second=second;
}
}
下麵用一個DateInterval類繼承Pair,並重寫setSecond()方法,確保seconde一定大於first:
public class DateInterval extends Pair<Date>{
...
@Override //註意這個註解,沒有報錯,說明重寫成功了的
public void setSecond(Date second){ //該方法確保了第二個日期一定大於第一個
if(second.compareTo(getFirst())>=0){
super.setSecond(second);
}
}
}
問題來了,setSecond(Date second)
重寫了setSecond(Object second)
,這不科學啊。
實際上,DateInterval編譯後新增加了一個橋方法
:
public void setSecond(Object second){ //實際完成重寫的是這個橋方法
setSecond((Date)Object); //調用DateInterval的setSecond(Date second)方法
}
除了set外,還有get也存在問題:
DateInterval中會存在這樣的兩個方法:
public Object getSecond(){}
public Date getSecond(){}
這裡兩個get方法的方法簽名是相同的,不能共存於一個類中。
但其實它們就是能共存,因為虛擬機辨別方法的時候還加上了返回值類型。
所以啊,泛型遇到編譯和虛擬機有點特別了:
虛擬機沒有泛型,只有普通的類和方法
類型參數要被其限定類型替換
插入橋方法來保持多態
插入了強制類型轉換
總結
- 泛型就是語法糖,僅存在於源代碼中,功能的實現還是Object(限定類型)+強制類型轉換實現
- 泛型能保證程式員少犯錯誤,將發現錯誤的時間點從運行期提到編譯期
- 可以定義泛型類、泛型介面、泛型方法。泛型類和泛型介面,把類型參數寫在類名或介面名前面;泛型方法將類型參數寫到方法名前面
- 類型參數可以進行限定,且能限定多個,可以是[0,1]個類+[0,n]個介面,如果有類,那得寫在第一位,標記式介面要寫在最後
- 泛型在編譯後,都會被擦除成原始類型,用限定類型替代類型參數,位元組碼和虛擬機中是沒有泛型的
- 編譯的時候,還會根據需要,加入橋方法,以保持多態