靜態類型 ― 多數程序員喜歡它或憎恨它。支持者誇耀說靜態類型讓他們寫出更干淨更可靠的代碼,沒有它們則做不到這麼好。批評者埋怨說靜態類型增加了程序的復雜性。
是的,靜態類型不是免費午餐;有時候,它們用起來很乏味。然而,如果我們主要關心的是使代碼沒有錯誤,那麼,總的說來,Java 編程還是擁有並使用靜態類型好些。為什麼?靜態類型檢查:
通過早期錯誤檢測,提高健壯性
通過在最佳的時候作所需的檢查,提高性能
彌補單元測試的缺點
我們來更仔細地分析這些原因,並看一看靜態類型檢查和結對編程(pair programming)混用。
通過早期檢測,提高健壯性
靜態類型檢查能提高程序健壯性。為什麼?因為它有助於盡快找到錯誤 ― 在程序運行前。這裡的邏輯捨此無它。錯誤越早被發現,問題診斷起來就越容易,也就只有越少的數據會被錯誤的計算毀壞。
在程序運行前就找到並診斷錯誤是理想狀態。這個優點使靜態類型檢查成為編程語言設計中的偉大成功,因為它是少數幾種能在程序運行前自動檢測程序中的錯誤的方法,而且它可在可接受的時間內完成這個任務。“可接受的時間”意思是與程序長度呈線性關系的時間(有一個很小的常數系數),而不是其它形式自動檢查所要求的呈立方甚至呈指數關系的時間(許多甚至根本不保證完成)。
是的,類型系統越強大,編起程序來就越容易(且系統將檢測越多的錯誤)。我不否認 Java 簡單的類型系統留有很多缺憾;它經常阻礙我們,迫使我們用強制轉型來繞過。但這種狀況正在慢慢改善。
Sun JSR14 編譯器在語言中增加了形式有限的泛型(也稱為 參數化)類型;我們相信它遲早會被加入該語言,因為它目前在 Java 社區過程中得到了堅定的支持。更高級的語言擴展(例如 NextGen)承諾在 JSR14 提供的增加的表達力上更上一層樓。那是好事,因為在很多環境下 NextGen 都有助於減少甚至在 JSR14 中也是需要的一些增加的復雜性。請參閱 參考資料找到關於這個問題的更多信息。(除了 JSR14 鏈接,還有些關於 參數的多態性的文章。)
然而,靜態類型檢查的好處不僅僅是健壯性。它還能保護您的程序的性能。
通過減少所需的檢查,提高性能
在安全的語言中(“安全”的意思是指不允許我們破壞它自己的抽象的語言),對傳給方法的參數的類型作各種檢查是必需的並一定得完成,對被存取的域的類型的檢查也是必需的並一定得完成。如果這些檢查不是靜態地完成,那麼它們必須在運行時完成。
進行這些所需的檢查是費時的,在運行時進行這些檢查的語言,其性能會相應受損。當不變量被靜態地檢查時,我們不必在運行時檢查它,從而加快程序運行。所以,靜態類型檢查使我們得以寫出更健壯更高效的代碼。
傳統上認為編譯時靜態類型檢查是低效的。對於用 C/C++ 之類語言寫的大程序來說,在文件間鏈接各種類型引用是很費時的,因為每次編譯時,各種文件必須被合在一起生成一個大的可執行文件。但是 Java 語言完全避免了這個問題,因為類是分開編譯的,在需要時裝入到 JVM。沒有必要把所有的引用文件鏈接成一個可執行文件 ― 所以在編譯時沒有相應的放慢。
現在,我們對那些聲稱靜態類型在單元測試環境下是不必要的人說些什麼呢?
突破單元測試的限制
本專欄的老讀者知道,我是單元測試的堅定支持者。對您的程序全部進行單元測試是最好的作法。然而,我首先要承認單元測試的局限性。
單元測試僅能測試程序在某次特定運行時輸入某些特定數據時的行為。是的,在那次運行的狹小環境下,我們可以測試程序的 深層屬性。
相對而言,類型檢查檢查 淺層屬性,但它針對的是該程序所有可能的運行和各種可能的輸入。
結合類型檢查和單元測試
正如指定程序行為時,素材和單元測試互相補充一樣,在確定和排除錯誤時,單元測試和靜態類型檢查互相補充。這兩種排錯方法的結合效果大於它們各自的效果之和。
有些設計師和程序員會提出,成熟的程序中發生的錯誤種類比靜態類型檢查能發現的要深得多;所以,他們的結論是靜態類型系統的弊大於利。無疑,靜態類型語言使程序更冗長,甚至阻止我們寫出一些從不引起任何錯誤的程序。
總是有折衷。使用靜態類型並不是免費的午餐。我們用靜態類型語言寫的程序常常要比我們不使用類型系統寫的程序更復雜。但是,甚至“成熟”程序也會有那種淺層錯誤,成為靜態類型檢查的特別獵物。
即使程序中的這些淺層錯誤被消滅,重構也很容易就會重新產生它們。如果我們打算采納不斷重構的極端編程思想,我們將在這樣的淺層錯誤被引入後盡快地捕獲它們。(相反地,單元測試有助於捕獲重構時發生的更深層錯誤。這兩個概念的整體之和大於它們的部分之和。)在極端編程的環境下,靜態類型檢查效果不錯。
類型檢查和單元測試的矛盾
盡管如此,靜態類型檢查和單元測試之間仍有一個須提到的矛盾。極端編程要求我們把單元測試的編寫和代碼的編寫交叉起來以實現那些測試。
每組單元測試有助於指定功能性的一個新方面,應寫在允許我們通過那些測試的代碼之前。在理想情況下,我們要在寫完它們後馬上編譯那些測試,這樣我們可以確保它們已准備就緒。
但這裡有一個問題: 在我們定義測試所引用的類和方法前,新測試不能通過靜態類型檢查。這些類和方法可以是我們以後填充的空架子,但是除非我們有點東西,否則靜態檢查器會認為在測試中引用它們是沒意義的。
考慮清單 1 中比較簡單的示例,它顯示了一個多集合(multi-set)的實現的測試類(一個抽象的數據結構):
清單 1. 一個多集合的實現的測試類
import junit.framework.*;
import java.io.*;
/**
* A test class for MultiSet.
*
*/
public class MultiSetTest extends TestCase {
private static String W = "w";
private static String X = "x";
private static String Y = "y";
private static String Z = "z";
private static MultiSet<String> EMPTY = new MultiSet<String>();
private static MultiSet<String> XY = new MultiSet<String>(X, Y);
private static MultiSet<String> YZ = new MultiSet<String>(Y, Z);
private static MultiSet<String> XYZ = new MultiSet<String>(X, Y, Z);
private static MultiSet<String> XYY = new MultiSet<String>(X, Y, Y);
private static MultiSet<String> WXY = new MultiSet<String>(W, X, Y);
/**
* Constructor.
* @param String name
*/
public MultiSetTest(String name) {
super(name);
}
/**
* Creates a test suite for JUnit to run.
* @return a test suite based on the methods in this class
*/
public static Test suite() {
return new TestSuite(MultiSetTest.class);
}
private void _assertOrder(MultiSet set, String key, int value) {
assertEquals("order for key " + key, value, set.order(key));
}
public void testEmpty() {
_assertOrder(EMPTY, X, 0);
_assertOrder(EMPTY, Y, 0);
_assertOrder(EMPTY, Z, 0);
}
public void testOrder() {
_assertOrder(XY, X, 1);
_assertOrder(XY, Y, 1);
_assertOrder(YZ, Y, 1);
_assertOrder(YZ, Z, 1);
}
public void testAdd() {
MultiSet added = XY.add(YZ);
_assertOrder(added, X, 1);
_assertOrder(added, Y, 2);
_assertOrder(added, Z, 1);
}
public void testSubset() {
assertTrue(XY.subset(XYZ));
assertTrue(YZ.subset(XYZ));
assertTrue(! YZ.subset(XY));
assertTrue(! XY.subset(YZ));
assertTrue(! XYZ.subset(XY));
assertTrue(! XYZ.subset(YZ));
assertTrue(! XYY.subset(XYZ));
assertTrue(! XYZ.subset(XYY));
}
public void testSubtract() {
MultiSet XYYZ = XY.add(YZ);
assertEquals(YZ, XYYZ.subtract(WXY));
assertEquals(YZ, XYYZ.subtract(XY));
assertEquals(XY, XYYZ.subtract(YZ));
assertEquals(EMPTY, EMPTY.subtract(YZ));
assertEquals(EMPTY, YZ.subtract(YZ));
}
public void testUnion() {
assertEquals(XYZ, XY.union(YZ));
}
public void testIsEmpty() {
assertTrue(EMPTY.isEmpty());
assertTrue(! XY.isEmpty());
}
}
類 MultiSet 在哪?還有方法 union() 、 isEmpty() 等等是怎麼樣的?
類型檢查器不會比您更清楚這些類和方法的位置,所以這些代碼在我的環境中可以編譯,但不能在您的環境中編譯。也就是說,這些代碼要到您實現類 MultiSet 及所有適當的方法後才能編譯。請記住,在靜態類型語言中,要直到您至少為您試圖測試的類和方法生成空架子之後,您才能編譯新的單元測試。
面向測試的開發工具的使用能容易地緩和這個矛盾。具體地說,您需要一個這樣的開發工具,它能讀完一個單元測試,累計該測試通過靜態類型檢查所必需的類和方法引用(及適當的簽名),然後生成空架子類。
如果您考慮一下這樣一個開發工具會設計成什麼樣子,很顯然,面向測試的開發工具的規劃與靜態類型檢查器非常像,除了它只是累計記錄它所需生成的空架子而不是生成錯誤。我們目前正在為 NextGen 實現靜態檢查器,它有一個正是做這件事的“空架子生成”模式。
結對編程:另一淺層錯誤檢查
靜態類型檢查具有對淺層但普遍的錯誤的檢測能力,對這一能力的另一補充是 結對編程,它是極端編程的原則之一。多個聰明的人互相檢查工作是消滅許多淺層錯誤的好方法。
另一獲得這種效果的有效方法是開放源代碼編碼。當代碼被開放後,代碼常會更健壯 ― 畢竟,有不止一對程序員的兩雙眼睛在查看代碼並尋找最微小的“gotchas”問題。正如 Eric Raymond 在他著名的“The Cathedral and the Bazaar”所說(他把它稱為 Linus 定律),“如果有足夠多的眼球,所有的錯誤都是淺層的”。
超越簡單的類型檢查
當然,靜態類型檢查是有益的,出於同樣的理由,更高級形式的靜態檢查也是有益的。術語“靜態檢查”和“靜態分析”是比僅僅檢查類型更廣泛的概念 ― 它們指任何為確定程序在運行時的行為而分析程序文本的機制。
正如其他小組已證明的那樣,可以擴展 Java 語言,讓它包括其它形式的靜態檢查,例如斷言(assertion)的有限靜態驗證。今後工作的另一方向是在 Java 語言上加入各種“軟類型化系統(soft typing system)”,在軟類型化系統中,像強制轉型這樣的運算在某些環境中可以被成功驗證,但並不禁止未經驗證的強制轉型。
在排錯時,我們應該用所有的武器來解決問題,開發新的有效的靜態檢查系統,以檢查盡可能多的不變量。在以後的幾篇文章裡,我們將探討一些可用於 Java 編程的靜態分析工具,既作為原型工具,也作為生產工具。