天天看點

《Java學習指南》—— 1.4 設計安全

本節書摘來異步社群《java學習指南》一書中的第1章,第1.4節,作者:【美】patrick niemeyer , daniel leuck,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

java被設計為一種安全語言,對于這一事實你肯定早已耳熟能詳了。但是在此“安全”指的是什麼呢?對什麼而言安全,或者對誰安全呢?對于java,得到頗多關注的安全性是那些使新型動态可移植軟體成為可能的有關特性。java提供了多層保護以避免惡意代碼,并防止諸如病毒和特洛伊木馬等更具危險性的東西。在下一節中,我們将檢視java虛拟機體系結構如何在代碼運作前評估其安全性,還将介紹java類加載器(java解釋器的位元組碼加載機制)如何在不可信類周圍加築圍牆。這些特性為進階安全性政策提供了基礎,進而可以在每個應用的基礎上允許或禁止各種操作。

不過,在本節中,我們将了解java程式設計語言的一些通用特性。較之于特定的安全特性,java通過解決通用設計和程式設計問題所提供的安全性可能更為重要,但在安全性讨論中這一點往往被忽視了。java力圖做到盡可能安全,即不僅要“抵制”我們自己所犯的簡單錯誤,而且還要避免由原有軟體所遺傳的錯誤。java的目标是保持語言的簡單性,并提供展示其有用性的工具,同時令使用者可以在需要時基于該語言建構更為複雜的功能。

java有着簡單性的原則。因為java出身清白,它可以避免那些在其他語言中已經證明為糟糕或有争議的那些特性。例如,java不允許程式員自定義操作符重載(overloading),而在某些語言中,允許程式員重新定義+和-這樣的基本操符号的含義。java沒有源代碼預處理器,是以沒有宏、#define語句或條件源編譯。這些在其他語言中存在的構造主要是為了支援平台依賴性,是以從這個意義上講,它們在java中是不需要的。條件編譯通常還用于調試,但是java的進階運作時優化以及斷言這樣的功能,較為優雅地解決了該問題。(我們将在第4章中讨論有關内容)。

java為組織類檔案提供了一個定義良好的包結構。此包系統允許編譯器處理傳統make實用工具的某些功能(make是用于将源代碼建構為可執行代碼的一個工具)。編譯器還可以直接處理已編譯java類,因為所有類型資訊都得到了保留;在此無需“頭檔案”,這一點與c或c++ 有所不同。所有這些都意味着java代碼需要讀取的上下文環境資訊更少。實際上,你有時可能會發現檢視java源代碼比參考類文檔更為快捷。

對于在其他語言中遭遇麻煩的一些特性,java則将其取而代之。例如,java隻支援單一的類繼承層次體系(每個類隻能有一個“父”類),但是允許對接口多重繼承。接口類似于c++ 中的一個抽象類,可以指定一個對象的多個操作,但是不會定義其實作,這是一個功能強大的機制,它允許開發者為對象定義一個“契約”,任何具體的對象實作都可以使用并引用該契約。java中的接口消除了類的多重繼承需求,同時不會導緻與多重繼承相關的問題。在第4章中你将會看到,java是一種簡單而又優雅的程式設計語言,而這仍然是它最大的吸引力。

語言的一大屬性是其采用何種類型檢查。一般地,在将一種語言劃歸為“靜态”或“動态”時,我們所指的是:有關變量類型的資訊究竟是在編譯時更多地得到明确,還是直至應用運作時方能更多地加以确定。

在諸如c或c++ 這樣的嚴格靜态類型語言中,資料類型在編譯源代碼時即已固化。這有利于編譯器得到足夠的資訊,進而在代碼執行前就能捕獲多種錯誤,例如,編譯器不會允許你在一個整數變量中儲存一個浮點值。這樣,代碼将不再需要運作時類型檢查,是以可以編譯為小而快速的可執行代碼。但是靜态類型語言不夠靈活。它們不能支援諸如集合的進階構造,而這些構造對于帶有動态類型檢查的語言則相當自然,另外對于靜态類型語言而言,應用在運作時也不可能安全地導入新的資料類型。

與此相反,諸如smalltalk或lisp等動态語言則有一個運作時系統,可以管理對象的類型,并在應用執行時完成必要的類型檢查。這些語言允許更為複雜的操作,另外在許多方面,其功能也更為強大。不過,它們往往速度較慢,不太安全,同時也較難調試。

語言之間的差别可以比作不同汽車之間的差别1。靜态類型語言(如c++)可以比作跑車,相當安全,速度也很快,但是隻有在柏油大道上才能很好地奔馳。動态性很好的語言(如smalltalk)則更像是越野車:它們可以提供更大的自由度,但是稍難操控。也許在叢林裡駕駛着它馳騁相當有趣(有時也更快),但是有時則未免會陷入壕溝或者遭到熊的襲擊。

語言的另一個屬性是采用何種方式将方法調用綁定至其定義。在諸如c或c++這樣的語言中,方法的定義通常在編譯時綁定,除非程式員特别指出。smalltalk則有所不同,它被稱為是一種“延遲綁定”(late-binding)語言,因為它在運作時才會動态地确定方法的定義。出于性能方面的原因,早期綁定(early-binding)相當重要;如此可以運作應用,而不會有運作時搜尋方法所帶來的開銷。但是延遲綁定更為靈活。另外在面向對象語言中,這也是必要的,在此子類可以覆寫其超類中的方法,而且隻有運作時系統才能确定應當運作哪個方法。

java博采了c++ 和smalltalk的優點,它是一種靜态類型、延遲綁定的語言。java中的每個對象都有一個編譯時即已确定的定義良好的類型。這說明,java編譯器可以像是在c++中一樣,完成同樣的靜态類型檢查和使用分析。是以,你無法給對象賦予錯誤的變量類型,也不能在一個對象上調用不存在的方法。更有甚者,java編譯器還可以防止使用未初始化的變量以及建立不會執行的語句(請見第4章)。

不過,java同時也完全可以做到在運作時确定類型。java運作時系統會跟蹤所有對象,并使得在執行時确定其類型和關系成為可能。這說明,可以在運作時檢查一個對象以确定它究竟是什麼。與c或c++不同的是,将一種對象類型強制轉換為另一種類型時,要由運作時系統加以檢查,而且有可能使用新型的動态加載對象(具有一定類型安全性的)。另外,由于java是一種延遲綁定語言,一個子類總是有可能覆寫其超類中的方法,即使這是一個運作時加載的子類。

java從其源代碼中将所有資料類型和方法簽名資訊帶入到其編譯後的位元組碼形式中。這就意味着,java類可以遞增地進行開發。你自己的java類也可以安全地與來自于其他來源(編譯器從未見過此來源)的類一同使用。換句話說,可以編寫新的代碼來引用二進制類檔案,而不會丢失從源代碼所得到的類型安全性。

困擾c++ 的一個常見問題是“脆弱基類”問題(fragile base class)。在c++ 中,由于一個基類有多個派生類,是以其實作可能被有效地“當機”了;修改基類可能需要重新編譯所有的派生類。對于類庫的開發人員來說,這個問題尤其困難。java通過在類中動态地定位字段,進而避免了這一問題。隻要類維護了其原始結構的一個合法形式,那麼就可以對其加以改進,而不會對由該類派生或使用了該類的其他類造成破壞。

java和c(c++)這樣的低級語言之間的一些最為重要的差别涉及到java如何管理記憶體。java取消了可以引用記憶體的任意部分的臨時的指針,并且為語言增加了垃圾回收和進階數組。這些特性消除了有關安全性、可移植性和優化的許多問題,否則這些問題将很難解決。

垃圾回收本身就可以使無數的程式員免于進行顯式的記憶體配置設定和釋放,而這在c或c++ 中也最容易導緻錯誤。除了在記憶體中維護對象外,java運作時系統還記錄了對這些對象的所有引用。隻要某個對象不再使用,java即會自動地将其從記憶體中删除。你隻需在不再使用對象時将其忽略,并确信解釋器在适當的時候會予以清除。

java使用了一個複雜的垃圾回收器,它在背景間歇性地運作,這意味着大多數垃圾回收工作均發生在空閑時間裡,即介于i/o暫停、滑鼠點選或按鍵之間。進階的運作時系統(如hotspot)則可完成更進階的垃圾回收工作,甚至可以區分對象的使用模式(如對短期對象和長期對象加以差別),并且可以優化其收集過程。java運作時現在可以自動調整自身,以便針對不同的應用程式,根據其行為來優化記憶體的配置設定。通過這種運作時探查,自動化的記憶體管理比最勤奮的程式員所管理的資源也要快很多,而某些老派的程式員仍然對此難以置信。

你可能聽說過java沒有指針。嚴格地說,這種說法是正确的,但是它也會帶來誤導。java所提供的是引用(reference),這是一種“安全型”指針,而且在java中,引用是相當普遍的。引用是對象的一個強類型句柄。除了基本數字類型之外,java中的所有對象都可以通過引用來通路。如果必要的話,可以使用引用來建構所有一般的資料結構,如連結清單、樹等等,對于這些資料結構,以往c程式員慣用的做法是采用指針來建構。唯一的差別在于利用引用必須以一種類型安全的方式來操作。

在引用和指針間還有一個重要的差別,即無法通過引用更改其值(執行指針的算術運算)。引用隻能指向特定的對象或某個數組中的元素。引用是一個原子性事物;除非将引用賦給一個對象,否則無法操作引用的值。引用采用傳值方式傳遞,而且引用一個對象時,間接層不能多于一層。對引用的保護是java安全性中最基本的一個方面。這說明,java代碼必須“按規章辦事”,即不得“越權”行事。

不同于c或c++ 指針,java引用隻能指向類的類型。在此不存在指向方法的指針。人們有時會對此有所抱怨,但是你會發現,若任務需要方法指針,那麼大多數時候,采用接口和擴充卡類會更為漂亮地将其完成。另外還需提到一點,java有一個複雜的“反射(reflection)”api,這确實允許你引用和調用單個的方法。不過,這并不是正常做法。我們将在第7章讨論反射。

最後,java中的數組是真正的頭等(first-class)對象。它們可以像其他對象一樣動态地配置設定和指派。數組知道其自己的大小和類型,而且盡管你無法直接定義或派生數組類的子類,但是基于其基類型的關系,它們确實有一個定義良好的繼承關系。語言中若擁有真正的數組,則可以消除c或c++等語言中對指針算術運算的需求。

java的出發點在于網絡化裝置和嵌入式系統。對于這些應用,擁有健壯而且智能的錯誤管理機制是至關重要的。java有一個強大的異常處理機制,這一點有些類似于c++ 的最新實作。異常提供了一個更為自然和優雅的方式來處理錯誤。異常可以将錯誤處理代碼從一般的代碼中分離出來,進而得到更為簡潔、更具可讀性的應用。

出現一個異常時,将導緻程式執行流程轉移到一個提前指定的“捕獲”代碼塊。異常附帶有一個對象,其中包含有導緻出現異常的情形的相關資訊。java編譯器要求方法所聲明的異常要麼是其能夠生成的,要麼是可以自行捕獲和處理的。這将錯誤資訊的重要性,提高到與參數(argument)和傳回類型相同的層次。作為一個java程式員,應當清楚地知道哪些異常情況需要處理,而且編譯器還有助于你編寫正确的軟體,進而不會讓這些異常“放任自流”而未加處理。

如今的應用都需要高度的并行性。即使一個非常簡單的應用也可能有一個複雜的使用者界面,而這就需要并發的活動。随着機器速度越來越快,使用者對于為完成無關任務而占用時間的現象也越來越敏感。線程為客戶和伺服器應用提供了高效的多處理和任務配置設定機制。java使得線程很易于使用,因為其對線程的支援是内置于語言中的。

并發性固然很好,但是采用線程程式設計所做的不僅僅是同時完成多項任務。在大多數情況下,線程需要得到同步(協調),如果沒有顯式的語言支援則會相當棘手。java基于螢幕和條件模型可以支援同步,這是一種用于通路資源的加鎖和鑰匙系統。關鍵字synchronized指定方法和代碼塊要在對象内得到安全、串行化的通路。也存在一些簡單的基本方法,進而可以在對同一對象加以處理的線程之間顯式地等待和标記。

java還有一個進階的并發包,它提供了強大的工具來解決多線程程式設計中的常見模式,例如,線程池、任務的協調以及複雜的鎖定。通過這個并發包和相關的工具,java提供了一些比任何其他語言都更為進階的線程相關工具。

盡管一些開發者可能永遠不必編寫多線程代碼,但學習使用線程程式設計,是掌握java程式設計的一個重要部分,這是所有程式員都應該掌握的内容。參見第9章關于這個主題的更多讨論。

在最低的層次上,java程式由類組成。類被設計為小型的子產品化元件。在類之上,java提供了包,這是一個結構層,它将類分組為功能單元。包為類的組織提供了一個命名約定,另外還對java應用中變量和方法的可見性提供了另一級組織控制。

在一個包中,類可以是公開可見的,也可能有所保護以避免外部通路。包構成了另一種類型的作用域,它與應用級更為接近。這有助于建構能夠在系統中協同工作的可複用元件。包還有助于設計一個可伸縮的應用,進而在擴充應用時,代碼不至于過于互相依賴。

《Java學習指南》—— 1.4 設計安全