程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> Java編程手冊-泛型

Java編程手冊-泛型

編輯:JAVA綜合教程

Java編程手冊-泛型


1. 泛型的引入(JDK 1.5)
在方法中傳入一個參數,這個大家一定非常熟悉,一般的做法就是把參數放在一個圓括號()中並且將他們傳遞給方法,在泛型中,我們可以跟方法中傳遞參數一樣來傳遞類型信息,做法就是將類型放在一個尖括號<>中。

JDK 1.5中引入了泛型,它允許我們對類型進行參數化,也就是類的設計者在類的定義過程中使用泛型,這樣使用者在類的實例化或者方法調用的時候可以動態指定類型,這樣在類的內部就可以動態使用這個類型了。

例如,類ArrayList就是被設計者設計成泛型,它帶有一個泛型類型

 

public class ArrayList implements List .... {
   // Constructor
   public ArraList() { ...... }

   // Public methods
   public boolean add(E e) { ...... }
   public void add(int index, E element) { ...... }
   public boolean addAll(int index, Collection c)
   public abstract E get(int index) { ...... }
   public E remove(int index)
   .......
}

當在實例化一個ArrayList的時候,使用者就需要為E指定一個具體的類型,這個具體的類型就會取代類中所有的E,也就是類中使用到類型E的地方全部被取代為我們指定的真實類型。

 

 

ArrayList lst1 = new ArrayList(); // E substituted with Integer
lst1.add(0, new Integer(88));
lst1.get(0);
 
ArrayList lst2 = new ArrayList();   // E substituted with String
lst2.add(0, "Hello");
lst2.get(0);

上面的例子就是類的設計者在定義類的時候使用到了泛型,這樣使用者在實例化這個類的時候,就需要為泛型類型E指定一個真實的類型,類型信息的傳遞是通過<>傳遞的,這就類似於方法參數通過()傳遞一樣。

在集合引入泛型之前都是類型不安全的
如果大家對於JDK 1.5之前版本的集合比較屬性的話,應該知道集合內部的元素類型都是使用的java.lang.Object,它使用的就是一種多態的原理,因為任何Object的子類都可以被Object代替,而Object是Java中所有對象類型的超類,這樣我們集合中就可以存放任何的對象類型了。但是它存在一個明顯的問題,假如我們定義了一個存放String對象的ArrayList,在add(Object)的時候,我們的String類型就會被向上轉換為Object類型,這個是編譯器隱式操作的,但是當我們獲取這個元素的時候,我們獲取的是Object類型的對象,這個時候,我們需要手動顯式的將Object類型對象轉換為String類型的對象,如果我們將得到的Object類型對象轉換為一個非String類型的對象或者我們add(Object)存放的是一個非String對象,獲取這個元素的時候將它轉化為String類型,這個時候,編譯器是檢查不出來錯誤的,但是在運行的時候進行手動類型轉換的時候就會拋出ClassCastException異常。

 

// Pre-JDK 1.5
import java.util.*;
public class ArrayListWithoutGenericsTest {
   public static void main(String[] args) {
      List strLst = new ArrayList();  // List and ArrayList holds Objects
      strLst.add("alpha");            // String upcast to Object implicitly
      strLst.add("beta");
      strLst.add("charlie");
      Iterator iter = strLst.iterator();
      while (iter.hasNext()) {
         String str = (String)iter.next(); // need to explicitly downcast Object back to String
         System.out.println(str);
      }
      strLst.add(new Integer(1234));       // Compiler/runtime cannot detect this error
      String str = (String)strLst.get(3);  // compile ok, but runtime ClassCastException
   }
}
為了解決這個問題,也就是在編譯的時候就可以進行類型的檢查,這樣就引入了泛型。

 

2. 泛型

下面是一個自定義的ArrayList版本,叫做MyArrayList,它沒有使用泛型。

 

// A dynamically allocated array which holds a collection of java.lang.Object - without generics
public class MyArrayList {
   private int size;     // number of elements
   private Object[] elements;
   
   public MyArrayList() {         // constructor
      elements = new Object[10];  // allocate initial capacity of 10
      size = 0;
   }
   
   public void add(Object o) {
      if (size < elements.length) {
         elements[size] = o;
      } else {
         // allocate a larger array and add the element, omitted
      }
      ++size;
   }
   
   public Object get(int index) {
      if (index >= size)
         throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
      return elements[index];
   }
   
   public int size() { return size; }
}
從上面很輕易的就可以看出MyArrayList不是類型安全的,例如,如果我們希望創建一個存放String類型對象的MyArrayList,但是我們向裡面添加了一個Integer對象,編譯器是不能檢查出異常,這是因為我們MyArrayList設計的是存放Object類型的元素,並且任何對象類型都可以向上轉換為Object類型的對象。

 

public class MyArrayListTest {
   public static void main(String[] args) {
      // Intends to hold a list of Strings, but not type-safe
      MyArrayList strLst = new MyArrayList();
      // adding String elements - implicitly upcast to Object
      strLst.add("alpha");
      strLst.add("beta");
      // retrieving - need to explicitly downcast back to String
      for (int i = 0; i < strLst.size(); ++i) {
         String str = (String)strLst.get(i);
         System.out.println(str);
      }
   
      // Inadvertently added a non-String object will cause a runtime
      // ClassCastException. Compiler unable to catch the error.
      strLst.add(new Integer(1234));   // compiler/runtime cannot detect this error
      for (int i = 0; i < strLst.size(); ++i) {
         String str = (String)strLst.get(i);   // compile ok, runtime ClassCastException
         System.out.println(str);
      }
   }
}

從上面可以看出,如果我們想創建一個String類型的List,但是我們添加了一個非String類型的對象元素,這個對象同樣是可以向上轉換為Object對象類型的,並且編譯器自動完成,編譯器並不能檢查它是否合法,這樣就存在一個隱患,當我們獲取這個元素的時候,它是一個Object類型,我們需要手動轉換為String類型,這個時候就會拋出ClassCastException異常,它發生在運行時期。

 

2.1 泛型類

JDK 1.5引入了所謂的泛型來解決這一問題,泛型允許我們去進行類型的抽象,我們可以創建一個泛型類並且在類實例化的時候指定具體類型信息。編譯器在編譯器期間會進行相應的類型檢查,這樣就確保了在運行時期不會有類型轉換的異常發生,這就是所謂的類型安全。

下面我們來看看java.util.List的聲明接口。

 

public interface List extends Collection {
   boolean add(E o);
   void add(int index, E element);
   boolean addAll(Collection c);
   boolean containsAll(Collection c);
   ......
}

就是形式化的類型參數,在類實例化的時候就可以傳遞真實的類型參數進去來替換這個形式化的類型參數。

這個跟方法的調用是一樣的,在定義方法的時候我們會聲明形參,在調用方法的時候,形參就會接受實參進來。

例如:方法的定義,聲明形參

 

// A method's definition
public static int max(int a, int b) {  // int a, int b are formal parameters
   return (a > b) ? a : b;
}
方法的調用,傳遞實參

 

// Invocation: formal parameters substituted by actual parameters
int maximum = max(55, 66);   // 55 and 66 are actual parameters
int a = 77, b = 88;
maximum = max(a, b);         // a and b are actual parameters
返回到上面的java.util.List,假如我們創建的了一個List,這個時候,形參類型E就接受到了一個實參類型,這樣我們在使用E的時候實際就是使用這個實參類型Integer了。

 

正式的類型參數命名規范

一般使用一個大寫的字母作為類型參數。例如:

 

表示一個集合元素的類型
表示一個類型
表示鍵和值的類型
表示一個數字類型
S,U,V,等表示第二個、第三個、第四個類型參數
泛型類的例子 下面創建了一個GenericBox,它接收一個泛型類型E,表示一個content的類型,在構造器、getter、setter中都使用到了這個參數化類型E,toString方法返回的是它的真實類型。

 

 

public class GenericBox {
   // Private variable
   private E content;
 
   // Constructor
   public GenericBox(E content) {
      this.content = content;
   }
 
   public E getContent() {
      return content;
   }
 
   public void setContent(E content) {
      this.content = content;
   }
 
   public String toString() {
      return content + " (" + content.getClass() + ")";
   }
}

下面使用不同的類型(String, Integer and Double)來檢測這個GenericBoxes類,需要注意的是JDK 1.5也引入了基本數據類型與對應對象類型直接的自動裝箱和解箱操作。

 

 

public class TestGenericBox {
   public static void main(String[] args) {
      GenericBox box1 = new GenericBox("Hello");
      String str = box1.getContent();  // no explicit downcasting needed
      System.out.println(box1);
      GenericBox box2 = new GenericBox(123);  // autobox int to Integer
      int i = box2.getContent();       // downcast to Integer, auto-unbox to int
      System.out.println(box2);
      GenericBox box3 = new GenericBox(55.66);  // autobox double to Double
      double d = box3.getContent();     // downcast to Double, auto-unbox to double
      System.out.println(box3);
   }
}

輸出結果:

 

Hello (class java.lang.String)
123 (class java.lang.Integer)
55.66 (class java.lang.Double)

移除類型
從上面的例子我們可以看到,似乎編譯器在類實例的時候使用真實類型(例如String, Integer)替換到了參數化類型E,事實上,編譯器是使用Object類型替換到了類中所有的參數化類型E,只是編譯器會針對傳入的真實類型參數來進行類型檢測和轉換,這樣做的目的是它可以跟之前非泛型的類進行兼容,並且同一個類可以使用所有對象類型參數。這個過程就叫做類型的移除。

下面我們返回到上面我們寫的MyArrayList的例子,我們知道它不是一個泛型類型,下面我們來寫一個泛型的版本。

 

// A dynamically allocated array with generics
public class MyGenericArrayList {
   private int size;     // number of elements
   private Object[] elements;
   
   public MyGenericArrayList() {  // constructor
      elements = new Object[10];  // allocate initial capacity of 10
      size = 0;
   }
   
   public void add(E e) {
      if (size < elements.length) {
         elements[size] = e;
      } else {
         // allocate a larger array and add the element, omitted
      }
      ++size;
   }
   
   public E get(int index) {
      if (index >= size)
         throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
      return (E)elements[index];
   }
   
   public int size() { return size; }
}

解析程序
MyGenericArrayList聲明了一個帶有類型參數E的泛型類,在真實實例化的時候,例如:MyGenericArrayList, 真實類型會取代參數類型E。編譯器對於泛型的處理其實就是將泛型代碼轉換或者重寫成非泛型代碼,這樣確保了向後兼容性,這個過程也就是類型移除,例如將ArrayList 轉換為ArrayList,也就是類型參數E默認會被Object類型替代,當出現類型不匹配的時候,編譯器就會插入類型轉換操作。

 

MyGenericArrayList被編譯器處理後形式如下:

 

// The translated code
public class  MyGenericArrayList {
   private int size;     // number of elements
   private Object[] elements;
   
   public MyGenericArrayList() {  // constructor
      elements = new Object[10];  // allocate initial capacity of 10
      size = 0;
   }
   
   // Compiler replaces E with Object, but check e is of type E, when invoked to ensure type-safety
   public void add(Object e) {
      if (size < elements.length) {
         elements[size] = e;
      } else {
         // allocate a larger array and add the element, omitted
      }
      ++size;
   }
   
   // Compiler replaces E with Object, and insert downcast operator (E) for the return type when invoked
   public Object get(int index) {
      if (index >= size)
         throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
      return (Object)elements[index];
   }
   
   public int size() { 
      return size; 
   }
}

當類使用真實類型參數進行實例化的時候,例如:MyGenericArrayList,這樣編譯器就會確保add(E e)只能針對String類型進行操作,當使用get()返回類型E的對象的時候,編譯器也會插入相應的類型轉換操作來匹配類型String。

例如

 

public class MyGenericArrayListTest {
   public static void main(String[] args) {
      // type safe to hold a list of Strings
      MyGenericArrayList strLst = new MyGenericArrayList();
   
      strLst.add("alpha");   // compiler checks if argument is of type String
      strLst.add("beta");
   
      for (int i = 0; i < strLst.size(); ++i) {
         String str = strLst.get(i);   // compiler inserts the downcasting operator (String)
         System.out.println(str);
      }
   
      strLst.add(new Integer(1234));  // compiler detected argument is NOT String, issues compilation error
   }
}

可以看到,其實泛型跟非泛型的其實是相同的,只是對於泛型,編譯器會針對傳入的真實類型在編譯期間進行類型檢查,確保類型的一致性,這樣就避免了運行時的類型安全問題。

跟C++中模板不同的是,在C++中,對於每一個指定的參數類型都會單獨創建一個新的類,但是在Java中,泛型類編譯一次之後就可以被每一個指定的類型參數進行使用。

 

2.2 泛型方法
方法也可以定義為泛型類型,例如:

 

public static  void ArrayToArrayList(E[] a, ArrayList lst) {
   for (E e : a) lst.add(e);
}
在泛型方法中,需要在返回類型之前聲明類型參數,這樣類型參數就可以在方法的參數列表或者返回類型上使用了。

 

和泛型類相似,當編譯器也會使用Object類型來替換參數類型E,例如上面的代碼被編譯器處理後形式如下:

 

public static void ArrayToArrayList(Object[] a, ArrayList lst) {  // compiler checks if a is of type E[],
                                                                  //   lst is of type ArrayList
   for (Object e : a) lst.add(e);                                 // compiler checks if e is of type E
}

同樣編譯器添加了類型的檢查的操作來確保類型安全。它會檢查a是否為類型E[],lst是否為類型ArrayList,e是否為類型E,其中參數類型E會根據傳入真實類型動態確定。
import java.util.*;
public class TestGenericMethod {
   
   public static  void ArrayToArrayList(E[] a, ArrayList lst) {
      for (E e : a) lst.add(e);
   }
   
   public static void main(String[] args) {
      ArrayList lst = new ArrayList();
   
      Integer[] intArray = {55, 66};  // autobox
      ArrayToArrayList(intArray, lst);
      for (Integer i : lst) System.out.println(i);
   
      String[] strArray = {"one", "two", "three"};
      //ArrayToArrayList(strArray, lst);   // Compilation Error below
   }
}

 

另外,在泛型方法中,泛型有一個可選的語法就是指定泛型方法中的類型。你可以將指定的真實類型放在點操作符和方法名之間。

TestGenericMethod.ArrayToArrayList(intArray, lst);
這個語法可以增加代碼的可讀性,另外可以在類型模糊的地方來指定泛型類型。

2.3 通配符

對於下面這行代碼

ArrayList
它會出現類型不兼容的錯誤,因為ArrayList不是一個ArrayList
跟上面一樣,第二行代碼會出現編譯錯誤,但是如果第二行代碼成功的話,就會出現另一個問題:任意的對象都可以添加到strList中,這又會引起類型不安全的問題。

對應上面問題,我們可以看到,如果希望寫一個方法printList(List<.>)來打印List的所有元素,如果我們定義方法為 printList(List

非受限的通配符

為了解決這個問題,泛型中引入了一個通配符(?),它代表任何未知類型,例如我們可以重寫上面的printList()方法,它可以接受任何未知類型的List。

public static void printList(List lst) {
  for (Object o : lst) System.out.println(o);
}

上限通配符 

通配符表示接受類型type以及它的子類,例如:

public static void printList(List lst) {
  for (Object o : lst) System.out.println(o);
}

List接受Number以及Number子類型的List,例如:List 和 List

很顯然,可以理解為,因為它可以接受任何對象類型。

下限通配符

跟上限通配符類似,表示接受的類型是type以及type的父類

2.4 受限泛型

在使用泛型的時候,我們也可以使用上面的限制來指定參數類型。例如:表示接收Number以及它的子類(例如:Integer 和 Double)

例子
下面方法add()中聲明了參數類型

public class MyMath {
   public static  double add(T first, T second) {
      return first.doubleValue() + second.doubleValue();
   }
 
   public static void main(String[] args) {
      System.out.println(add(55, 66));     // int -> Integer
      System.out.println(add(5.5f, 6.6f)); // float -> Float
      System.out.println(add(5.5, 6.6));   // double -> Double
   }
}

編譯器是如何對待受限泛型的呢?
上面我們說過,默認情況下,所有的泛型類型會被Object類型替換,但是對於受限類型會有些不同,例如中的泛型類型會被Number類型替換。

例如:

public class TestGenericsMethod {
   public static > T maximum(T x, T y) {
      return (x.compareTo(y) > 0) ? x : y;
   }
   
   public static void main(String[] args) {
      System.out.println(maximum(55, 66));
      System.out.println(maximum(6.6, 5.5));
      System.out.println(maximum("Monday", "Tuesday"));
   }
}

默認情況下,Object是所有參數類型的上限類型,但是在>中,它顯示的指定了上限類型為Comparable,因此編譯器會將參數類型轉換為Comparable類型。
public static Comparable maximum(Comparable x, Comparable y) {   // replace T by upper bound type Comparable
                                                                 // Compiler checks x, y are of the type Comparable
                                                                 // Compiler inserts a type-cast for the return value
   return (x.compareTo(y) > 0) ? x : y;
}

當方法被調用的時候,例如maximum(55, 66),基本數據類型int會被裝箱為Integer對象,然後就會被隱式的轉換為Comparable類型,編譯器會進行類型的檢查來確保類型安全,對於返回類型,它也會顯式的插入類型轉換操作。
(Comparable)maximum(55, 66);
(Comparable)maximum(6.6, 5.5);
(Comparable)maximum("Monday", "Tuesday");

我們不需要傳遞真實的類型參數給泛型方法,因為編譯器會根據傳入的參數自動的確定參數類型並進行類型的轉換。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved