程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 跨越邊界:淺談Java模型以外的類型策略

跨越邊界:淺談Java模型以外的類型策略

編輯:關於JAVA

當談到 Java 語言的類型方法時,Java 社區分為兩大陣營。一些人喜歡編譯時錯誤檢查,更好的安全 性,以及改善的工具 —— 這些都是靜態類型所能提供的特性。而另一些人則偏愛更動態的類型體驗。這 一次在 跨越邊界 中,您將看到兩種高生產力的非 Java 語言所使用的一些截然不同的類型策略,並發現 在 Java 編程中提高類型靈活性的一些方法。

在對任何編程語言的討論中,爭議較大的一個問題就是類型模型。類型決定可以使用哪些種類的工具 ,並影響到應用程序的設計。很多開發人員將類型與生產率或可維護性聯系起來(我就是其中的一個)。 典型的 Java 開發人員通常都特別樂於維護 Java 語言的類型模型的地位,強調 Java 語言可采用更好的 開發工具,在編譯時捕捉某些種類的 bug(例如類型不兼容和拼寫錯誤),以及性能等方面的優勢。

如果您想理解一種新的編程語言,甚至一系列語言,那麼通常應該從類型策略著手。在本文中,您將 看到 Java 之外的一些語言中的類型模型。我首先簡要介紹任何語言設計者在類型模型中必須考慮的一些 決策,著重介紹靜態類型和動態類型的一些不同的決策。我將展示一些不同極端的例子 —— Objective Caml 中的靜態類型和 Ruby 中的動態類型。我還將談到 Java 語言的類型限制,以及如何突破 Java 類 型的限制快速編程。

類型策略

至少可以從三個角度來看待類型:

靜態類型還是動態類型,這取決於何時 實施類型模型。靜態類型語言在編譯時實施類型。而動態類型 語言通常基於一個對象的特征在運行時實施類型。

強類型還是弱類型,這取決於如何 實施類型模型。強類型嚴格地實施類型,如果發現有違反類型規則 的情況,則會拋出運行時或編譯時錯誤。而弱類型則留有更多的余地。極端情況下,弱類型語言(例如 Assembler)允許將任意數據類型賦給另一種類型(不管這種賦值是否有意義)。靜態類型的語言既可以 有強類型,也可以有弱類型;而動態類型系統通常是強類型的,但也不完全是。

顯式類型還是隱式類型,這取決於語言如何確定一個給定對象的類型。顯式類型語言要求聲明每個變 量和每個函數參數。而隱式類型語言則根據語言中的語法和結構線索來確定對象的類型。靜態類型語言通 常是顯式類型的,但也不完全是;而動態類型語言幾乎都是隱式類型的。

下面兩個例子很好地闡釋了其中兩個角度的內涵。假設您編譯下面這段 Java 代碼:

class Test {
public static void test(int i) {
String s = i;
}
}

會收到如下錯誤消息:

Test.java:3: incompatible types
found : int
required: java.lang.String
String s = i;
^
1 error

執行以下 Ruby 代碼:

1 + "hello"

會收到以下錯誤消息:

TypeError: String can't be coerced into Fixnum
from (irb):3:in '+'
from (irb):3

這兩種語言都傾向於強類型,因為當您試圖使用一個它們期望之外的類型結構的對象時,它們都會拋 出錯誤消息。Java 類型策略在編譯時給出錯誤消息,因為它執行靜態類型檢查。而 Ruby 則是在運行時 給出錯誤消息,因為 Ruby 支持動態類型。換句話說,Java 在編譯時將對象綁定到類型。而 Ruby 則是 在運行時每當更改對象的時候將對象綁定到類型。由於我是在 Java 代碼中,而不是在 Ruby 中聲明變量 的,因此可以看到 Java 語言的顯式類型與 Ruby 的隱式類型的工作方式不同。

在這三個角度中,靜態類型與動態類型對於語言的特征有最大的影響,因此接下來我將重點解釋這兩 種策略各自的優點。

靜態類型的優點

在靜態類型語言中,程序員(通過聲明或根據約定)或編譯器(根據結構和語法線索)將一種類型指 定給一個變量,然後那個類型就不會改變。靜態類型通常需要額外的成本,因為靜態類型語言(例如 Java 語言)通常是顯式類型的。這意味著必須聲明所有的變量,然後編譯代碼。成本也伴隨著收益:早 期的錯誤檢測。靜態類型在最基層為編譯器提供多得多的信息。更多信息所帶來的好處就是,可以更早地 捕捉到某些類型的錯誤,而動態類型語言只有到運行時才能檢測到這些錯誤。如果您一直等到運行時才捕 捉這些 bug,那麼其中一些將進入生產環境。也許這正是動態類型語言受到最多指責的一個方面。

另一種觀點則認為現代軟件開發團隊通常會運行自動測試,動態語言的支持者聲稱,即使是最簡單的 自動測試也可以捕捉到大多數的類型錯誤。而動態語言的支持者所能提供的對編譯時錯誤檢測不利的最好 論據是,早期檢測所帶來的好處相對於成本來說是得不償失的,因為不管是否使用動態類型,最終都要進 行測試。

一種有趣的折中方法是在靜態類型語言中使用隱式類型,從而減少類型的成本。開放源代碼語言 Objective Caml (OCaml) 是靜態類型語言 Lisp 的衍生物,它既能提供很好的性能,又不會犧牲生產率 。OCaml 使用隱式類型,因此可以編寫下面這樣的采用靜態類型的代碼:

# let x = 4+7

OCaml 返回:

val x : int = 11

根據表達式中的語法線索,OCaml 推斷出 x 的類型。4 是 int 型,7 也是 int 型,因此 x 也必定 是 int 型。隱式類型語言可以擁有 Java 語言所具有的所有類型安全性,甚至更多。不同之處在於您需 要提供的信息量,以及在閱讀程序時可用的信息量。很多喜歡靜態類型的人更偏愛隱式類型。他們寧願讓 編譯器來做這種事情,而不願意被迫重復地在代碼中輸入變量的類型。

隱式類型系統一個較大的優點是,不需要為函數的參數聲明類型,因為編譯器會從傳入的值推斷出參 數的類型。因此同一個方法可以有多種用途。

關於重構的謬誤

有些人認為,要獲得具有重構(refactoring)支持的良好 IDE,就必須具有靜態類型特性,這種觀點 是荒謬的。大多數現代 IDE 多少從早期的 Smalltalk IDE 中吸收了一些東西。實際上,Eclipse 早期也 有一些基本的東西是 Visual Age for Java 中的,而後者最初就是被發布在 Smalltalk 虛擬機上! Smalltalk Refactoring Browser 仍然是如今可用的功能最完善的重構工具之一(見參考資料)。Java 語言仍然比大多數流行的動態語言(除了 Smalltalk)具有更好的工具,靜態類型是最大的原因。

並不是只有編譯器才能利用靜態類型所提供的附加信息。IDE 可以通過靜態類型為重構提供更好的支 持。幾年前,一種革命性的思想改變了開發環境的工作方式。在 IDEA 和 Eclipse 中,您的代碼看上去 像一個文本視圖,但是開發環境實際上正在編輯 Abstract Syntax Tree (AST)。因此,當需要重新命名 一個方法或者類的時候,開發環境很容易通過在 AST 中精確定位找到方法或類被引用的每個地方。如今 ,如果沒有通過靜態類型簡化的優秀的重構,我們很難想像用 Java 語言編程。在我探索 Ruby 的時候, 相對於其他任何工具或特性,我更懷念 IDEA。

靜態類型還有其他一些優點,在這裡我不會詳細描述。靜態類型可以提供更好的安全性,而且顯然還 可以提高代碼的可讀性。靜態類型還可以提供更多的信息,使得編譯器更容易進行優化,從而提高性能。 但是靜態類型贏得開發人員青睐的最大原因是更容易檢測錯誤,而且有更多可用的工具。

動態類型的優點

Ruby 專家 Dave Thomas 將動態類型稱作duck typing(見參考資料),這有兩層意思。第一層意思是 說,這種語言不真正實現類型 —— 它利用鴨子理論 解決這個問題。第二層意思是說,如果什麼東西走 起來像鴨子,叫起來也像鴨子,那麼它很可能就是一只鴨子。在編程語言的上下文中,duck typing 意味 著如果一個對象對於某種類型的方法有反應,那麼事實上就可以把它看作那種類型。這樣的特性可以導致 一些有趣的優化。

大多數偏愛動態類型的開發人員除了強調早期錯誤檢測會帶來不必要的成本外,還提到動態類型語言 具有很好的可表達性和生產率。很簡單,您通常可以用更少的關鍵詞表達更多的思想。作為一名新的 Ruby 擁護者,我深信動態語言更能提高生產率,雖然我不能比常見的靜態語言的支持者拿出更多具體的 證據來。但是,從我開始編寫更多的 Ruby 代碼起,我就感覺到自己的生產率有了明顯的提高。誠然,我 仍然會看到靜態類型的優點,尤其是在工具集方面,但是我逐漸認識到了靜態類型的缺點。 當我開始用 Ruby 編寫代碼時,我受到的最大改變是產生和使用元編程結構的能力。如果您從頭開始一直關注跨越邊 界 系列統的話,您就知道元編程,或者說編寫用於編寫程序的程序,是 Ruby on Rails 的一大推動力量 ,更一般地說,是特定於領域的語言的一大推動力量。用 Ruby 編程時,我通常會編寫更大的構建塊,或 者用更大的塊進行構建。我發現,與使用 Java 編程相比,我可以用更多類型的可重用塊擴展我的程序。 就像在 Java 編程中,您可以用新的類來擴展程序。還可以添加方法和數據到已有的類中,因為類是開放 式的。您可以使用 mix-in(後面 運行時綁定 會講到)來添加核心功能到已有的類中。還可以在任何時 候根據需要改變一個對象的定義。我還是一名 Ruby 編程新手,這些功能用到的不多,但是當我真正開始 使用它們時,結果令人大吃一驚。

例如,為了添加一個攔截器,只需重新命名一個方法,並為原有的方法創建一個新的實現。為了攔截 new,可以編寫以下代碼:

class Class
alias_method :old_new, :new
def new(*args)
puts "Intercepted new" #do interception work here
old_new(*args)
end
end

您不需要 AspectJ 庫、字節碼增強或一大堆的庫。您可以直接編寫所需的攔截器。

動態類型在原始代碼行方面也可以節省精力。由於動態語言幾乎都是類型推斷式的,所以您不需要花 多大力氣來表達基本思想。變量無需聲明即可直接使用。您也不必表達參數類型的所有可能排列,只需輸 入一組名稱。您的代碼可以更加具有多態性 —— 任何對一種類型的方法有反應的對象都可以看作這種類 型 —— 所以通常可以比其他語言更精簡地表達思想。代碼中的耦合也可以變得更松散。當您想改變某個 東西的類型時,這種變化所波及的范圍很有限,所以不需要在更多的地方作出相應的更改。

安全性還是靈活性

從某種意義上說,語言的靜態與動態之爭的關鍵在於安全性與靈活性之間的取捨。靜態語言的支持者 相信更安全的語言更好。而動態語言的支持者卻不願意為安全性付出任何代價。對於他們而言,對一種語 言的衡量標准在於能多快地表達思想,目標在於最大化程序員的效率。而另一方面,靜態語言專家則說, 如果能 在早期捕捉到 bug,那麼就應該 這麼做,而且工具可以彌補語言中的限制。

生產率提高的最後一個原因是減少了編譯環節。很多動態類型語言是解釋性的,所以在編寫程序後可 以立即看到變化。即使沒有慣用的調試器,在 Ruby 中探索庫和應用程序代碼的行為也更為容易,因為您 可以打開一個解釋器,通常可以直接在調試會話中打開,然後隨意探索。

但是……

然而,編譯不只是支持靜態類型。靜態類型的支持者還認為可以獲得更好的性能。很多靜態語言,例 如 Java 代碼、C 和 C++,都被稱作系統語言,因為它們是構建操作系統、設備驅動程序和其他高性能系 統代碼的最常用的語言。這又經常導致動態語言的支持者指責靜態語言總是太低級,用它們來編寫應用程 序生產率很低 —— 但那是一種很狹隘的觀點。OCaml 語言是一種很高級的語言,支持面向對象程序設計 、函數式程序設計(如 Lisp 或 Erlang)或傳統的結構化程序設計。其類型模型是靜態的,很多人說它 的性能甚至比 C++ 的性能還好(參見 參考資料)。使用 OCaml 時,靜態類型導致的開銷很小,因為這 種語言是類型推斷式的。雖然付出了這一點成本,但可以得到非常好的性能,編譯時類型檢查,以及一個 非常高級的語言。即使是 duck typing 最頑固的支持者也不得不承認那些優點。

Java 語言中的類型限制

Java 開發人員充分利用靜態類型。他們有最好的開發工具,這些工具帶有代碼完成和重構等功能,這 些都傾向於靜態類型。現在開始利用測試優先開發的很多 Java 程序員獲得了更大的穩定性,因為編譯器 可以捕捉與類型相關的 bug。新的類型特性,例如泛型,增強了類型模型,並為編譯器提供更多的信息。 但 Java 開發人員常常對動態類型的優點一無所知。

運行時綁定

動態類型的靈活性比您想像的更重要。在某些方面,Java 開發人員試圖通過使用更多的 XML(這樣可 以推遲到運行時進行綁定)和字符串(這樣可以表示很多不同的類型)來突破靜態類型的限制。Ruby 中 的配置通常采用 Ruby 代碼的形式,而 Java 編程中的配置通常采用 XML 的形式。考慮 Spring 框架( 參見 參考資料):為了配置一個一般的 Spring bean,您使用 XML。您必須提供一個有效的 Java 類名 ,並為每個變量設置屬性。例如,持久引擎(如 Hibernate)需要一個會話工廠(參見 參考資料)。用 Java 語法配置一個數據訪問對象很輕松:

Dao myDataAccessObject = Dao.new(sessionFactory);

問題是,這行代碼是在編譯時綁定的,這就太靜態了。為了測試,您常常需要用其他東西,例如一個 模擬的數據訪問對象來替換會話工廠或數據訪問對象。所以,您不必像前面那樣硬編碼這個例子,而是使 用一個 Spring 之類的框架,以 XML 來配置項目,如下所示(摘自名為 petclinic 的 Spring Framework 例子):

<bean id="myDao" class="org.springframework.samples.petclinic.hibernate.HibernateClinic">
<property name="sessionFactory" ref="sessionFactory">
</bean>

Spring 框架是目前 Java 社區中最重要、最有影響力的框架之一,因為它使您可以延遲綁定,並使系 統主要元素之間的耦合性更為松散。而且,您不需要關心繼承就可以去耦。在 Java 編程中,尤其是在編 寫越來越多的 POJO(plain old Java object)的時候,使用繼承時必須特別小心,因為在 Java 語言中 只有一次這樣的機會。

在動態語言,例如 Ruby 中,解決方案就截然不同。首先,我傾向於使用一個 mix-in 來實現持久性 。所有關聯只在 mix-in 中發現一次。可以把一個 mix-in 想像成一個接口,其背後有一個實現。換句話 說,通過 mix-in,可以添加多個功能到同一個對象中,而不必使用多重繼承。實際上,Active Record 通過繼承一個公共基類來解決這個問題,這個公共基類混合了多種功能:

class Pojo < ActiveRecord::Base

在 Ruby 中,您不必關心繼承,因為使用開放的類(允許動態添加功能)和模塊(允許混入其他功能 ),您可以隨意添加更多的功能到對象中。那麼緊密耦合呢?如果您想按 Java 的方式實現該類,那麼可 以看到:

class MyClass
attr_accessor myDao #defines getters and setters for myDao
def initialize(session_factory)
myDao = Dao.new(session_factory)
end
...

initialize() 方法中的代碼看上去像一開始的屬於禁忌的 Java 版本,因為它在編譯時將數據訪問對 象綁定到會話工廠。但這是一種動態類型語言,所以不必把自己關在一個小天地裡。為了測試,總可以動 態地改變類的定義。您可以在之後打開已有的類:

class MyClass #not redefining the class; just opening the existing class
def myDao #redefine the getter for myDao
#do some work to generate the mock object
return myMockObject
end
end

結束語

從某種意義上講,作為某種編程語言的用戶,您就是那種語言的類型策略的奴隸。而作為一名 Java 程序員,您應該盡量用一種擁護類型的方式編寫 Java 代碼。最大限度地利用類型,並依靠社區來通過框 架獲得更好的元編程支持,而不是自己進行元編程,這些都是發揮自身優勢的好方法。有很多 Java 框架 都支持用於持久性(Hibernate 和 JDO)、事務(Spring 和 EJB)、模型-視圖-控制器(WebFlow 和 RIFE)以及編程模型(AspectJ)的元編程。

但是有時候需要放棄您所選擇的語言的類型,不管您是在編寫需要附加描述以獲得更好可讀性的代碼 ,還是試圖延遲類型綁定,都可以這樣。Java 語言非常強大,您可以利用很多現成的項目:

Spring 框架使您可以將綁定推遲到運行時,並提供動態類型語言的很多功能。Spring 特別適合於添 加功能到 POJO,運行時配置,以及繞過 Java 語言的類型限制。

AspectJ 是面向方面編程模型在 Java 平台上的一種實現。AspectJ 使您可以引入橫切關注點,而不 必引入額外的語法,這種技術還使您可以克服 Java 語言的靜態特性。

Hibernate 項目和 Java Persistence API (JPA) 使您可以添加持久性到 POJO 中,同樣也不必改變 底層的類型。

XML 讓您可以同時表達數據和應用程序配置。很多框架使用 XML 來克服 Java 語言的類型限制。

您還有一個選擇。通過理解其他語言中的類型策略,可以識別不適合 Java 策略的問題。當需要訪問 Java 平台 而不是 Java 語言 時,可以使用其他語言的 JVM 實現。

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