15泛型_15.2簡單泛型

15.2 簡單泛型

有許多原因促成了泛型的出現,而最引人往目的一個原因,就是爲了創造容器類。容器,就是存放要使用的對象的地方.數組也是如此,不過與簡單的數組相比,容器類更加靈活,具備更多不同的功能。事實上,所有的程序,在運行時都要求你持有一大堆對象,所以,容器類算得上最具重用性的類庫之一。

我們先來看看一個只能持有單個對象的類。當然了,這個類可以明確指定其持有的對象的類型:

//: generics/Holder1.java

class Automobile {}

public class Holder1 {
  private Automobile a;
  public Holder1(Automobile a) { this.a = a; }
  Automobile get() { return a; }
} ///:~

不過,這個類的可重用性就不怎麼樣了,它無法特有其他類型的任何對象。我們可不希望爲碰到的每個類型都編寫一個新的類。
在Javaa SE5之前,我們可以讓這個類直接持有Object類型的對象:

//: generics/Holder2.java

public class Holder2 {
  private Object a;
  public Holder2(Object a) { this.a = a; }
  public void set(Object a) { this.a = a; }
  public Object get() { return a; }
  public static void main(String[] args) {
    Holder2 h2 = new Holder2(new Automobile());
    Automobile a = (Automobile)h2.get();
    h2.set("Not an Automobile");
    String s = (String)h2.get();
    h2.set(1); // Autoboxes to Integer
    Integer x = (Integer)h2.get();
  }
} ///:~

現在,Holder2可以存儲任何類型的對象,在這個例子中,只用了一個Hlolder2對象,卻先後三次存儲了三種不同類型的對象。

有些情況下,我們確實希望容器能夠同時持有多種類型的對象。但是,通常而言,我們只會使用容器來存儲一種類型的對象。泛型的主要目的之一就是用來指定容器要持有什麼類型的對象。而且由編譯器來保證類型的正確性。

因此,與其使用Object,我們更喜歡暫時不指定類型,而是稍後再決定具體使用什麼類型。要達到這個目的,需要使用類型參數,用尖括號括住,放在類名後面。然後在使用這個類的時候,再用實際的類型替換此類型參數。在下面的例子中,T就是類型參數:

//: generics/Holder3.java

public class Holder3<T> {
  private T a;
  public Holder3(T a) { this.a = a; }
  public void set(T a) { this.a = a; }
  public T get() { return a; }
  public static void main(String[] args) {
    Holder3<Automobile> h3 =
      new Holder3<Automobile>(new Automobile());
    Automobile a = h3.get(); // No cast needed
    // h3.set("Not an Automobile"); // Error
    // h3.set(1); // Error
  }
} ///:~

現在,當你創建Holder3對象時,必須指明想持有什麼類型的對象,將其置於尖括號內。就像main()中那樣。然後,你就只能在Holder3中存入該類型(或其子類,因爲多態與泛型不衝突)的對象了。並且,在你從Holder3中取出它持有的對象時,自動地就是正確的類型。
這就是Java泛型的核心概念:告訴編譯器想使用什麼類型,然後編譯器幫你處理一切細節。

一般而言,你可以認爲泛型與其他的類型差不多,只不過它們碰巧有類型參數罷了。稍後我們會看到,在使用泛型時,我們只需指定它們的名稱以及類型參數列表即可。

15.2.1 一個元組類庫

僅一次方法調用就能返回多個對象,你應該經常需要這樣的功能吧。可是return語句只允許返回單個對象,因此,解決辦法就是創建一個對象,用它來持有想要返回的多個對象。當然,可以在每次需要的時候,專門創建一個類來完成這樣的工作。可是有了泛型。我們就能夠一次性地解決該問題,以後再也不用在這個問題上浪費時間了。同時,我們在編譯期就能確保類型安全。

這個概念稱爲元組(tuple),它是將一組對象直接打包存儲幹其中的一個單一對象。這個容器對象允許談取其中元素,但是不允許向其中存放新的對象。(這個概念也稱爲數據傳這對象或信使。)

通常,元組可以具有任意長度,同時,元組中的對象可以是任意不同的類型。不過,我們希望能夠爲每一個對象指明其類型,並且從容器中讀取出來時,能夠得到正確的類型。要處理不同長度的問題,我們需要創建多個不同的元組。下面的程序是一個2維元組,它能夠持有兩個對象:

//: net/mindview/util/TwoTuple.java
package net.mindview.util;

public class TwoTuple<A,B> {
  public final A first;
  public final B second;
  public TwoTuple(A a, B b) { first = a; second = b; }
  public String toString() {
    return "(" + first + ", " + second + ")";
  }
} ///:~

構造器捕獲了要存儲的對象,而toString()是一個便利函數,用來顯示列表中的值。注意,元組隱含地保持了其中元素的次序。

第一次閱讀上面的代碼時,你也許會想,這不是違反了Java編程的安全性原則嗎?first和second應該聲明爲private,然後提供getFirst()和getSecond()之類的訪問方法纔對呀?讓我們仔細看看這個例子中的安全性:客戶端程序可以讀取first和second對象,然後可以隨心所欲地使用這兩個對象。但是,它們卻無法將其他值賦予first或second。因爲final聲明爲你買了相同的安全保險,而且這種格式更簡潔明瞭。

還有另一種設計考慮,即你確實希望允許客戶端程序員改變first辦second所引用的對象。然而,採用以上的形式無疑是更安全的做法,這樣的話,如果程序員想要使用具有不同元素的元組,就強制要求他們另外創建一個新的TwoTuple對象。

我們可以利用繼承機制實現長度更長的元組。從下面的例子中可以看到,增加類型參數是件很簡單的事情:

//: net/mindview/util/ThreeTuple.java
package net.mindview.util;

public class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
  public final C third;
  public ThreeTuple(A a, B b, C c) {
    super(a, b);
    third = c;
  }
  public String toString() {
    return "(" + first + ", " + second + ", " + third +")";
  }
} ///:~

爲了使用元組,你只需定義一個長度適合的元組,將其作爲方法的返回值,然後在return語句中創建該元組,井返回即可。

//: generics/TupleTest.java
import net.mindview.util.*;

class Amphibian {}
class Vehicle {}

public class TupleTest {
  static TwoTuple<String,Integer> f() {
    // Autoboxing converts the int to Integer:
    return new TwoTuple<String,Integer>("hi", 47);
  }
  static ThreeTuple<Amphibian,String,Integer> g() {
    return new ThreeTuple<Amphibian, String, Integer>(
      new Amphibian(), "hi", 47);
  }
  static
  FourTuple<Vehicle,Amphibian,String,Integer> h() {
    return
      new FourTuple<Vehicle,Amphibian,String,Integer>(
        new Vehicle(), new Amphibian(), "hi", 47);
  }
  static
  FiveTuple<Vehicle,Amphibian,String,Integer,Double> k() {
    return new
      FiveTuple<Vehicle,Amphibian,String,Integer,Double>(
        new Vehicle(), new Amphibian(), "hi", 47, 11.1);
  }
  public static void main(String[] args) {
    TwoTuple<String,Integer> ttsi = f();
    System.out.println(ttsi);
    // ttsi.first = "there"; // Compile error: final
    System.out.println(g());
    System.out.println(h());
    System.out.println(k());
  }
} /* Output: (80% match)
(hi, 47)
(Amphibian@1f6a7b9, hi, 47)
(Vehicle@35ce36, Amphibian@757aef, hi, 47)
(Vehicle@9cab16, Amphibian@1a46e30, hi, 47, 11.1)
*///:~

由於有了泛型,你可以很容易地創建元組,令其返回一組任意類型的對象。而你所要做的,只是編寫表達式而已。

通過ttsi.first=“there”語句的錯誤,我們可以看出,final明確實能夠保護public元素,在對象被構造出來之後,聲明爲final的元素便不能被再賦予其他值了。

在上面的程序中,new表達式確實有點羅嗦。本章稍後會介紹,如何利用泛型方法簡化這樣的表達式。

15.2.2 一個堆棧類

接下來我們看一個稍微複雜一點的例子:傳統的下推堆棧。在第11章中,我們看到,這個堆棧是作爲net.mindview.util.Stack類,用一個LinkedList實現的。在那個例子中,LinkedList本身已經具備了創建堆棧所必需的方法,而Stack也可以通過兩個泛型的類Stack<T>LinkedList<T>的組合來創建。在那個示例中,我們可以看出,泛型類型也就是另一種類型罷了(稍候我們會一些例外的情況)。

現在我們不用LinkedList,來實現自己的內部鏈式存儲機制:

public class LinkedStack<T> {
  private static class Node<U> {
    U item;
    Node<U> next;
    Node() { item = null; next = null; }
    Node(U item, Node<U> next) {
      this.item = item;
      this.next = next;
    }
    boolean end() { return item == null && next == null; }
  }
  private Node<T> top = new Node<T>(); // End sentinel
  public void push(T item) {
    top = new Node<T>(item, top);
  }    
  public T pop() {
    T result = top.item;
    if(!top.end())
      top = top.next;
    return result;
  }
  public static void main(String[] args) {
    LinkedStack<String> lss = new LinkedStack<String>();
    for(String s : "Phasers on stun!".split(" "))
      lss.push(s);
    String s;
    while((s = lss.pop()) != null)
      System.out.println(s);
  }
}

/* Output:
stun!
on
Phasers
*/

內部類Node也是一個泛型,它擁有自己的類型參數。

這個例子使用了一個末端哨兵(end sentinel)來判斷堆棧何時爲空。這個末端哨兵是在構造LinkedStack時創建的。然後,每調用一次push()方法,就會創建一個Node<T>對象,並將其鏈接到前一個Node<T>對象。當你調用pop()方法時,總是返top.item,然後丟棄當前top所指的Node<T>,並將top轉移到下一個Node<T>,除非你已經碰到了末端哨兵,這時候就不再移動top了。如果已經到了末端,客戶端程序還繼續調用pop()方法,它只能得到null,說明堆棧已經空了。

15.2.3 RandomList

作爲容器的另一個例子,假設我們需要一個持有特定類型對象的列表,每次調用其上的select()方法時,它可以隨機地選取一個元素。如果我們希望以此構建一個可以應用幹各種類型的對象的工具,就需要使用泛型:

//: generics/RandomList.java
import java.util.*;

public class RandomList<T> {
  private ArrayList<T> storage = new ArrayList<T>();
  private Random rand = new Random(47);
  public void add(T item) { storage.add(item); }
  public T select() {
    return storage.get(rand.nextInt(storage.size()));
  }
  public static void main(String[] args) {
    RandomList<String> rs = new RandomList<String>();
    for(String s: ("The quick brown fox jumped over " +
        "the lazy brown dog").split(" "))
      rs.add(s);
    for(int i = 0; i < 11; i++)
      System.out.print(rs.select() + " ");
  }
} /* Output:
brown over fox quick quick dog brown The brown lazy brown
*///:~
發佈了39 篇原創文章 · 獲贊 3 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章