天天看點

《Android程式設計》一2.4 Java程式設計慣例

對于一門語言,介于程式設計語言語義規範和好的面向模式的設計之間的是對語言的良好使用。一名喜歡遵循慣例的程式員會使用一緻的代碼來表達類似的思想,而且通過使用這種方式,程式會更易于了解,且能夠在充分利用運作時環境的同時避免語言中存在的“陷阱”。

java的一個主要設計目标是程式設計安全。其中存在的很多備援和不靈活機制都是為了幫助編譯器預防在運作時出現各種錯誤,而這些措施在其他語言,如ruby、python和objective-c中是不存在的。

java的靜态類型(static typing)是已經被證明的特性,其優越性已不再局限于java自己的編譯器中。機器能夠自動解析并識别java代碼的語義是開發強大的工具,如findbugs和ide重構工具的主要驅動力。

很多開發人員對這些限制表示認同,尤其是對于使用了一些現代編碼工具的開發人員,他們認為,與能夠快速定位問題相比,這些限制條件所耗費的代價很低,因為如果不能快速定位這些問題,它們可能隻有在部署時才會顯現出來。當然,也一定會有大量的開發者認為在動态語言中,他們節省了很多編碼時間,使用節省的這些時間可以編寫出廣泛的單元測試和內建測試用例,也同樣能夠提早發現問題。

在這些讨論中,無論你支援的是哪一方,盡可能充分地利用工具是非常有意義的。java的靜态綁定絕對是一種限制,而在另一方面,java是一門非常好的靜态綁定的語言。java不是一門好的動态語言。實際上,使用java的reflection機制和内省(introspection)api也能執行很多類型轉換類的動态功能。除了在非常受限的環境中之外,java語言的這種使用方式通常是為了實作跨平台。你的程式可能會非常緩慢,即使使用很多android工具進行優化也無濟于事。可能最大的好處是,如果平台很少被使用到的那部分存在bug,你會是第一個找到這些bug的人。希望你能夠喜歡java的靜态本質(至少在有另一門更好的動态語言之前)并充分利用java的靜态特征。

封裝

開發人員通過代碼封裝(encapsulation)的方式實作對對象成員可見性的限制。封裝的思想是對象不要暴露其本身不想提供的詳細資訊。回到之前提到的雞尾酒的例子,當要制作雞尾酒時,你隻在乎同僚給你買了必要的配料,卻并不關心她是如何買的。然而,假定你和她這麼說:“你可以去買一下配料嗎?另外,出去的時候,是否可以順便給玫瑰花澆水?”這句話意味着你不再不關心她是如何買配料的,因為你現在已經對她買配料所要經過的路線有所考慮了。

同樣,一個對象的接口(有時簡稱為api)就是調用者可以通路的方法和變量。仔細封裝之後,開發者可以做到其開發的對象的實作細節對于使用它的代碼而言是透明的。使用這種控制和保護機制開發出的程式更加靈活,開發者在對其實作進行修改時不需要調用方進行任何修改。

getter和setter方法

在java中,一種簡單常用的封裝方式是使用getter和setter方法。下面這段代碼是一個簡單的命名為contact的類的定義:

《Android程式設計》一2.4 Java程式設計慣例

該定義使得外部對象能夠直接通路類contact的成員變量,如下所示:

《Android程式設計》一2.4 Java程式設計慣例

用過幾次之後,發現contact實際上需要包含多個email位址。遺憾的是,當在這個類的實作中增加多個位址時,整個程式中的每個調用contact.email的地方都需要進行相應的修改。

下面這個類的情況與其相反:

《Android程式設計》一2.4 Java程式設計慣例
《Android程式設計》一2.4 Java程式設計慣例

在上面這個版本的contact類中,使用了private通路修飾符,它不允許直接通路類的成員變量。提供了public類型的getter方法,使用方使用這些方法來得到所需要的contact對象的name、age和email位址。例如,可以将email位址儲存在對象内部,正如前一個例子那樣,也可以使用username和hostname組合,隻要這種方式對于給定的應用更便捷即可。在類的内部,成員變量age可以是int類型或integer類型。這個版本的contact類可以做到支援多個email位址,而不需要對用戶端有任何修改。

java允許對成員變量直接引用,而它和某些語言不同,它不支援對getter和setter方法中的成員變量的引用進行封裝。為了防止封裝,必須自己定義每個通路方法。大多數ide會提供代碼生成功能,可以快速準确地完成這個功能。

通過getter和setter這種封裝方法可以提供靈活性,因為直接的成員變量通路意味着如果某個成員變量的類型發生變化,則使用該成員變量的所有代碼都需要進行修改。getter和setter方法代表的是一種簡單的對象封裝機制。一個良好的慣例是建議所有的成員變量都定義為private類型或final類型。編寫良好的java程式會使用getter和setter方法,以及一些其他更複雜的封裝方式,進而保證在複雜程式中的靈活性。

有ui開發經驗的開發人員對于回調函數會很熟悉:當ui發生變化時,需要能夠通知你的應用程式。可能是按下了某個按鈕,應用模型需要進行相應的狀态變化;也可能是在網絡中有新的資料,需要對它進行顯示。需要在架構中添加一個代碼塊,以便于自己後期執行它。

盡管java語言确實提供了傳遞代碼塊的機制,但有點怪異的是,代碼塊或方法都不是類對象。在java中,無論是代碼塊還是方法,都無法直接使用它。

可以建立一個類的執行個體的引用。在java中,不支援代碼塊或方法的傳遞,能夠傳遞的是定義了所需要的代碼的整個類,需要使用的代碼塊和方法是這個類的一個方法。提供回調函數api的服務會使用接口來定義其協定。服務用戶端定義該接口的實作,并把它傳遞給應用架構,如android中實作使用者按鍵響應的機制。在android中,類view定義了接口onkeylistener,該接口又定義了方法onkey。如果在你的代碼中,把方法onkeylistener的實作傳遞給類view,那麼每當類view得到一個新的按鍵事件時,就會調用其onkey方法。

其代碼看起來如下所示:

《Android程式設計》一2.4 Java程式設計慣例

當建立一個新的mydatamodel類時,需要在其構造函數中以參數的形式告訴類所依附的view。構造函數會建立回調類新的執行個體keyhandler,并在view中裝載這個執行個體。後續的所有按鍵事件都和模型執行個體的handlekey方法關聯起來。

雖然這種方式是可行的,但看起來很醜陋,尤其當你的模型類需要處理多個view的多種事件時!程式執行一段時間後,所有這些類型定義都混雜在最上方。定義和使用方式可能有很大差別,如果你考慮這個問題,它們一點用處都沒有。

java提供了一種簡化它的方式,即使用匿名類(anonymous class)。下面這段代碼類似于之前給出的,其差別在于用的是匿名類:

《Android程式設計》一2.4 Java程式設計慣例

除了解析可能需要更多一些時間外,這塊代碼和前面給出的例子幾乎完全相同。在調用中,它把新建立的子類的執行個體view.onkeylistener作為參數傳遞給view.setonkeylistener。然而,在這個例子中,view.setonkeylistener的參數包含特殊的語義,它定義了接口view.onkeylistener的新子類,并在語句中對它進行了執行個體化。新的執行個體是沒有名字的類的執行個體:它是匿名類,其定義隻存在于對它執行初始化的那條語句中。

匿名類是一個非常便捷的工具,而且是java實作多種代碼塊的習慣用法。使用匿名類建立的對象是頂級對象,可以用于任何具有相同類型的其他對象中。舉個例子,可以按照下述方式進行指派:

《Android程式設計》一2.4 Java程式設計慣例

你可能會覺得奇怪,在這個例子中,為什麼匿名類要把其實際實作(handlekey方法)委托(delegate)給其所在的類(containing class)呢?沒有什麼規則限制匿名類的内容:它絕對也可以包含全部的實作。但是,良好的、慣用的方式是把改變對象狀态的代碼放到對象類中。如果實作放在包含匿名類的類中,它就可以用于其他方法和調用。匿名類隻是起到中間作用,而這正是希望它做的。

關于作用域中變量的使用,java确實在匿名類内包含一些強限制(任何在塊内定義的)。特别是,匿名類隻能指向從作用域繼承的聲明為final類型的變量。例如,以下代碼片段将無法編譯:

《Android程式設計》一2.4 Java程式設計慣例

其解決方法是把參數聲明為final類型。當然,聲明為final類型意味着它在匿名類中不會發生變化,但是如下所示是這種情形的一種簡單、慣用的方式:

雖然java中的類擴充為開發人員提供了很大的靈活性,便于重新定義在不同上下文中使用的對象,但是要想利用好類和接口,還需要相當多的經驗。理想情況下,開發人員緻力于建立能夠經受得住時間考驗的代碼塊,且盡可能在很多不同的上下文中能重用,甚至作為庫在多個應用中使用。這種程式設計方式可以減少代碼中的bug,并能縮短應用開發的時間。子產品化程式設計、封裝和關注點切分都是在最大程式上增強代碼可重用性和穩定性的關鍵政策。

在面向對象的開發中,委托(delegate)或繼承是重用已有代碼的基礎設計思路。下面給出的這一系列例子,顯示了多種組織結構,這些結構可以用來描述汽車類遊戲應用中的各種元件。每個例子就是一種子產品化方式。

開發人員首先建立一個汽車類,它包含了所有的汽車邏輯及每種類型的引擎的所有邏輯,如下所示:

《Android程式設計》一2.4 Java程式設計慣例

這段代碼很簡單。雖然它可能能夠正常工作,但它把一些不相關的實作混合在了一起(如所有類型的汽車引擎),可能難以擴充。例如,修改實作以适應新的引擎類型(nuclear類型)。各種引擎的代碼互相之間都是可見的。一種引擎的某個漏洞,可能會意外地影響到另外一個完全不相關的引擎。一個引擎的變化也可能意外地導緻另外一個引擎發生變化。一台使用電動引擎的汽車必須巡閱一遍已有的各種類型的引擎。今後要使用monolithicvehicle類的開發人員必須了解清楚所有複雜的互動,才能夠對代碼進行修改。這種代碼不具備可擴充性。

如何改進這個實作呢?一種較常見的方法是使用子類(subclassing)。可以按照下面這段代碼中的方式來實作不同的機動車類型,每個類型的機動車都和其引擎類型綁定:

《Android程式設計》一2.4 Java程式設計慣例

相對上一段代碼而言,這段代碼顯然是個改進。每種類型的引擎的代碼封裝在一個獨立的類中,互相之間不會幹擾。可以對每種汽車類型進行擴充,而不會影響到其他類型。在很多情況下,這是一種理想的實作方式。

另一方面,如果把tightlyboundgasvehicle轉換成biodiesel,會發生什麼情況呢?在這個實作中,car和engine是同一個對象,不能分開。如果在現實情況中需要分别考慮,那麼架構需要更松散一些:

《Android程式設計》一2.4 Java程式設計慣例
《Android程式設計》一2.4 Java程式設計慣例

在這個架構中,vehicle類把所有和引擎相關的行為委托給了它的引擎對象。這種方式有時被稱之為has-a,這種方式和前面的子類例子中的is-a是有差別的。has-a的方式更靈活,因為它把引擎真正的工作方式方面的資訊封裝起來了。每個vehicle把這些工作委托給了松散耦合的引擎類型,而不關心該引擎具體會如何實作這些行為。has-a這種方式中使用的是可重用的delegatingvehicle類,當需要使用一種新的引擎時,該類一點都不需要改變。vehicle可以使用任何類型的引擎接口實作。此外,還可以建立不同的vehicle類型,如suv、簡約型和豪華型等,每個都可以使用多種不同的引擎類型。

使用委托模式最小化兩個對象之間的互相依賴,最大化後期對它們進行修改的靈活性。委托模式和繼承模式相比,更易于對代碼進行後期的擴充和改進。使用接口來定義對象及其委托之間的關系,開發人員能夠確定委托會按照期望的行為運轉。

java語言支援多線程并發運作的執行方式。不同線程中的語句是按序執行,但是不同線程中的語句之間不存在順序關系。java中并發執行的基礎單元封裝在類java.lang.thread中。對線程進行擴充的推薦方式是使用接口java.lang.runnable的實作,如下例子所示:

《Android程式設計》一2.4 Java程式設計慣例

在前面這個例子中,方法spawnthread建立了一個新的線程,它把新的concurrenttask執行個體傳遞給了線程的構造函數,然後該方法對新的線程調用了start方法。當調用線程的start方法時,底層虛拟機(vm)會建立新的執行線程,該線程又會執行runnable接口的run方法,和擴充的線程并發執行。在這一點,vm是啟動兩個獨立的程序來運作:一個線程中的代碼執行的順序和時間與另一個線程互相獨立,完全無關。

thread類不是final類型。可以通過實作thread子類的方式來定義一個新的并發任務,并重寫其run方法,然而這種實作方式沒有什麼優勢。實際上,使用runnable接口的靈活性更高。因為runnable是一個接口,從傳遞給thread構造函數的runnable接口可以擴充出一些其他有用的類。

當兩個或多個線程都能夠通路同一組變量時,某些線程可能會修改這些變量,導緻資料不一緻,這會破壞其他線程的邏輯。這種無意的并發通路bug稱為“線程安全破壞”(thread safety violations)。這種問題複現的難度較大,難以找到,也難以測試。

java沒有明确要求對多個線程都會通路的變量進行強制的通路限制。相反,java為支援線程安全所提供的主要機制是通過synchronized這個關鍵字。該關鍵字序列化通路其控制的代碼塊,而且更重要的是,它會對兩個線程之間的可見狀态進行同步。使用java的并發功能時,很容易忘記同步機制同時控制了通路和可見性。例如下面的程式:

《Android程式設計》一2.4 Java程式設計慣例

可能有人會認為:“好了,這裡不需要同步變量shouldstop。當然,主線程和派生線程在通路該變量時可能會發生沖突。那又有什麼關系呢?派生線程總是很快就把這個變量值設定為true。布爾寫操作是原子性的。如果主線程這次通路該變量時值為false,那麼下一次它通路時應該就是true。”這種思考方式是非常危險的,而且是錯誤的。它沒有考慮到編譯器優化和處理器的緩存機制!事實上,該程式可能永遠都不會停止。這兩個線程很可能隻會使用自己的那份shouldstop資料副本,該副本隻存在于某個本地處理器緩存中。由于在兩個線程之間不存在同步,緩存副本可能永遠都不會對外釋出,是以派生線程生成的資料值在主線程中可能是始終不可見的。

在java中,有一個簡單的實作線程安全的規則:當兩個不同的線程通路同一個可變的狀态(變量)時,對該狀态的所有通路都必須持有鎖才可以執行。

有些開發人員可能會違反這個規則,對其程式中共享狀态的行為進行分析,嘗試優化代碼。因為目前在android平台上實作的很多裝置實際上并不能真正提供并發執行功能(相反,隻是在多個線程之間序列化共享一個處理器),這些程式有可能會正确執行。然而,不可避免地,當移動裝置中裝備了多核處理器及大量的多層處理器緩存,這種帶有潛在bug的程式就有可能會出現錯誤,而且這種錯誤很嚴重,是間歇性的,定位特别

困難。

在java中實作并發時,最佳的方式是使用強大的java.util.concurrent庫。在這個庫中,幾乎可以找到需要的所有并發結構,它們的實作都經過了良好的優化和測試。在java中,為了實作雙向連結清單,開發人員實在沒有什麼理由不使用java.util.concurrent庫中所提供的雙向連結清單而選擇使用底層的并發構造實作一個自己的版本。

關鍵字synchronized可以用于3種場合:建立代碼塊、動态方法或靜态方法。當synchronized用于定義一個代碼塊時,該關鍵字使用一個對象的引用作為參數,也就是信号量。所有對象都可以作為信号量,但基礎資料類型不可以。

當synchronized作為動态方法的修飾符時,該關鍵字的行為類似于該方法的内容包在一個同步塊中,其鎖就是執行個體本身。下面這個例子就是對這個特點的說明:

《Android程式設計》一2.4 Java程式設計慣例

這種實作方式非常便捷,但是必須慎重使用。一個包含多個綜合方法的複雜的類,使用這種方式實作同步時,可能會自己導緻不同方法之間的鎖競争。如果多個外部線程同時嘗試通路不相關的資料片段,則最好使用多個不同的鎖分别保護這些資料塊。

如果在靜态方法中使用synchronized這個關鍵字,則它的行為表現是,方法的内容似乎是包在基于對象的類的同步代碼塊中。一個給定類的所有執行個體的所有靜态同步方法會競争該類本身的那個單獨鎖。

最後,值得注意的是,在java中對象鎖是可重入的(reentrant)。以下代碼非常安全,不會導緻任何死鎖:

《Android程式設計》一2.4 Java程式設計慣例

類java.lang.object定義了方法wait()和notify(),作為每個對象的鎖協定的一部分。因為java中的所有類都是從類object派生的,所有對象執行個體都支援通過這些方法來控制和執行個體相關的鎖。

關于java并發的底層機制的全面探讨超出了本書的讨論範疇。對這方面感興趣的開發人員可參考brian goetz的優秀書籍《java concurrency in practice》(addison-wesley professional出版社出版)。下面這個例子說明了支援兩個線程執行的必要基礎元素。當一個線程在完成其需要執行的任務時,另一個線程暫停執行:

《Android程式設計》一2.4 Java程式設計慣例
《Android程式設計》一2.4 Java程式設計慣例
《Android程式設計》一2.4 Java程式設計慣例

實際上,大多數開發人員不會用到如wait和notify這樣的底層工具,通常使用到的是java.util.concurrent包中所提供的更進階的工具。

android支援功能豐富的java标準版的java集合庫。如果仔細研讀java集合庫,會發現大多數集合都包含兩個版本:list和vector、hashmap和hashtable等。java在1.3版本中引入了全新的集合架構。這個新的集合架構完全取代了老的集合架構。然而,為了確定向後相容,老版本還是可用的。

相比于老版本的集合庫,應該盡量使用新版本的集合庫。在新版本的集合庫中,其api更統一,有更好的工具支援等。但是,可能最重要的是,遺留版本的集合庫都是同步的。這聽起來是件好事,但是正如以下例子所示,亦有其不足之處:

《Android程式設計》一2.4 Java程式設計慣例

雖然對vector的每次使用都是完全同步的,并且每次調用其方法都可以確定是原子性的,但是該程式在執行過程中還是會被中斷掉。對vector的完全同步是不夠的,當然,由于代碼通過size()方法保留了該vector的大小,即使在使用中其他線程改變了該vector的大小,它還是會使用原來該vector的大小。

由于隻是對集合對象的一個方法進行同步往往是不夠的,是以,java集合庫在新的架構中沒有做任何同步。如果調用該集合庫的代碼本身會進行同步,則在集合庫内部再進行同步完全是多餘的。

繼續閱讀