天天看點

《Android程式設計》一2.2 Java類型系統

java語言基礎資料類型有兩種:對象和基本類型(primitives)。java通過強制使用靜态類型來確定類型安全,要求每個變量在使用之前必須先聲明。舉個例子,變量i的類型聲明是int(原始32位整數),代碼如下:

int i;

這種機制和非靜态類型的語言有很大差别,非靜态語言不要求對變量進行聲明。雖然顯式類型聲明看起來較煩瑣,但其有助于編譯器對很多程式設計錯誤的預防,例如,由于變量名拼寫錯誤導緻建立了沒有用的變量,調用了不存在的方法等,顯式聲明可以徹底防止這些錯誤被生成到運作代碼中。關于java類型系統的詳細說明可以在java語言規範(java language specification)中找到。

java的基本類型不是對象,它們不支援本章稍後将會描述的對象相關的操作。基本資料類型隻能通過一些預定義的操作符來修改它們,例如,“+”、“-”、“&”、“|”及“=”等。java中的基本類型如下所示:

boolean(布爾型)

值為真或假

byte(位元組)

8位二進制整數

short(短整型)

16位二進制整數

int(整型)

32位二進制整數

long(長整型)

64位二進制整數

char(字元型)

16位無符号整數,表示一個utf-16編碼單元

float(浮點型)

32位ieee 754标準的浮點數

double(雙精度浮點型)

64位的ieee 754标準的浮點數

java是一種面向對象的語言,其重點不是基礎資料類型,而是對象(資料的組合及對這些資料的操作)。類(class)定義了成員變量(資料)和方法(程式),它們一起組成一個對象。在java中,該定義(建構對象所用的模闆)本身就是一種特定類型的對象,即類。在java中,類是類型系統的基礎,開發人員可以用它來描述任意複雜的對象,包括複雜的、專門的對象和行為。

與絕大多數面向對象的語言一樣,在java語言中,某些類型可以從其他類型繼承而來。如果一個類是從另一個類中繼承來的,那麼可以說這個類是其父類的子類(subtype或subclass),而其父類被稱為超類(supertype或superclass)。有多個子類的類可以稱為這些子類的基類(base type)。

在一個類中,方法和成員變量的作用域都可以是全局的,在對象外可以通過對這個類的執行個體的引用來通路它們。以下給出了一個非常簡單的類的例子,它隻有一個成員變量ctr和一個方法 incr():

《Android程式設計》一2.2 Java類型系統

使用關鍵字new建立一個新的對象,即某個類的執行個體,如:

trivial trivial = new trivial();

在指派運算符“=”的左邊定義了一個變量,名為trivial。該變量的類型是trivial,是以隻能賦給它類型為trivial的對象。指派符右邊為新建立的trivial類的執行個體配置設定記憶體,并對該執行個體進行實體化。指派操作符為新建立的對象變量配置設定引用。

在trivial這個類中,變量ctr的定義是絕對安全的,雖然沒有對它進行顯式初始化,這可能會讓你很吃驚。java會保證給ctr的初始值賦為0。java會確定所有的字段在對象建立時自動進行初始化。布爾值初始化為false,基本數值類型初始化為0,所有的對象類型(包括string)初始化為null。

警告: 上述的初始化指派隻适用于對象的成員變量。局部變量在被引用前必須進行初始化!

可以在定義類時,通過構造函數更好地控制對象的初始化。構造函數的定義看起來很像一個方法,差別在于構造函數沒有傳回類型且名字必須和類的完全相同:

《Android程式設計》一2.2 Java類型系統

事實上,java中的每個類都會有一個構造函數。如果沒有顯式定義的構造函數,java編譯器會自動建立一個不帶參數的構造函數。此外,如果子類的構造函數沒有顯式調用超類的構造函數,那麼java編譯器會自動隐式調用超類的無參數的構造函數。前面給出了trivial的定義(它沒有顯式地指定構造函數),實際上java編譯器會自動為它建立一個構造函數:

public trivial() { super(); }

如上所示,由于lesstrivial類顯式定義了一個構造函數,是以java不會再給它隐式地定義一個預設的構造函數。這意味着如果建立一個沒有參數的lesstrivial對象,會出現錯誤:

lesstrivial fail = new lesstrivial(); // error!!

lesstrivial ok = new lesstrivial(18); // ... works

有兩個不同的概念,需要對它們進行區分:“無參數的構造函數”和“預設的構造函數”。“預設的構造函數”是沒有給一個類定義任何構造函數時,java隐式地建立的構造函數,這個預設的構造函數剛好也是無參數的構造函數。而無參數的構造函數僅僅是沒有參數的構造函數。java不要求一個類包含沒有參數的構造函數,也不需要定義無參數的構造函數,除非存在某些特定的需求。

警告: 有一種特殊情況,需要無參數的構造函數,需要特别注意。有些庫需要能夠建立通用的新的對象。例如,junit架構,不管要測試什麼,都需要能夠建立新的測試用例。對持久性存儲或網絡連接配接進行編碼(marshal)和解碼(unmarshal)的庫也需要能夠建立新的對象。因為這些庫在運作時難以确定具體對象所需要的調用函數,它們通常要求顯式指定沒有參數的構造函數。

如果一個類有多個構造函數,則最好采用級聯(cascade)的方法建立它們,進而確定隻會有一份代碼對執行個體進行初始化,所有其他構造函數都調用它。為了便于說明,我們用一個例子來示範一下。為了更好地模拟常見情況,我們給lesstrivial類增加一個無參數的構造函數:

《Android程式設計》一2.2 Java類型系統

級聯方法(cascading method)是java中标準的用來為一些參數賦預設值的方法。一個對象的初始化代碼應該統一放在一個單一、完整的方法或構造函數中,所有其他方法或構造函數隻是簡單地調用它。在級聯方法中,在類的構造函數中必須顯式調用其超類的構造函數。

構造函數應該是簡單的,而且隻應該包含為對象的成員變量指定一緻性的初始狀态的操作。舉個例子,設計一個對象用來表示資料庫或網絡連接配接,可能會在構造函數中執行連接配接的建立、初始化和可用性的驗證操作。雖然這看起來很合理,但實際上這種方式會導緻代碼子產品化程度不夠,進而難以調試和修改。更好的設計是構造函數隻是簡單地把連接配接狀态初始化為closed,并另外建立一個方法來顯式地設定網絡連接配接。

java類object(java.lang.object)是所有類的根類,每個java對象都是一個object。如果一個類在定義時沒有顯式指定其超類,它就是object類的直接子類。object類中定義了一組方法,這些方法是所有對象都需要的一些關鍵行為的預設實作。除非子類重寫了(override)這些方法,否則都會直接繼承自object類。

object類中的wait、notify和notifyall方法是java并發支援的一部分。2.4.6節将對這些方法進行探讨。

tostring方法是對象用來建立一個自我描述的字元串的方法。tostring方法的一個有趣的使用方式是用于字元串連接配接,任何一個對象都可以和一個字元串進行連接配接。以下這個例子給出了輸出相同消息的兩種方式,它們的運作結果完全相同。在這兩個方法中,都為foo類建立了新的執行個體并調用其tostring方法,随後把結果和文本字元串連接配接起來,最後輸出結果:

《Android程式設計》一2.2 Java類型系統

在object類中,tostring方法的實作基于對象在堆中的位置,其傳回一個沒什麼用的字元串。在代碼中對tostring方法進行重寫是友善後期調試良好的開端。

clone方法和finalize方法屬于曆史遺留,隻有在子類中重寫了finalize方法時,java才會在運作時調用該方法。但是,當類顯式地定義了finalize方法時,對該類的對象執行垃圾回收時會調用該方法。java不但無法保證什麼時候會調用finalize方法,實際上,它甚至無法確定一定會調用這個方法。此外,調用finalize方法可能會重新激活一個對象!其中的道理很複雜。當一個對象不存在可用的引用時,java就會自動對它執行垃圾回收。但是,finalize方法的實作會為這個對象“建立”一個新的可用的引用,例如把實作了finalize的對象加到某個清單中!由于這個原因,finalize方法的實作阻礙了對所定義的類的很多優化。使用finalize方法,不會帶來什麼好處,卻帶來了一堆的壞處。

通過clone方法,可以不調用構造函數而直接建立對象。雖然在object類中定義了clone方法,但在一個對象中調用clone方法會導緻異常,除非該對象實作了cloneable接口。當建立一個對象的代價很高時,clone方法可以成為一種有用的優化方式。雖然在某些特定情況下,使用clone方法可能是必需的,但是通過複制構造函數(以已有的執行個體作為其唯一參數)顯得更簡單,而且在很多情況下,其代價是可以忽略的。

object類的最後兩個方法是hashcode和equals,通過這兩個方法,調用者可以知道一個對象是否和另一個對象相同。在api文檔中,object類的equals方法的定義規定了equals的實作準則。equals方法的實作應確定具有以下4個特性,而且相關的聲明必須始終為真:

自反性

x.equals(x)

對稱性

x.equals(y) == y.equals(x)

傳遞性

(x.equals(y) && y.equals(z)) == x.equals(z)

一緻性

如果x.equals(y)在程式生命周期的任意點都為真,隻要x和y值不變,則x.equals(y)就始終為真。

要滿足這4大特性,實際上需要很細緻的工作,而且其困難程度可能超出預期。常見的錯誤之一是定義一個新的類(違反了自反性),它在某些情況下等價于已有的類。假設程式使用了已有的定義了類englishweekdays的庫,假設又定義了類frenchweekdays。顯然,我們很可能會為frenchweekdays類定義equals方法,該方法和englishweekdays相應的french等值進行比較并傳回真。但是千萬不要這麼做!已有的englishweekdays類看不到新定義的frenchweekdays類,因而它也永遠都無法确定你所定義的類的執行個體是否是等值的。是以,這種方式違反了自反性!

hashcode方法和equals方法應該是成對出現的,隻要重寫了其中一個方法,另外一個也應該重寫。很多庫程式把hashcode方法作為判斷兩個對象是否等價的一種優化方式。這些庫首先比較兩個對象的哈希碼,如果這兩個對象的哈希碼不同,那麼就沒有必要執行代價更高的比較操作,因為這兩個對象一定是不同的。哈希碼算法的特點在于計算非常快速,這方面可以很好地取代equals方法。一方面,通路大型數組的每個元素來計算其哈希碼,很可能還比不上執行真正的比較操作,而另一方面,通過哈希碼計算可以非常快速地傳回0值,隻是可能不是非常有用。

java支援多态(polymorphism),多态是面向對象程式設計的一個關鍵概念。對于某種語言,如果單一類型的對象具備不同的行為,則認為該語言具備多态性。如果某個類的子類可以被賦給其基礎類型的變量,那麼就認為這個類是多态的,下面通過例子說明會更清晰。

在java中,聲明子類的關鍵字是extends。java繼承的例子如下:

《Android程式設計》一2.2 Java類型系統

ragtop是car的子類。從前面的介紹中,可以知道car是object的子類。ragtop重新定義(即重寫)了car的drive方法。car和ragtop都是car類型(但它們并不都是ragtop類型),它們的drive方法有着不同的行為。

現在,我們來示範一個多态的例子:

car auto = new car();

auto.drive();

auto = new ragtop();

盡管把ragtop類型指派給了car類型的變量,但這段代碼可以編譯通過(雖然把ragtop類型指派給car類型的變量)。它還可以正确運作,并輸出如下結果:

going down the road!

top down!

got the radio on!

auto這個變量在生命的不同時期,分别指向了兩個不同的car類型的對象引用。其中一個對象,不但是car類型,也是其子類ragtop類型。auto.drive()語句的确切行為取決于該變量目前是指向基類對象的引用還是子類對象的引用,這就是所謂的多态行為。

類似很多其他的面向對象程式設計語言,java支援類型轉換,允許聲明的變量類型為多态形式下的任意一種變量類型。

ragtop funcar;

funcar = (ragtop) auto; //error! auto is a car, not a ragtop!

ragtop funcar = (ragtop) auto; //works! auto is a ragtop

雖然類型轉換(casting)在某些情況下是必要的,但過度使用類型轉換會使得代碼很雜亂。顯然,根據多态規則,所有的變量都可以聲明為object類型,然後進行必要的轉換,但是這種方式違背了靜态類型(static typing)準則。

在上面兩個執行個體中,靜态類定義和靜态方法定義的共同點在于靜态對象在其命名空間内都是可見的,而動态對象隻能通過每個執行個體的引用才可以使用。此外,靜态對象和動态對象之間的差別則更為微妙。

靜态方法和動态方法之間的一個顯著差別在于靜态方法在子類中不能重寫。例如,下面的代碼在編譯時會出錯:

《Android程式設計》一2.2 Java類型系統

在方法park(car auto)的聲明中,car類型的對象是其唯一參數。但是在方法letsgo()中,在調用它時傳遞的參數類型是ragtop,即car的子類。同樣,變量mycar指派的類型為ragtop,方法whatsinthegarage傳回變量mycar的值。如果一個對象是ragtop類型,當調用drive方法時,它會輸出“top down!”和“got the radio on!”資訊;另一方面,因為它又是car類型,它還可以用于任何car類型可用的方法調用中。這種子類型可取代父類型是多态的一個關鍵特征,也是其可以保證類型安全的重要因素。在編譯階段,一個對象是否和其用途相容也已經非常清晰。類型安全使得編譯器能夠及早發現錯誤,這些錯誤如果隻是在運作時才出現,那麼發現這些錯誤的成本就會高很多。

java有11個關鍵字可以用作聲明的修飾符,這些修飾符會改變被聲明對象的行為,有時是很重要的改變。例如,在前面的例子中使用了多次的關鍵字:public和private。這兩個修飾符的作用是控制對象的作用域和可見性。在後面的章節中還會更詳細地介紹它們。在本節中,我們将探讨的是另外兩個修飾符,這兩個修飾符是全面了解java類型系統的基礎:final和static。

如果一個對象的聲明前面包含了final修飾符,則意味着這個對象的内容不能再被改變。類、方法、成員變量、參數和局部變量都可以是final類型。

當用final修飾類時,意味着任何為其定義子類的操作都會引發錯誤。舉個例子,string類是final類型,因為作為其内容的字元串必須是不可改變的(也就是說,建立了一個字元串後,就不能夠改變它)。如果你仔細思考一下,就會發現,確定其内容不被改變的唯一方式就是確定不能以string類型為基類來建立子類。如果能夠建立子類,例如deadlystring,就可以把deadlystring類的執行個體作為參數,并在驗證完其内容後,馬上在代碼中把該執行個體的值從“fred”改成“‘; drop table contacts;”(把惡意sql注入你的系統中,對你的資料庫進行惡意修改)!

當用final修飾方法時,它表示子類不能重寫(override)譯注1這個方法。開發人員使用final方法來設計繼承性(inheritance),子類的行為必須和實作高度相關,而且不允許改變其實作。舉個例子,一個實作了通用的緩存機制的架構可能會定義一個基類cacheableobject,程式設計人員使用該架構的子類型來建立每個新的可緩存的對象類型。然而,為了維護架構的完整性,cacheableobject可能需要計算一個緩存鍵(cache key),該緩存鍵對于各對象類型都是一緻的。在這種情況下,該緩存架構就可以把其方法computecachekey聲明為final類型。

當用final修飾變量——成員變量、參數和局部變量時,它表示一旦對該變量進行了指派,就不能再改變。這種限制是由編譯器負責保障的:不但變量的值“不會”發生變化,而且編譯器必須能夠證明它“不能”發生改變。用final修飾成員變量時,表示該成員變量的值必須在變量的聲明或者構造函數中指定。如果沒有在變量的聲明或構造函數中對final類型的成員變量進行初始化,或者試圖在任何其他地方對它進行指派,都會出現錯誤。

當用final修飾參數時,表示在這個方法内,該參數的值一直都是在調用時傳遞進來的那個值。如果對final類型的參數進行指派,就會出現錯誤。當然,由于參數值很可能是某種對象的引用,對象内部的内容是有可能會發生變化的。用關鍵字final修飾參數時僅僅表示該參數不能被指派。

注意: 在java中,參數都是按值傳遞:函數的參數就是調用時所傳遞值的一個副本。另一方面,在java中,在大部分情況下,變量是對象的引用,java隻是複制引用,而不是整個對象!引用就是所傳遞的值!

final類型的變量隻能對其指派一次。由于使用一個沒有初始化的變量在java中會出現錯誤,是以final類型的變量隻能夠被指派一次。該指派操作可以在函數結束之前任何時候進行,當然要在使用該參數之前。靜态(static)聲明可以用于類,但不能用于類的執行個體。和static相對應的是dynamic(動态)。任何沒有聲明為static的實體,都是預設的dynamic類型。下述例子是對這一特點的說明:

《Android程式設計》一2.2 Java類型系統

在這個例子中,quietstatic是一個類,ex是該類的一個執行個體的引用。靜态成員變量classmember是quietstatic的成員變量;可以通過類名引用它(quietstatic. classmember)。反之,instancemember是quietstatic類的執行個體的成員變量,通過類名引用它(quietstatic.instancemember)就會出現錯誤。這種處理機制是有道理的,因為可以存在很多個名字為instancemember的不同的變量,每個變量屬于quietstatic類的一個執行個體。如果沒有顯式指定是哪個instancemember,那麼java也不可能知道是哪個instancemember。

正如下一組語句所示,java确實允許通過執行個體引用來引用類的(靜态)變量。這容易讓人産生誤解,被認為是不好的程式設計習慣。如果這麼做,大多數編譯器和ide就會生成警告。

靜态聲明和動态聲明的含義之間的差別很微妙。最容易了解的是靜态成員變量和動态成員變量之間的差別。再次說明,靜态定義在一個類中隻有一份副本,而動态定義對于每個執行個體都有一份副本。靜态成員變量儲存的是一個類的所有成員所共有的資訊。

《Android程式設計》一2.2 Java類型系統
《Android程式設計》一2.2 Java類型系統

該程式的輸出是:

classmember: 2, instancemember: 1

在前面這個例子中,變量classmember的初始值被設定為0。在兩個不同的執行個體ex1和ex2中,分别調用incr()方法對它執行遞加操作,兩個執行個體輸出的classmember值都是2。變量instancemember在每個執行個體中,其初始值也都是被設定為0。但是,每個執行個體隻對自己的instancemember執行遞加操作,是以輸出的instancemember值都為1。

在上面的兩個執行個體中,靜态類和方法定義的相似之處在于靜态對象都是可見的,而動态對象隻能通過每個執行個體的引用才可見。然而,實際上其不同之處更複雜。

《Android程式設計》一2.2 Java類型系統

在java中,幾乎沒有理由要使用靜态方法。在java的早期實作中,動态方法調用明顯慢于靜态方法。開發人員常常傾向于使用靜态方法來“優化”其代碼。在android的即時編譯dalvik環境中,不再需要這種優化。過度使用靜态方法通常意味着架構設計不良。

靜态類和動态類之間的差別是最微妙的。應用中的絕大部分類都是靜态的。類通常是在最高層聲明和定義的——在任何代碼塊之外。預設情況下,所有這些聲明都是靜态的;相反,很多其他聲明,在某些類之外的代碼塊,預設情況下是動态的。雖然成員變量預設是動态的,其需要顯式地使用靜态修飾符才會是靜态的,但類預設是靜态的。

注意: 代碼塊(block)是指兩個大括号之間的代碼,即{和}之間的代碼。在代碼塊内所定義的一切,即變量、類型和方法等,在代碼塊内及其内嵌的代碼塊内都是可見的。而在一個代碼塊内,不止是在其中定義的類,所定義的一切在代碼塊外都是不可見的。

實際上,這完全符合一緻性要求。根據對“靜态”的定義(屬于類但不屬于類的執行個體),高層聲明應該是靜态的,因為它們不屬于任何一個類。但是,如果是在代碼塊内定義的(例如在高層類内定義),那麼類的定義預設也是動态的。是以,為了動态地聲明一個類,隻需要在另一個類内定義它。

這一點也說明了靜态類和動态類之間的差別。動态類能夠通路代碼塊内的類(因為它屬于執行個體)的執行個體成員變量,而靜态類卻無法通路。以下代碼是對這個特點的示例說明:

《Android程式設計》一2.2 Java類型系統

稍加思考,這段代碼就可了解。成員變量x是類class的執行個體的成員變量,也就是說,可以有很多名字為x的變量,每個變量都是outer的運作時執行個體的成員變量。類innertube是類outer的一部分,但不屬于任何一個outer執行個體。是以,在innertube類中無法通路outer的執行個體成員變量x。相反,由于類innerone是動态的,它屬于類outer的一個執行個體。是以可以把類innerone了解成隸屬于類outer的每個執行個體的獨立的類(雖然不是這個含義,但實際上就是這麼實作的)。是以,innerone能夠通路其所屬的outer類的執行個體的成員變量x。

類outertest說明了對于成員變量,我們可以使用類名.内部靜态類來定義,并可以使用該靜态類型的類的内部定義outer.innertube(在這個例子中,是建立該類的一個執行個體),而動态類型的類的定義隻有在類的執行個體中才可用。

在java的聲明中,如果将類及其一個或者多個方法聲明為抽象類型,則允許這個類的定義中可以不包含這些方法的實作:

《Android程式設計》一2.2 Java類型系統
《Android程式設計》一2.2 Java類型系統

不能對抽象類進行執行個體化。抽象類的子類必須提供其父類的所有抽象方法的定義,或者該子類本身也定義成抽象類。

正如前面的例子所示,抽象類可以用于實作常見的模闆模式,它提供可重用的代碼塊,支援在執行時自定義特定點。可重用代碼塊是作為抽象類實作的。子類通過實作抽象方法對模闆自定義。

其他程式設計語言(例如c++、python和perl)支援多繼承,即一個對象可以有多個父類。多繼承有時非常複雜,程式執行和預期的不同(如從不同的父類中繼承兩個相同名字的成員變量)。為了簡單起見,java不支援多繼承性。和c++、python和perl等不同,在java中,一個類隻能有一個父類。

和多繼承性不同,java支援一個類通過接口(interface)實作對多種類型的繼承。接口支援隻對類型進行定義但不實作。可以把接口想象成一個抽象類,其所有的方法也都是抽象方法。java對一個類可以實作的接口的數量沒有限制。

下面這個例子是關于java接口和實作該接口的類的示例:

《Android程式設計》一2.2 Java類型系統
《Android程式設計》一2.2 Java類型系統

再次說明,接口隻是方法的聲明,而沒有方法的實作。這種分工在日常生活中也是很常見的。假如你和同僚正在準備雞尾酒會,你可能會分派任務,讓同僚去買薄荷。當你攪拌杯子裡的東西時,你的同僚是開車去商店還是步行去後院的果汁店買薄荷和你沒有關系,重要的是你拿到了薄荷。

關于接口,再舉個例子。假設程式需要根據郵件位址排序,顯示一個聯系人清單。我們肯定會期望android的運作時庫包含一些通用的排序程式。但是,由于這些程式是通用的,它們無法知道某個特定類的執行個體期望用什麼方式來進行排序。為了使用庫中的排序程式,在類中需要定義自己的排序方法。在java中,是通過接口comparable定義排序

方法。

comparable類的對象實作方法compareto。一個對象接受另一個相同類型的對象作為參數,如果作為參數的對象大于、等于或小于原目标對象,就分别傳回不同的整數值。程式庫可以對任何comparable類型的對象進行排序。要實作對聯系人清單的排序,隻需要把聯系方式contact定義成comparable類型,實作compareto方法,就可以做到對這些聯系方式進行排序:

《Android程式設計》一2.2 Java類型系統
《Android程式設計》一2.2 Java類型系統

在類内部,collections.sort程式隻知道contacts包含一組類型為comparable的清單。它調用類的compareto方法來決定如何對這些清單進行排序。正如這個例子所說明的,接口使得開發人員可以複用通用的程式,這些程式能夠對任何實作了comparable接口的清單進行排序。除了這個簡單的示例,java接口庫中還提供了一組複雜的程式設計模式的實作。這裡強烈推薦一本優秀的書籍《effective java》,joshua bloch著(prentice hall出版社出版)。

java語言使用異常(exceptions)作為處理異常情況的簡便方式。通常情況下,這些情況是錯誤的。

舉個例子,要解析web頁面的代碼,如果不能通過網絡讀取頁面,就無法繼續執行。當然,可以先檢查網絡讀取頁面是否成功,确認成功後再繼續其他操作,如下例所示:

《Android程式設計》一2.2 Java類型系統

使用異常,程式可以更完善和健壯:

《Android程式設計》一2.2 Java類型系統
《Android程式設計》一2.2 Java類型系統
《Android程式設計》一2.2 Java類型系統

這段代碼的功能是在網絡失敗時進行重試。注意,抛出networkexception異常的點是在另一個方法readpagefromnet中。這裡提到的程式從“最近的”try-catch代碼塊恢複執行這種機制,是java中的異常處理方式。

如果在方法内的throw語句沒有和try-catch代碼塊一起使用,那麼抛出異常類似于馬上執行return(傳回)語句。不需要執行進一步的操作,傳回為空。例如,在之前的例子中,網絡擷取頁面之後的代碼,都不需要關注其前提條件(讀取到頁面)是否有得到滿足。當出現異常時,方法會立即終止,程式傳回到getactions方法。由于getactions方法也沒有包含try-catch代碼塊,它也會立即終止并傳回到它的調用函數處。

在這個例子中,當抛出networkexception異常時,程式會跳轉到catch代碼塊的第一條語句,即調用日志,記錄網絡錯誤。異常會在第一個catch語句中被捕獲,其參數是抛出的異常的類型或者是其父類。處理會從catch代碼塊的第一條語句處恢複,并依次執行後面的操作。

在這個例子中,從網絡讀取頁面時如果出現網絡錯誤将會導緻readpagefromnet方法和getpage方法都被終止。在catch代碼塊中記錄過失敗資訊後,在for循環中會重新嘗試擷取頁面,最多嘗試執行max_retries次。對java異常類樹結構有清晰的了解是很有幫助的,如圖2-1所示。

《Android程式設計》一2.2 Java類型系統

所有異常都是throwable類的子類。在代碼中,基本不需要引用throwable類。可以把throwable類當作一個抽象類型的基類,其包含兩個子類:error和exception。error類及其子類是保留類,隻用于dalvik運作時環境本身的錯誤。雖然可以寫代碼來捕獲error(或throwable),但實際上,無法捕捉到這些錯誤。這種情況的一個例子是oome,即outofmemoryexception錯誤。當dalvik系統出現記憶體溢出時,再簡單的代碼都無法繼續運作。實作一些複雜的代碼來捕捉oome,并釋放一些預配置設定的記憶體也許是可行的——也許不可行。嘗試捕捉throwable或error的代碼絕對是徒勞。

java要求在方法的聲明中包含其将要抛出的異常。在前面這個例子中,getpage聲明其抛出三個異常,因為它調用了三個方法,每個方法捕捉一個錯誤。調用getpage的方法的定義中必須指明getpage抛出的三個異常及它調用的其他方法抛出的異常。

不難想象,在這種機制中,調用樹的最高層方法會顯得多麼臃腫。最高層方法可能需要指明數10種不同類型的異常,僅僅因為它調用的方法抛出了這些異常。這個問題可以通過建立一棵和應用樹一緻的異常樹來緩解,一個方法隻需要聲明其抛出的所有異常的超類。如果建立一個名為myapplicationexception的基類,然後建立其子類mynetworkexception和myuiexception,分别用于網絡和ui子系統中,則最高層代碼隻需要處理myapplicationexception異常。

這實際上隻是緩解了部分問題。例如,假設這段網絡連接配接的代碼沒有成功地建立起網絡連接配接。随着異常在重試和其他條件選擇代碼中不斷上傳給上層代碼的過程中,有時會出現能夠說明真實問題的異常資訊被丢失的情況。例如,一個具體的資料庫異常對于嘗試預安裝電話号碼的代碼是沒有任何意義的。把該資料庫異常加到方法簽名中是毫無用處的,還不如簡單地讓所有方法聲明抛出exception異常類。

runtimeexception類是exception類的特殊子類。runtimeexception的子類被稱為“未檢查的(unchecked)”異常,不需要聲明。例如,以下代碼可以編譯通過:

在java社群中,關于何時使用及何時不使用未檢查的異常有很多争論。顯然,可以在應用中使用未檢查的異常,而從不聲明任何在你的方法簽名中的異常。一些java程式設計學派甚至推薦這種方式。然而,使用未檢查的異常,使得能夠利用編譯器來檢查代碼錯誤,這很符合“靜态類型”(static typing)的思想,以經驗和風格為指南。

2.2.10 java collections架構

java collections架構是java最強大和便捷的工具之一,它提供了可以用來表示對象的集合(collections)的對象:list、set和map。java collections架構庫的所有接口和實作都可以在java.util包中擷取。

在java.util包中,幾乎沒有什麼曆史遺留類,基本都是java collections架構的一部分,最好記住這些類,并避免定義具有相同名字的類。這些類是vector、hashtable、enumeration和dictionary。

collection接口類型

java collections庫中的5種主要對象類型都是使用接口定義的,如下所示。

collection

這是collections庫中所有對象的根類型。collection表示一組對象,這些對象不一定是有序的,也不一定是可通路的,還可能包含重複對象。在collection中,可以增加和删除對象,擷取其大小并對它執行周遊(iterate)操作(後面将對iteration作更多說明)。

list

list是一種有序的集合。list中的對象和整數從0到length-1一一映射。在list中,可能存在重複元素。list支援collection的所有操作。此外,在list中,可以通過get方法擷取索引對應的對象,反之,也可以通過indexof方法擷取某個對象的索引。還可以用add(index,e)方法改變某個特定索引所對應的元素。list的iterator(疊代器)按序依次傳回各個元素。

set

set是一個無序集合,它不包含重複元素。set也支援collection的所有操作。但是,如果在set中添加的是一個已經存在的元素,則set的大小并不會改變。

map

map和list類似,其差別在于list把一組整數映射到一組對象中,而map把一組key對象映射到一組value對象。與其他集合類一樣,在map中,可以增加和删除key-value對(鍵值對),擷取其大小并對它執行周遊操作。map的具體例子包括:把單詞和單詞定義的映射,日期和事件的映射,或url和緩存内容的映射等。

iterator

iterator(疊代器)傳回集合中的元素,其通過next方法,每次傳回一個元素。iterator是對集合中所有元素進行操作的一種較好的方式。一般不建議使用下面這種方式周遊:

《Android程式設計》一2.2 Java類型系統

collection實作方式

這些接口類型有多種實作方式,每個都有其适用的場景。最常見的實作方式包括以下

幾種。

arraylist

arraylist(數組清單)是一個支援數組特征的list。它在執行索引查找操作時很快,但是涉及改變其大小的操作的速度很慢。

linkedlist

linkedlist(連結清單)可以快速改變大小,但是查找速度很慢。

hashset

hashset是一個以hash方式實作的set。在hashset中,增、删元素,判斷是否包含某個元素及擷取hashset的大小這些操作都可以在常數級時間内完成。hashset可以

為空。

hashmap

hashmap是使用hash表作為索引,其實作了map接口。在hashmap中,增、删元素,判斷是否包含某個元素及擷取hashmap的大小這些操作都可以在常數級時間内完成。它最多隻可以包含一個空的key值,但是可以有任意個value值為空的元素。

treemap

treemap是一個有序的map。如果實作了comparable接口,則treemap中的對象是按自然序排序;如果沒有實作comparable接口,則是根據傳遞給treemap構造函數的comparator類來排序。

經常使用java的使用者隻要可能,往往傾向于使用接口類型的聲明,而不是實作類型的聲明。這是一個普遍的規則,但在java collections架構下最易于了解其中的原因。

假設有一個會傳回一個新的字元串清單的方法,其主要内容是傳遞給它的第二個參數的字元串清單,但是在傳回的新的字元串清單中,每個字元串的字首是第一個參數。該方法如下所示:

《Android程式設計》一2.2 Java類型系統

然而,這種實作方式存在一個問題:它無法在所有類型的list上都正常工作!它隻能在arraylist上正常工作。如果調用這個方法的代碼需要從arraylist改成linkedlist,就不能再使用這個方法,是以沒有理由要使用這樣的實作方式。

更好的實作方式如下所示:

《Android程式設計》一2.2 Java類型系統

這個版本的靈活性更強,因為它沒有把方法綁定到特定的實作。該方法隻依賴于參數實作了某個接口,并不關心是如何實作的。使用接口類型作為參數,它确切地知道自己要做什麼。

事實上,還可以進一步對該版本進行改進,把參數和傳回類型設定成collection類型。

java泛型(java generics)

在java中,泛型是相當大且複雜的一個話題。有些書整本都在探讨這個主題。本節介紹java泛型中最常用的設定,即collections library(集合庫),但是不會詳細探讨它們。

在引入java泛型之前,是無法對容器類的内容作靜态類型化的(statically type)。我們經常看到這樣的代碼:

《Android程式設計》一2.2 Java類型系統

以上程式的問題非常明顯,uselist方法不能保證makelist方法建立了一個thing類型的對象清單。java編譯器不能驗證uselist中的轉換可以工作,代碼可能會在運作時崩潰。

java泛型解決了這個問題,但其代價是使得實作變得較為複雜。以下是對上面代碼的改寫,在其中加入了泛型

容器中的對象類型是在尖括号(<>)中指定,它是容器類型的一部分。注意,在uselist中不再需要類型轉換,因為編譯器知道第一個參數是thing類型的list。

泛型描述可能會變得非常煩瑣冗長,下面這樣的聲明是很常見的:

java是一種支援垃圾收集的語言,這意味着代碼不需要對記憶體進行管理。相反,我們的代碼可以建立新的對象,可以配置設定記憶體,當不再需要這些對象時,隻是停止使用這些對象而已。dalvik運作時會自動删除這些對象,并适當地執行記憶體壓縮。

在不遠的過去,開發人員不得不為垃圾收集器擔心,因為垃圾收集器可能會暫停下所有的應用處理以恢複記憶體,導緻應用長時間、不可預測地、周期性地沒有響應。很多開發人員,早期那些使用java及後來使用j2me的開發人員,都還記得那些技巧、應對方式及不成文的規則來避免由早期垃圾收集器造成的長時間停頓和記憶體碎片。垃圾收集機制在這些年有了很大改進。dalvik明顯不存在這些問題。建立新的對象基本上沒有開銷,隻有那些對ui響應要求非常高的應用程式(例如遊戲)需要考慮垃圾收集造成的程式

暫停。

繼續閱讀