Java泛型入門


 Java泛型入門

在學習本章之前,需要對Java的集合(CollectionMap)有一定的基礎。

Java集合有一個缺點,就是把一個對象“丟進”集合裏後,集合就會“忘記”這個對象的數據類型,當再次取出該對象時,該對象的編譯類型就變成了Object類型(其運行時類型沒變)。

之所以這樣設計是因爲設計集合的程序員不會知道我們要用它來保存什麼類型的對象,所以這樣設計具有很好的通用性。但是這樣做帶來如下兩個問題:

l  集合對與元素類型沒有限制,如一個集合能保存一個蘋果對象也能保存一個香蕉對象;

l  由於對象進入集合後失去了其狀態信息,所以取出來時需要進行強制類型轉換。

看下面的程序:

     List list = new ArrayList();

     list.add(1);

     list.add(2);

     list.add("五號");//一不小心插入了String

     for (Object object : list){

        //取出五號時報ClassCastException

        Integert = (Integer)object;

  }

上面是一個集合沒有使用泛型時出現的錯誤,那麼當我們使用泛型時,該錯誤就會避免,例如:

     List<Integer>list = newArrayList<Integer>();

     list.add(1);

     list.add(2);

     list.add("五號");//插入String,引起編譯錯誤

 

1、認識泛型

Java的參數化類型被稱爲泛型,即允許我們在創建集合時就指定集合元素的類型,該集合只能保存其指定類型的元素。

泛型允許在定義類、接口、方法時使用類型形參,這個類型形參將在聲明變量、創建對象、調用方法時動態地指定。例如:

//定義一個接口

interface Money<E>{

   Eget(intindex);

   boolean add(E e);

}

//定義一個類

public classApple<T>{

   private T info;

   public Apple(T info) {

     this.info = info;

   }

  

   public T getInfo(){

     return this.info;

   }

  

   public void setInfo(T info){

     this.info = info;

   }

    

   public static void main(String[] args) {

     Apple<String>ap1 = newApple<String>("小蘋果");

     System.out.println(ap1.getInfo());

     Apple<Double>ap2 = newApple<Double>(1.23);

     System.out.println(ap2.getInfo());

   }

}

需要注意的是,在靜態方法、靜態初始化塊或者靜態變量的聲明和初始化中不允許使用類型形參。因爲不管爲泛型的類型形參傳入哪一種類型實參,對於Java來說,它們依然被當成同一個類處理,在內存中也只佔用一塊內存空間。不管泛型的實際類型參數是什麼,它們在運行時總有同樣的類(class),例如下面的程序將輸出true

     List<String> l1 = new ArrayList<String>();

     List<Double> l2 = new ArrayList<Double>();

     System.out.println(l1.getClass() == l2.getClass());

 

2、類型通配符

如果我們想定義一個方法,這個方法的參數是一個集合形參,但是集合形參的元素類型是不確定的。可能會想到下面兩種定義方法:

   public void test1(List l){ }

   public void test2(List<Object> l){ }

test1可以使用,但是會報泛型警告;

test2在傳入非List<Object>時無法使用,會報“test2List<Object>)對於參數(List<String>)不適用”;

爲了表示各種泛型List的父類,我們需要使用類型通配符,類型通配符是一個問號(?),將一個問號作爲類型實參傳給List集合,寫作List<?>,那麼我們就可以這樣定義上面的方法:

   public void test3(List<?> l){ }

此時我們就可以傳入以任何元素類型爲集合形參的List了,就算是自定義類也可以。

但還有一種特殊的情形,我們不想這個List<?>是任何泛型List的父類,只想表示它是某一類泛型List的父類,例如我們有一個圓形類(Circle)、矩形類(Rectangle),這兩個類都繼承了Shape抽象類,那麼我們就可以這樣設計:

public classTest{

   public static void main(String[] args) {

     List<Circle>list = newArrayList<Circle>();

     list.add(new Circle());

     Canvasc = newCanvas();

     c.drawAll(list);

   }

}

abstract classShape{

   public abstract void draw(Canvas c);

}

class Canvas{

   public String toString() {

     return "Canvas";

   }

   public void drawAll(List<? extends Shape> shapes){

     for (Shape shape : shapes){

        shape.draw(this);

      }

   }

}

class Circle extends Shape{

   public void draw(Canvas c) {

     System.out.println("在畫布" + c + "上畫一個圓");

   }

}

class Rectangle extends Shape{

   public void draw(Canvas c) {

     System.out.println("在畫布" + c + "上畫一個矩形");

   }

}

List<? extends Shape>是受限制通配符的例子,此處的問號(?)代表一個未知的類型,就想前面看到的通配符一樣。但是此處的這個未知類型一定是Shape的子類或Shape本身,因此我們可以把Shape稱爲這個通配符的上限。

這裏需要注意的是,因爲不知道這個受限制通配符的具體類型,所以不能把Shape對象或其子類的對象加入這個泛型集合中,例如下面的代碼會報編譯錯誤:

   public void drawAll(List<? extends Shape> shapes){

     //此處會報編譯錯誤

     shapes.add(new Rectangle());

}

Java泛型不僅允許在使用通配符形參時設定上限,而且可以在定義類型形參時設定上限,用於表示傳給該類型形參的實際類型要麼是該上限類型,要麼是該上限類型的子類,例如:

public class Apple<T extends Number>{

   T colT;

   public static void main(String[]args) {

     Apple<Integer> ai = new Apple<Integer>();

     Apple<Double> ad = new Apple<Double>();

     //下面代碼將引發編譯錯誤,因爲String類不是Number的子類型

     Apple<String> as = new Apple<String>();

   }

}

 

3、泛型方法

所謂泛型方法,就是在聲明方法時定義一個或多個類型形參。泛型方法的用法格式如下:

修飾符<T,S> 返回值類型 方法名(形參列表){

   //方法體

}

把上面方法的格式和普通方法的格式進行對比,不難發現泛型方法的方法簽名比普通方法的方法簽名多了類型形參聲明,類型形參聲明以尖括號包括起來,多個類型形參之間以逗號(,)隔開,所有的類型形參聲明方法方法修飾符和方法返回值類型之間。

例如,我們要寫一個這樣的方法,用於將一個Object數組的所有元素添加到一個Collection集合中:

  static <T> void fromArrayToCollection(T[] a,Collection<T> c){

     for (T t : a) {

        c.add(t);

     }

}

上面的方法是正確的,沒有錯誤,但是在我們實際使用的過程中,我們只能將數組中的元素加入Collection中,想要將一個Collection中的元素加入到另一個Collection中,上面的方法就不適用了。那我們可不可以這樣改進上面的方法呢?

   static <T> void fromArrayToCollection(Collection<T> a,Collection<T> b){

     for (T t : a) {

        b.add(t);

     }

}

  public static void main(String[] args) {

     List<String> a = new ArrayList<String>();

     List<Object> b = new ArrayList<Object>();

     fromArrayToCollection(a, b);

   }

顯然,在我們調用fromArrayToCollection時會引發編譯錯誤,這是因爲編譯器無法準確地推斷出泛型方法中類型形參的類型,不知道應該用String還是用Object。爲了避免這種錯誤,可以將方法改爲這樣:

  static <T> void fromArrayToCollection(Collection<? extends T> a, Collection<T> b){

     for (T t : a) {

        b.add(t);

     }

   }

   public static void main(String[]args) {

     List<String> a = new ArrayList<String>();

     List<Object> b = new ArrayList<Object>();

     fromArrayToCollection(a, b);

}

上面的方法中,將該方法前一個形參類型改爲Collection<? extends T>這種採用類型通配符的表示方式,只要該方法的前一個Collection集合裏的元素類型是後一個Collection集合裏的元素類型的子類即可。

 
 
4、泛型方法和類型通配符的區別

大多數時候都可以使用泛型方法來代替類型通配符。例如對於JavaCollection接口中兩個方法的定義:

public interface Collection<E>{

   boolean containsAll(Collection<?>c);

   booleanaddAll(Collection<? extends E> c);

   ...

}

上面集合中的兩個方法的形參都採用了類型通配符的形式,也可以採用泛型方法的形式,如下所示:

public interface Collection<E>{

   <T> boolean containsAll(Collection<T> c);

   <T extends E> boolean addAll(Collection<T> c);

   ...

}

上面方法使用了<Textends E>泛型形式,這時定義類型形參時設定上限(其中ECollection接口裏定義的類型形參,在該接口裏E可當成普通類型使用)。

上面兩個方法中類型形參T只使用了一次,類型形參T產生的唯一效果是可以在不同的調用點傳入不同的實際類型。對於這種情況,應該使用通配符:通配符就是被設計用來支持靈活的子類化的。

泛型方法允許類型形參被用來表示方法的一個或多個參數之間的類型依賴關係,或者方法返回值與參數之間的類型依賴關係。如果沒有這樣的類型依賴關係,就不應該使用泛型方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章