《Java学习指南》—— 1.4 设计安全
本節(jié)書摘來異步社區(qū)《Java學(xué)習(xí)指南》一書中的第1章,第1.4節(jié),作者:【美】Patrick Niemeyer , Daniel Leuck,更多章節(jié)內(nèi)容可以訪問云棲社區(qū)“異步社區(qū)”公眾號查看。
1.4 設(shè)計安全
Java被設(shè)計為一種安全語言,對于這一事實你肯定早已耳熟能詳了。但是在此“安全”指的是什么呢?對什么而言安全,或者對誰安全呢?對于Java,得到頗多關(guān)注的安全性是那些使新型動態(tài)可移植軟件成為可能的有關(guān)特性。Java提供了多層保護(hù)以避免惡意代碼,并防止諸如病毒和特洛伊木馬等更具危險性的東西。在下一節(jié)中,我們將查看Java虛擬機體系結(jié)構(gòu)如何在代碼運行前評估其安全性,還將介紹Java類加載器(Java解釋器的字節(jié)碼加載機制)如何在不可信類周圍加筑圍墻。這些特性為高級安全性策略提供了基礎(chǔ),從而可以在每個應(yīng)用的基礎(chǔ)上允許或禁止各種操作。
不過,在本節(jié)中,我們將了解Java編程語言的一些通用特性。較之于特定的安全特性,Java通過解決通用設(shè)計和編程問題所提供的安全性可能更為重要,但在安全性討論中這一點往往被忽視了。Java力圖做到盡可能安全,即不僅要“抵制”我們自己所犯的簡單錯誤,而且還要避免由原有軟件所遺傳的錯誤。Java的目標(biāo)是保持語言的簡單性,并提供展示其有用性的工具,同時令用戶可以在需要時基于該語言構(gòu)建更為復(fù)雜的功能。
1.4.1 語法簡單性
Java有著簡單性的原則。因為Java出身清白,它可以避免那些在其他語言中已經(jīng)證實為糟糕或有爭議的那些特性。例如,Java不允許程序員自定義操作符重載(overloading),而在某些語言中,允許程序員重新定義+和-這樣的基本操符號的含義。Java沒有源代碼預(yù)處理器,因此沒有宏、#define語句或條件源編譯。這些在其他語言中存在的構(gòu)造主要是為了支持平臺依賴性,因此從這個意義上講,它們在Java中是不需要的。條件編譯通常還用于調(diào)試,但是Java的高級運行時優(yōu)化以及斷言這樣的功能,較為優(yōu)雅地解決了該問題。(我們將在第4章中討論有關(guān)內(nèi)容)。
Java為組織類文件提供了一個定義良好的包結(jié)構(gòu)。此包系統(tǒng)允許編譯器處理傳統(tǒng)make實用工具的某些功能(make是用于將源代碼構(gòu)建為可執(zhí)行代碼的一個工具)。編譯器還可以直接處理已編譯Java類,因為所有類型信息都得到了保留;在此無需“頭文件”,這一點與C或C++ 有所不同。所有這些都意味著Java代碼需要讀取的上下文環(huán)境信息更少。實際上,你有時可能會發(fā)現(xiàn)查看Java源代碼比參考類文檔更為快捷。
對于在其他語言中遭遇麻煩的一些特性,Java則將其取而代之。例如,Java只支持單一的類繼承層次體系(每個類只能有一個“父”類),但是允許對接口多重繼承。接口類似于C++ 中的一個抽象類,可以指定一個對象的多個操作,但是不會定義其實現(xiàn),這是一個功能強大的機制,它允許開發(fā)者為對象定義一個“契約”,任何具體的對象實現(xiàn)都可以使用并引用該契約。Java中的接口消除了類的多重繼承需求,同時不會導(dǎo)致與多重繼承相關(guān)的問題。在第4章中你將會看到,Java是一種簡單而又優(yōu)雅的編程語言,而這仍然是它最大的吸引力。
1.4.2 類型安全和方法綁定
語言的一大屬性是其采用何種類型檢查。一般地,在將一種語言劃歸為“靜態(tài)”或“動態(tài)”時,我們所指的是:有關(guān)變量類型的信息究竟是在編譯時更多地得到明確,還是直至應(yīng)用運行時方能更多地加以確定。
在諸如C或C++ 這樣的嚴(yán)格靜態(tài)類型語言中,數(shù)據(jù)類型在編譯源代碼時即已固化。這有利于編譯器得到足夠的信息,從而在代碼執(zhí)行前就能捕獲多種錯誤,例如,編譯器不會允許你在一個整數(shù)變量中保存一個浮點值。這樣,代碼將不再需要運行時類型檢查,因此可以編譯為小而快速的可執(zhí)行代碼。但是靜態(tài)類型語言不夠靈活。它們不能支持諸如集合的高級構(gòu)造,而這些構(gòu)造對于帶有動態(tài)類型檢查的語言則相當(dāng)自然,另外對于靜態(tài)類型語言而言,應(yīng)用在運行時也不可能安全地導(dǎo)入新的數(shù)據(jù)類型。
與此相反,諸如Smalltalk或Lisp等動態(tài)語言則有一個運行時系統(tǒng),可以管理對象的類型,并在應(yīng)用執(zhí)行時完成必要的類型檢查。這些語言允許更為復(fù)雜的操作,另外在許多方面,其功能也更為強大。不過,它們往往速度較慢,不太安全,同時也較難調(diào)試。
語言之間的差別可以比作不同汽車之間的差別1。靜態(tài)類型語言(如C++)可以比作跑車,相當(dāng)安全,速度也很快,但是只有在柏油大道上才能很好地奔馳。動態(tài)性很好的語言(如Smalltalk)則更像是越野車:它們可以提供更大的自由度,但是稍難操控。也許在叢林里駕駛著它馳騁相當(dāng)有趣(有時也更快),但是有時則未免會陷入壕溝或者遭到熊的襲擊。
語言的另一個屬性是采用何種方式將方法調(diào)用綁定至其定義。在諸如C或C++這樣的語言中,方法的定義通常在編譯時綁定,除非程序員特別指出。Smalltalk則有所不同,它被稱為是一種“延遲綁定”(late-binding)語言,因為它在運行時才會動態(tài)地確定方法的定義。出于性能方面的原因,早期綁定(early-binding)相當(dāng)重要;如此可以運行應(yīng)用,而不會有運行時搜索方法所帶來的開銷。但是延遲綁定更為靈活。另外在面向?qū)ο笳Z言中,這也是必要的,在此子類可以覆蓋其超類中的方法,而且只有運行時系統(tǒng)才能確定應(yīng)當(dāng)運行哪個方法。
Java博采了C++ 和Smalltalk的優(yōu)點,它是一種靜態(tài)類型、延遲綁定的語言。Java中的每個對象都有一個編譯時即已確定的定義良好的類型。這說明,Java編譯器可以像是在C++中一樣,完成同樣的靜態(tài)類型檢查和使用分析。因此,你無法給對象賦予錯誤的變量類型,也不能在一個對象上調(diào)用不存在的方法。更有甚者,Java編譯器還可以防止使用未初始化的變量以及創(chuàng)建不會執(zhí)行的語句(請見第4章)。
不過,Java同時也完全可以做到在運行時確定類型。Java運行時系統(tǒng)會跟蹤所有對象,并使得在執(zhí)行時確定其類型和關(guān)系成為可能。這說明,可以在運行時檢查一個對象以確定它究竟是什么。與C或C++不同的是,將一種對象類型強制轉(zhuǎn)換為另一種類型時,要由運行時系統(tǒng)加以檢查,而且有可能使用新型的動態(tài)加載對象(具有一定類型安全性的)。另外,由于Java是一種延遲綁定語言,一個子類總是有可能覆蓋其超類中的方法,即使這是一個運行時加載的子類。
1.4.3 遞增開發(fā)
Java從其源代碼中將所有數(shù)據(jù)類型和方法簽名信息帶入到其編譯后的字節(jié)碼形式中。這就意味著,Java類可以遞增地進(jìn)行開發(fā)。你自己的Java類也可以安全地與來自于其他來源(編譯器從未見過此來源)的類一同使用。換句話說,可以編寫新的代碼來引用二進(jìn)制類文件,而不會丟失從源代碼所得到的類型安全性。
困擾C++ 的一個常見問題是“脆弱基類”問題(fragile base class)。在C++ 中,由于一個基類有多個派生類,因此其實現(xiàn)可能被有效地“凍結(jié)”了;修改基類可能需要重新編譯所有的派生類。對于類庫的開發(fā)人員來說,這個問題尤其困難。Java通過在類中動態(tài)地定位字段,從而避免了這一問題。只要類維護(hù)了其原始結(jié)構(gòu)的一個合法形式,那么就可以對其加以改進(jìn),而不會對由該類派生或使用了該類的其他類造成破壞。
1.4.4 動態(tài)內(nèi)存管理
Java和C(C++)這樣的低級語言之間的一些最為重要的差別涉及到Java如何管理內(nèi)存。Java取消了可以引用內(nèi)存的任意部分的臨時的指針,并且為語言增加了垃圾回收和高級數(shù)組。這些特性消除了有關(guān)安全性、可移植性和優(yōu)化的許多問題,否則這些問題將很難解決。
垃圾回收本身就可以使無數(shù)的程序員免于進(jìn)行顯式的內(nèi)存分配和釋放,而這在C或C++ 中也最容易導(dǎo)致錯誤。除了在內(nèi)存中維護(hù)對象外,Java運行時系統(tǒng)還記錄了對這些對象的所有引用。只要某個對象不再使用,Java即會自動地將其從內(nèi)存中刪除。你只需在不再使用對象時將其忽略,并確信解釋器在適當(dāng)?shù)臅r候會予以清除。
Java使用了一個復(fù)雜的垃圾回收器,它在后臺間歇性地運行,這意味著大多數(shù)垃圾回收工作均發(fā)生在空閑時間里,即介于I/O暫停、鼠標(biāo)點擊或按鍵之間。高級的運行時系統(tǒng)(如HotSpot)則可完成更高級的垃圾回收工作,甚至可以區(qū)分對象的使用模式(如對短期對象和長期對象加以區(qū)別),并且可以優(yōu)化其收集過程。Java運行時現(xiàn)在可以自動調(diào)整自身,以便針對不同的應(yīng)用程序,根據(jù)其行為來優(yōu)化內(nèi)存的分配。通過這種運行時探查,自動化的內(nèi)存管理比最勤奮的程序員所管理的資源也要快很多,而某些老派的程序員仍然對此難以置信。
你可能聽說過Java沒有指針。嚴(yán)格地說,這種說法是正確的,但是它也會帶來誤導(dǎo)。Java所提供的是引用(reference),這是一種“安全型”指針,而且在Java中,引用是相當(dāng)普遍的。引用是對象的一個強類型句柄。除了基本數(shù)字類型之外,Java中的所有對象都可以通過引用來訪問。如果必要的話,可以使用引用來構(gòu)建所有一般的數(shù)據(jù)結(jié)構(gòu),如鏈表、樹等等,對于這些數(shù)據(jù)結(jié)構(gòu),以往C程序員慣用的做法是采用指針來構(gòu)建。唯一的區(qū)別在于利用引用必須以一種類型安全的方式來操作。
在引用和指針間還有一個重要的區(qū)別,即無法通過引用更改其值(執(zhí)行指針的算術(shù)運算)。引用只能指向特定的對象或某個數(shù)組中的元素。引用是一個原子性事物;除非將引用賦給一個對象,否則無法操作引用的值。引用采用傳值方式傳遞,而且引用一個對象時,間接層不能多于一層。對引用的保護(hù)是Java安全性中最基本的一個方面。這說明,Java代碼必須“按規(guī)章辦事”,即不得“越權(quán)”行事。
不同于C或C++ 指針,Java引用只能指向類的類型。在此不存在指向方法的指針。人們有時會對此有所抱怨,但是你會發(fā)現(xiàn),若任務(wù)需要方法指針,那么大多數(shù)時候,采用接口和適配器類會更為漂亮地將其完成。另外還需提到一點,Java有一個復(fù)雜的“反射(reflection)”API,這確實允許你引用和調(diào)用單個的方法。不過,這并不是常規(guī)做法。我們將在第7章討論反射。
最后,Java中的數(shù)組是真正的頭等(first-class)對象。它們可以像其他對象一樣動態(tài)地分配和賦值。數(shù)組知道其自己的大小和類型,而且盡管你無法直接定義或派生數(shù)組類的子類,但是基于其基類型的關(guān)系,它們確實有一個定義良好的繼承關(guān)系。語言中若擁有真正的數(shù)組,則可以消除C或C++等語言中對指針?biāo)阈g(shù)運算的需求。
1.4.5 錯誤處理
Java的出發(fā)點在于網(wǎng)絡(luò)化設(shè)備和嵌入式系統(tǒng)。對于這些應(yīng)用,擁有健壯而且智能的錯誤管理機制是至關(guān)重要的。Java有一個強大的異常處理機制,這一點有些類似于C++ 的最新實現(xiàn)。異常提供了一個更為自然和優(yōu)雅的方式來處理錯誤。異常可以將錯誤處理代碼從一般的代碼中分離出來,從而得到更為簡潔、更具可讀性的應(yīng)用。
出現(xiàn)一個異常時,將導(dǎo)致程序執(zhí)行流程轉(zhuǎn)移到一個提前指定的“捕獲”代碼塊。異常附帶有一個對象,其中包含有導(dǎo)致出現(xiàn)異常的情形的相關(guān)信息。Java編譯器要求方法所聲明的異常要么是其能夠生成的,要么是可以自行捕獲和處理的。這將錯誤信息的重要性,提高到與參數(shù)(argument)和返回類型相同的層次。作為一個Java程序員,應(yīng)當(dāng)清楚地知道哪些異常情況需要處理,而且編譯器還有助于你編寫正確的軟件,從而不會讓這些異常“放任自流”而未加處理。
1.4.6 線程
如今的應(yīng)用都需要高度的并行性。即使一個非常簡單的應(yīng)用也可能有一個復(fù)雜的用戶界面,而這就需要并發(fā)的活動。隨著機器速度越來越快,用戶對于為完成無關(guān)任務(wù)而占用時間的現(xiàn)象也越來越敏感。線程為客戶和服務(wù)器應(yīng)用提供了高效的多處理和任務(wù)分配機制。Java使得線程很易于使用,因為其對線程的支持是內(nèi)置于語言中的。
并發(fā)性固然很好,但是采用線程編程所做的不僅僅是同時完成多項任務(wù)。在大多數(shù)情況下,線程需要得到同步(協(xié)調(diào)),如果沒有顯式的語言支持則會相當(dāng)棘手。Java基于監(jiān)視器和條件模型可以支持同步,這是一種用于訪問資源的加鎖和鑰匙系統(tǒng)。關(guān)鍵字synchronized指定方法和代碼塊要在對象內(nèi)得到安全、串行化的訪問。也存在一些簡單的基本方法,從而可以在對同一對象加以處理的線程之間顯式地等待和標(biāo)記。
Java還有一個高級的并發(fā)包,它提供了強大的工具來解決多線程編程中的常見模式,例如,線程池、任務(wù)的協(xié)調(diào)以及復(fù)雜的鎖定。通過這個并發(fā)包和相關(guān)的工具,Java提供了一些比任何其他語言都更為高級的線程相關(guān)工具。
盡管一些開發(fā)者可能永遠(yuǎn)不必編寫多線程代碼,但學(xué)習(xí)使用線程編程,是掌握J(rèn)ava編程的一個重要部分,這是所有程序員都應(yīng)該掌握的內(nèi)容。參見第9章關(guān)于這個主題的更多討論。
1.4.7 可伸縮性
在最低的層次上,Java程序由類組成。類被設(shè)計為小型的模塊化組件。在類之上,Java提供了包,這是一個結(jié)構(gòu)層,它將類分組為功能單元。包為類的組織提供了一個命名約定,另外還對Java應(yīng)用中變量和方法的可見性提供了另一級組織控制。
在一個包中,類可以是公開可見的,也可能有所保護(hù)以避免外部訪問。包構(gòu)成了另一種類型的作用域,它與應(yīng)用級更為接近。這有助于構(gòu)建能夠在系統(tǒng)中協(xié)同工作的可復(fù)用組件。包還有助于設(shè)計一個可伸縮的應(yīng)用,從而在擴展應(yīng)用時,代碼不至于過于相互依賴。
總結(jié)
以上是生活随笔為你收集整理的《Java学习指南》—— 1.4 设计安全的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: “玲珑杯”线上赛 Round #15 河
- 下一篇: 【Java】PMD规则学习(1) --字