程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> java集合的缺點:類型未知

java集合的缺點:類型未知

編輯:關於JAVA

使用Java集合的“缺點”是在將對象置入一個集合時丟失了類型信息。之所以會發生這種情況,是由於當初編寫集合時,那個集合的程序員根本不知道用戶到底想把什麼類型置入集合。若指示某個集合只允許特定的類型,會妨礙它成為一個“常規用途”的工具,為用戶帶來麻煩。為解決這個問題,集合實際容納的是類型為Object的一些對象的句柄。這種類型當然代表Java中的所有對象,因為它是所有類的根。當然,也要注意這並不包括基本數據類型,因為它們並不是從“任何東西”繼承來的。這是一個很好的方案,只是不適用下述場合:
(1) 將一個對象句柄置入集合時,由於類型信息會被拋棄,所以任何類型的對象都可進入我們的集合——即便特別指示它只能容納特定類型的對象。舉個例子來說,雖然指示它只能容納貓,但事實上任何人都可以把一條狗扔進來。
(2) 由於類型信息不復存在,所以集合能肯定的唯一事情就是自己容納的是指向一個對象的句柄。正式使用它之前,必須對其進行造型,使其具有正確的類型。

值得欣慰的是,Java不允許人們濫用置入集合的對象。假如將一條狗扔進一個貓的集合,那麼仍會將集合內的所有東西都看作貓,所以在使用那條狗時會得到一個“違例”錯誤。在同樣的意義上,假若試圖將一條狗的句柄“造型”到一只貓,那麼運行期間仍會得到一個“違例”錯誤。
下面是個例子:
 

//: CatsAndDogs.java
// Simple collection example (Vector)
import java.util.*;

class Cat {
  private int catNumber;
  Cat(int i) {
    catNumber = i;
  }
  void print() {
    System.out.println("Cat #" + catNumber);
  }
}

class Dog {
  private int dogNumber;
  Dog(int i) {
    dogNumber = i;
  }
  void print() {
    System.out.println("Dog #" + dogNumber);
  }
}

public class CatsAndDogs {
  public static void main(String[] args) {
    Vector cats = new Vector();
    for(int i = 0; i < 7; i++)
      cats.addElement(new Cat(i));
    // Not a problem to add a dog to cats:
    cats.addElement(new Dog(7));
    for(int i = 0; i < cats.size(); i++)
      ((Cat)cats.elementAt(i)).print();
    // Dog is detected only at run-time
  }
} ///:~


可以看出,Vector的使用是非常簡單的:先創建一個,再用addElement()置入對象,以後用elementAt()取得那些對象(注意Vector有一個size()方法,可使我們知道已添加了多少個元素,以便防止誤超邊界,造成違例錯誤)。
Cat和Dog類都非常淺顯——除了都是“對象”之外,它們並無特別之處(倘若不明確指出從什麼類繼承,就默認為從Object繼承。所以我們不僅能用Vector方法將Cat對象置入這個集合,也能添加Dog對象,同時不會在編譯期和運行期得到任何出錯提示。用Vector方法elementAt()獲取原本認為是Cat的對象時,實際獲得的是指向一個Object的句柄,必須將那個對象造型為Cat。隨後,需要將整個表達式用括號封閉起來,在為Cat調用print()方法之前進行強制造型;否則就會出現一個語法錯誤。在運行期間,如果試圖將Dog對象造型為Cat,就會得到一個違例。
這些處理的意義都非常深遠。盡管顯得有些麻煩,但卻獲得了安全上的保證。我們從此再難偶然造成一些隱藏得深的錯誤。若程序的一個部分(或幾個部分)將對象插入一個集合,但我們只是通過一次違例在程序的某個部分發現一個錯誤的對象置入了集合,就必須找出插入錯誤的位置。當然,可通過檢查代碼達到這個目的,但這或許是最笨的調試工具。另一方面,我們可從一些標准化的集合類開始自己的編程。盡管它們在功能上存在一些不足,且顯得有些笨拙,但卻能保證沒有隱藏的錯誤。

1. 錯誤有時並不顯露出來
在某些情況下,程序似乎正確地工作,不造型回我們原來的類型。第一種情況是相當特殊的:String類從編譯器獲得了額外的幫助,使其能夠正常工作。只要編譯器期待的是一個String對象,但它沒有得到一個,就會自動調用在Object裡定義、並且能夠由任何Java類覆蓋的toString()方法。這個方法能生成滿足要求的String對象,然後在我們需要的時候使用。
因此,為了讓自己類的對象能顯示出來,要做的全部事情就是覆蓋toString()方法,如下例所示:
 

//: WorksAnyway.java
// In special cases, things just seem
// to work correctly.
import java.util.*;

class Mouse {
  private int mouseNumber;
  Mouse(int i) {
    mouseNumber = i;
  }
  // Magic method:
  public String toString() {
    return "This is Mouse #" + mouseNumber;
  }
  void print(String msg) {
    if(msg != null) System.out.println(msg);
    System.out.println(
      "Mouse number " + mouseNumber);
  }
}

class MouseTrap {
  static void caughtYa(Object m) {
    Mouse mouse = (Mouse)m; // Cast from Object
    mouse.print("Caught one!");
  }
}

public class WorksAnyway {
  public static void main(String[] args) {
    Vector mice = new Vector();
    for(int i = 0; i < 3; i++)
      mice.addElement(new Mouse(i));
    for(int i = 0; i < mice.size(); i++) {
      // No cast necessary, automatic call
      // to Object.toString():
      System.out.println(
        "Free mouse: " + mice.elementAt(i));
      MouseTrap.caughtYa(mice.elementAt(i));
    }
  }
} ///:~


可在Mouse裡看到對toString()的重定義代碼。在main()的第二個for循環中,可發現下述語句:

System.out.println("Free mouse: " +
mice.elementAt(i));

在“+”後,編譯器預期看到的是一個String對象。elementAt()生成了一個Object,所以為獲得希望的String,編譯器會默認調用toString()。但不幸的是,只有針對String才能得到象這樣的結果;其他任何類型都不會進行這樣的轉換。
隱藏造型的第二種方法已在Mousetrap裡得到了應用。caughtYa()方法接收的不是一個Mouse,而是一個Object。隨後再將其造型為一個Mouse。當然,這樣做是非常冒失的,因為通過接收一個Object,任何東西都可以傳遞給方法。然而,假若造型不正確——如果我們傳遞了錯誤的類型——就會在運行期間得到一個違例錯誤。這當然沒有在編譯期進行檢查好,但仍然能防止問題的發生。注意在使用這個方法時毋需進行造型:
MouseTrap.caughtYa(mice.elementAt(i));

2. 生成能自動判別類型的Vector
大家或許不想放棄剛才那個問題。一個更“健壯”的方案是用Vector創建一個新類,使其只接收我們指定的類型,也只生成我們希望的類型。如下所示:
 

//: GopherVector.java
// A type-conscious Vector
import java.util.*;

class Gopher {
  private int gopherNumber;
  Gopher(int i) {
    gopherNumber = i;
  }
  void print(String msg) {
    if(msg != null) System.out.println(msg);
    System.out.println(
      "Gopher number " + gopherNumber);
  }
}

class GopherTrap {
  static void caughtYa(Gopher g) {
    g.print("Caught one!");
  }
}

class GopherVector {
  private Vector v = new Vector();
  public void addElement(Gopher m) {
    v.addElement(m);
  }
  public Gopher elementAt(int index) {
    return (Gopher)v.elementAt(index);
  }
  public int size() { return v.size(); }
  public static void main(String[] args) {
    GopherVector gophers = new GopherVector();
    for(int i = 0; i < 3; i++)
      gophers.addElement(new Gopher(i));
    for(int i = 0; i < gophers.size(); i++)
      GopherTrap.caughtYa(gophers.elementAt(i));
  }
} ///:~


這前一個例子類似,只是新的GopherVector類有一個類型為Vector的private成員(從Vector繼承有些麻煩,理由稍後便知),而且方法也和Vector類似。然而,它不會接收和產生普通Object,只對Gopher對象感興趣。
由於GopherVector只接收一個Gopher(地鼠),所以假如我們使用:
gophers.addElement(new Pigeon());
就會在編譯期間獲得一條出錯消息。采用這種方式,盡管從編碼的角度看顯得更令人沉悶,但可以立即判斷出是否使用了正確的類型。
注意在使用elementAt()時不必進行造型——它肯定是一個Gopher。

3. 參數化類型
這類問題並不是孤立的——我們許多時候都要在其他類型的基礎上創建新類型。此時,在編譯期間擁有特定的類型信息是非常有幫助的。這便是“參數化類型”的概念。在C++中,它由語言通過“模板”獲得了直接支持。至少,Java保留了關鍵字generic,期望有一天能夠支持參數化類型。但我們現在無法確定這一天何時會來臨。

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