程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 診斷Java代碼: 懸掛復合錯誤類型

診斷Java代碼: 懸掛復合錯誤類型

編輯:關於JAVA

空指針到處都有!

在一個 Java 程序員所能遇到的所有異常中,空指針異常屬於最恐怖的,這是因為:它是程序能給出的信息最少的異常。例如,不像一個類轉型異常,空指針異常不給出它所需要的內容的任何信息,只有一個空指針。此外,它並不指出在代碼的何處這個空指針被賦值。在許多空指針異常中,真正的錯誤出現在變量被賦為空值的地方。為了發現錯誤,我們必須通過控制流跟蹤,以發現變量在哪裡被賦值,並確定是否這麼做是不正確的。當賦值出現在包中,而不是出現在發生報錯的地方時,進程會被明顯地破壞。

許多 Java 開發人員告訴我,他們所遇到的絕大多數程序崩潰是空指針異常,並且他們渴望有一種工具,能在程序第一次運行前靜態地識別出這些錯誤。不幸的是,自動控制理論告訴我們,沒有工具可以靜態地決定哪些程序將拋出空指針異常。但是在一個程序中,用一個工具排除許多空指針異常是有可能的,留給我們僅僅一小部分需要我們必須人工檢查的潛在的問題所在。實際上,為了為 Java 程序(請參閱 參考資料)提供這樣一個工具,現在正做著一些研究。但是一個好的工具也只能為我們做這些。空指針異常將決不會被完全根除。當它們真的發生時,工具能幫我們弄清和它們相聯系的錯誤類型,這樣我們能快速診斷它們。另外,我們可以應用某些編程和設計技巧來顯著減少這些類型錯誤的出現。

懸掛復合類型

我們將探討的第一個關於空指針異常的錯誤類型,是一個我稱之為懸掛復合類型的錯誤類型。這種類型的錯誤是這樣產生的:定義的某些基本例沒有被給出它們自己的類,然後以這種方法定義了一個遞歸的數據類型。相反,空指針被插入到不同的復合數據類型中。數據類型實例的使用就好像空指針被正確填充了一樣。我稱之為懸掛復合類型是因為沖突代碼是復合設計類型的一個有缺點的應用程序,其中,復合數據類型包含懸掛的引用(也就是空指針)。

原因

考慮下面 LinkedList 類的單連接執行,它有一個懸掛復合類型。為了示例的簡單起見,我只執行在 java.util.LinkedList 中定義的一些方法。為了顯示這種類型的錯誤是多麼隱蔽,我已經在下面代碼中引入一個錯誤。看看你是否能發現它。

>清單 1. 單連接鏈表

import java.util.NoSuchElementException;
public class LinkedList {
  private Object first;
  private LinkedList rest;
  /**
  * Constructs an empty LinkedList.
  */
  public LinkedList() {
   this.first = null;
   this.rest = null;
  }
  /**
  * Constructs a LinkedList containing only the given element.
  */
  public LinkedList(Object _first) {
   this.first = _first;
   this.rest = null;
  }
  /**
  * Constructs a LinkedList consisting of the given Object followed by
  * all the elements in the given LinkedList.
  */
  public LinkedList(Object _first, LinkedList _rest) {
   this.first = _first;
   this.rest = _rest;
  }
}

這段代碼相當的糟糕。它在兩個域中都放置一個空指針來表示空鏈表,而不是為空鏈表定義一個單獨的類。一開始看來,用這種方法表示一個空鏈表使代碼簡單。畢竟,我們不必僅僅為了空鏈表而去定義一個額外的類。但是,正如我將證明的,這樣的簡單操作只是一個幻想。讓我們為這個類定義一些讀取器 (getter) 和設置器 (setter) 方法:

清單 2. 為 LinkedList 定義方法

public Object getFirst() {
   if (! (this.isEmpty())) {
    return this.first;
   }
   else {
    throw new NoSuchElementException();
   }
  }
  public LinkedList getRest() {

   if (! (this.isEmpty())) {
    return this.rest;
   }
   else {
    throw new NoSuchElementException();
   }
  }
  public void addFirst(Object o) {
   LinkedList oldThis = (LinkedList)this.clone();
   this.first = o;
   this.rest = oldThis;
  }
  public boolean isEmpty() {
   return this.first == null && this.rest == null;
  }
  private Object clone() {
   return new LinkedList(this.first, this.rest);
  }

注意,兩個讀取器采取的行動依賴於是否鏈表為空。這正好是那種一個正確構建的類層次所要防止的 if-then-else 鏈。由於這些鏈,我們不用在一個單一類型的鏈表上孤立地考慮這些讀取器。此外,如果在將來的某一天,我們需要第三種類型的鏈表(例如一個不可變的鏈表),我們將不得不重新編寫每一個方法的代碼。

但是真正簡單的方法是我們怎樣才能輕易地避免將錯誤引入到程序中。按照這種方法,清單 2 中的 LinkedList 的執行只能是可憐的失敗。實際上,就象我前面提到的,我們的 LinkedList 類已經包含一個微小的但有破壞性的錯誤(你發現了嗎?)。空鏈表的表示到底是什麼呢?我前面說過,空鏈表就是兩個域都包含一個空指針的 LinkedList 。實際上,零參數構造器就是建立一個這樣的空鏈表。但是注意單參數構造器 不是把空鏈表放入到 rest 域,這是構建一個只有一個值的鏈表所必須的。相反,它是用空指針替代。由於懸掛復合類型錯誤將空指針和基本例的位置標記符相混淆,象這樣的錯誤是很容易犯的。為了了解這錯誤怎樣表明自己是一個空指針異常,讓我們為清單寫一個 equals 方法:

清單 3. 哪裡錯了

public boolean equals(Object that) {
   // If the objects are not of the same class, then they are not equal.
   // Reflection is used in case this method is called from an instance of a
   // subclass. 
   if (this.getClass() == that.getClass()) {
   LinkedList _that = (LinkedList)that;
    if (this.isEmpty() || _that.isEmpty()) {
   return this.isEmpty() && _that.isEmpty();
    }
    else {
   boolean firstEltsMatch = this.getFirst().equals(_that.getFirst());
   boolean restEltsMatch = this.getRest().equals(_that.getRest());

   return firstEltsMatch && restEltsMatch;
    }
   }
   else {
    return false;
   }
  }

如果 this 和 that 都是非空,那麼 equals 方法可以正確地預計它能調用它們的 getFirst 和 getRest 而不出現錯誤信息。但是如果鏈表中的任意一個包含用單參數構造器建立的任何部分,那麼,在一個空鏈應該等待的地方,這個遞歸調用將最終表示為一個空指針。當它調用 getFirst 或 getRest 時,一個空指針異常就出現了。

一種觀點可能是簡單地直接把空鏈表表示成空指針,但是這個想法完全不可行的,因為在那時,不可能去掉鏈表的最後一個元素和在空鏈表中插入一個元素。

另一方面,可以照下面的方法重寫單參數構造器來修復錯誤:

清單 4. 修復錯誤

public LinkedList(Object _first) {
   this.first = _first;
   this.rest = new LinkedList();
  }

但是,象大多數的錯誤類型一樣,阻止它們的出現總比修補它們要好的多。修補錯誤使得代碼很容易被打斷,即使簡單的讀取器、設置器和 equals 方法都會變得龐大,這樣一個事實建議我們要采取一種更好的設計方法。

解決方法和預防措施

事實上有一個簡單的辦法來避免懸掛復合錯誤:給每個數據類型的基本例定義一個自己的類。我建議執行有著 LinkedList 類的鏈表,該類包含一個有 Empty 類或 Cons 類的域,而不是象我們前面做的那樣,單個執行鏈接鏈表。這些類執行一個公共接口,如圖 1 所示。

圖 1. Empty 和 Cons UML 示意圖

為了執行可變的方法,新的 LinkedList 類作為一個內部不可變的鏈表的容器,如清單 5 所示。這個步驟是必須的,因為真正的空鏈表沒有域可變,所以它們是不可變的。

清單 5. 每個基本例獲得自己的類

import java.util.NoSuchElementException;
public class LinkedList {
  private List value;
  /**
  * Constructs an empty LinkedList.
  */
  public LinkedList() { this.value = new Empty(); }
  /**
  * Constructs a LinkedList containing only the given element.
  */
  public LinkedList(Object _first) { this.value = new Cons(_first); }
  /**
  * Constructs a LinkedList consisting of the given Object followed by
  * all the elements in the given LinkedList.
  */
  public LinkedList(Object _first, LinkedList _rest) {
  this.value = new Cons(_first, _rest.value);
  }
  private LinkedList(List _value) { this.value = _value; }
  public Object getFirst() { return this.value.getFirst(); }
  public LinkedList getRest() { return new LinkedList(this.value.getRest()); }
  public void addFirst(Object o) { this.value = new Cons(o, this.value); }
  public boolean isEmpty() { return this.value instanceof Empty; }
  public boolean equals(Object that) {

   if (this.getClass() == that.getClass()) {
    // The above test guarantees that the cast to LinkedList will always
    // succeed.
    return this.value.equals(((LinkedList)that).value);
   }
   else {
    return false;
   }
  }
}

那時,執行一個不可變的鏈表是直截了當的,如清單 6 所示。

清單 6. 對節點作加法和乘法的方法

interface List {
  public Object getFirst();
  public List getRest();
}
class Empty implements List {
  public Object getFirst() { throw new NoSuchElementException(); }
  public List getRest() { throw new NoSuchElementException(); }
  public boolean equals(Object that) {
   return this.getClass() == that.getClass(); }
}
class Cons implements List {

  Object first;
  List rest;

  Cons(Object _first) {
   this.first = _first;
   this.rest = new Empty();
  }
  Cons(Object _first, List _rest) {
   this.first = _first;
   this.rest = _rest;
  }
  public Object getFirst() { return this.first; }
  public List getRest() { return this.rest; }
  public boolean equals(Object that) {
   if (this.getClass() == that.getClass()) {
    // The above test guarantees that the cast to Cons will always succeed.
    Cons _that = (Cons)that;
    boolean firstEltsMatch = this.getFirst().equals(_that.getFirst());
    boolean restEltsMatch = this.getRest().equals(_that.getRest());
    return firstEltsMatch && restEltsMatch;
   }
   else {
    return false;
   }
  }
}

每一個方法的邏輯現在相當簡單了。也請注意,雖然就如以前在單參數 Cons 構造器中一樣,它仍舊可能引入同樣的錯誤,但我們已經構造了一個顯式 Empty 類的事實使這種可能性大大減少。另外,任何阻斷我們的鏈表以及忽略檢查空例的代碼將返回一個 NoSuchElementException ,而不是那些沒什麼用的空指針異常。

這段代碼的一個簡單優化是對 Empty 類應用同一個設計類型,因為每一個 Empty 的實例都是同樣的。我省去了這個優化,因為它不能相應地消除空指針異常,並且使得代碼更復雜了一些。

總結

下面是這個星期的錯誤類型的分析:

類型:懸掛復合

症狀:使用遞規定義的數據類型的代碼報告一個空指針異常。

原因:定義的某些基本例沒有給出自己的類,然後以這種方法定義了遞歸數據類型。相反,空指針被插入到不同的復合數據類型。客戶端代碼對基本例處理不一致。

解決方法和預防措施:確保基本例的表示和檢查的一致性。為每個基本例給出一個自己的類。

我們這時還不能結束對空指針問題的討論。在下一個部分,我們將還要注視另外一個非常普遍的,也被證明是一個空指針異常的錯誤類型,以及怎樣識別它和避免它。

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