《Java学习指南》—— 1.4 设计安全
本節書摘來異步社區《Java學習指南》一書中的第1章,第1.4節,作者:【美】Patrick Niemeyer , Daniel Leuck,更多章節內容可以訪問云棲社區“異步社區”公眾號查看。
1.4 設計安全
Java被設計為一種安全語言,對于這一事實你肯定早已耳熟能詳了。但是在此“安全”指的是什么呢?對什么而言安全,或者對誰安全呢?對于Java,得到頗多關注的安全性是那些使新型動態可移植軟件成為可能的有關特性。Java提供了多層保護以避免惡意代碼,并防止諸如病毒和特洛伊木馬等更具危險性的東西。在下一節中,我們將查看Java虛擬機體系結構如何在代碼運行前評估其安全性,還將介紹Java類加載器(Java解釋器的字節碼加載機制)如何在不可信類周圍加筑圍墻。這些特性為高級安全性策略提供了基礎,從而可以在每個應用的基礎上允許或禁止各種操作。
不過,在本節中,我們將了解Java編程語言的一些通用特性。較之于特定的安全特性,Java通過解決通用設計和編程問題所提供的安全性可能更為重要,但在安全性討論中這一點往往被忽視了。Java力圖做到盡可能安全,即不僅要“抵制”我們自己所犯的簡單錯誤,而且還要避免由原有軟件所遺傳的錯誤。Java的目標是保持語言的簡單性,并提供展示其有用性的工具,同時令用戶可以在需要時基于該語言構建更為復雜的功能。
1.4.1 語法簡單性
Java有著簡單性的原則。因為Java出身清白,它可以避免那些在其他語言中已經證實為糟糕或有爭議的那些特性。例如,Java不允許程序員自定義操作符重載(overloading),而在某些語言中,允許程序員重新定義+和-這樣的基本操符號的含義。Java沒有源代碼預處理器,因此沒有宏、#define語句或條件源編譯。這些在其他語言中存在的構造主要是為了支持平臺依賴性,因此從這個意義上講,它們在Java中是不需要的。條件編譯通常還用于調試,但是Java的高級運行時優化以及斷言這樣的功能,較為優雅地解決了該問題。(我們將在第4章中討論有關內容)。
Java為組織類文件提供了一個定義良好的包結構。此包系統允許編譯器處理傳統make實用工具的某些功能(make是用于將源代碼構建為可執行代碼的一個工具)。編譯器還可以直接處理已編譯Java類,因為所有類型信息都得到了保留;在此無需“頭文件”,這一點與C或C++ 有所不同。所有這些都意味著Java代碼需要讀取的上下文環境信息更少。實際上,你有時可能會發現查看Java源代碼比參考類文檔更為快捷。
對于在其他語言中遭遇麻煩的一些特性,Java則將其取而代之。例如,Java只支持單一的類繼承層次體系(每個類只能有一個“父”類),但是允許對接口多重繼承。接口類似于C++ 中的一個抽象類,可以指定一個對象的多個操作,但是不會定義其實現,這是一個功能強大的機制,它允許開發者為對象定義一個“契約”,任何具體的對象實現都可以使用并引用該契約。Java中的接口消除了類的多重繼承需求,同時不會導致與多重繼承相關的問題。在第4章中你將會看到,Java是一種簡單而又優雅的編程語言,而這仍然是它最大的吸引力。
1.4.2 類型安全和方法綁定
語言的一大屬性是其采用何種類型檢查。一般地,在將一種語言劃歸為“靜態”或“動態”時,我們所指的是:有關變量類型的信息究竟是在編譯時更多地得到明確,還是直至應用運行時方能更多地加以確定。
在諸如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是一種延遲綁定語言,一個子類總是有可能覆蓋其超類中的方法,即使這是一個運行時加載的子類。
1.4.3 遞增開發
Java從其源代碼中將所有數據類型和方法簽名信息帶入到其編譯后的字節碼形式中。這就意味著,Java類可以遞增地進行開發。你自己的Java類也可以安全地與來自于其他來源(編譯器從未見過此來源)的類一同使用。換句話說,可以編寫新的代碼來引用二進制類文件,而不會丟失從源代碼所得到的類型安全性。
困擾C++ 的一個常見問題是“脆弱基類”問題(fragile base class)。在C++ 中,由于一個基類有多個派生類,因此其實現可能被有效地“凍結”了;修改基類可能需要重新編譯所有的派生類。對于類庫的開發人員來說,這個問題尤其困難。Java通過在類中動態地定位字段,從而避免了這一問題。只要類維護了其原始結構的一個合法形式,那么就可以對其加以改進,而不會對由該類派生或使用了該類的其他類造成破壞。
1.4.4 動態內存管理
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++等語言中對指針算術運算的需求。
1.4.5 錯誤處理
Java的出發點在于網絡化設備和嵌入式系統。對于這些應用,擁有健壯而且智能的錯誤管理機制是至關重要的。Java有一個強大的異常處理機制,這一點有些類似于C++ 的最新實現。異常提供了一個更為自然和優雅的方式來處理錯誤。異常可以將錯誤處理代碼從一般的代碼中分離出來,從而得到更為簡潔、更具可讀性的應用。
出現一個異常時,將導致程序執行流程轉移到一個提前指定的“捕獲”代碼塊。異常附帶有一個對象,其中包含有導致出現異常的情形的相關信息。Java編譯器要求方法所聲明的異常要么是其能夠生成的,要么是可以自行捕獲和處理的。這將錯誤信息的重要性,提高到與參數(argument)和返回類型相同的層次。作為一個Java程序員,應當清楚地知道哪些異常情況需要處理,而且編譯器還有助于你編寫正確的軟件,從而不會讓這些異常“放任自流”而未加處理。
1.4.6 線程
如今的應用都需要高度的并行性。即使一個非常簡單的應用也可能有一個復雜的用戶界面,而這就需要并發的活動。隨著機器速度越來越快,用戶對于為完成無關任務而占用時間的現象也越來越敏感。線程為客戶和服務器應用提供了高效的多處理和任務分配機制。Java使得線程很易于使用,因為其對線程的支持是內置于語言中的。
并發性固然很好,但是采用線程編程所做的不僅僅是同時完成多項任務。在大多數情況下,線程需要得到同步(協調),如果沒有顯式的語言支持則會相當棘手。Java基于監視器和條件模型可以支持同步,這是一種用于訪問資源的加鎖和鑰匙系統。關鍵字synchronized指定方法和代碼塊要在對象內得到安全、串行化的訪問。也存在一些簡單的基本方法,從而可以在對同一對象加以處理的線程之間顯式地等待和標記。
Java還有一個高級的并發包,它提供了強大的工具來解決多線程編程中的常見模式,例如,線程池、任務的協調以及復雜的鎖定。通過這個并發包和相關的工具,Java提供了一些比任何其他語言都更為高級的線程相關工具。
盡管一些開發者可能永遠不必編寫多線程代碼,但學習使用線程編程,是掌握Java編程的一個重要部分,這是所有程序員都應該掌握的內容。參見第9章關于這個主題的更多討論。
1.4.7 可伸縮性
在最低的層次上,Java程序由類組成。類被設計為小型的模塊化組件。在類之上,Java提供了包,這是一個結構層,它將類分組為功能單元。包為類的組織提供了一個命名約定,另外還對Java應用中變量和方法的可見性提供了另一級組織控制。
在一個包中,類可以是公開可見的,也可能有所保護以避免外部訪問。包構成了另一種類型的作用域,它與應用級更為接近。這有助于構建能夠在系統中協同工作的可復用組件。包還有助于設計一個可伸縮的應用,從而在擴展應用時,代碼不至于過于相互依賴。
總結
以上是生活随笔為你收集整理的《Java学习指南》—— 1.4 设计安全的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: “玲珑杯”线上赛 Round #15 河
- 下一篇: 【Java】PMD规则学习(1) --字