Java類型系統(tǒng)
Java語言基礎數(shù)據(jù)類型有兩種:對象和基本類型(Primitives)。Java通過強制使用靜態(tài)類型來確保類型安全,要求每個變量在使用之前必須先聲明。
這種機制和非靜態(tài)類型的語言有很大差別,非靜態(tài)語言不要求對變量進行聲明。雖然顯式類型聲明看起來較繁瑣,但其有助于編譯器對很多編程錯誤的預防,例如,由于變量名拼寫錯誤導致創(chuàng)建了沒有用的變量,調(diào)用了不存在的方法等。顯式聲明可以徹底防止這些錯誤被生成到運行代碼中。關于Java類型系統(tǒng)的詳細說明可以在Java語言規(guī)范(Java Language Specification)中找到。
基本類型
Java的基本類型不是對象,它們不支持對象相關的操作。基本數(shù)據(jù)類型只能通過一些預定義的操作符來修改它們。Java中的基本類型如下:
boolean(布爾型):值為true或false
byte(字節(jié)):8位二進制整數(shù)
short(短整型):16位二進制整數(shù)
int(整型):32位二進制整數(shù)
long(長整型):64位二進制整數(shù)
char(字符型):16位無符號整數(shù),表示一個UTF-16編碼單元
float(浮點型):32位IEEE-754標準的浮點數(shù)
double(雙精度浮點型):64位IEEE-754標準的浮點數(shù)
對象和類
Java是一種面向?qū)ο蟮恼Z言,其重點不是基礎數(shù)據(jù)類型,而是對象(數(shù)據(jù)的組合及對這些數(shù)據(jù)的操作)。類(class)定義了成員變量(數(shù)據(jù))和方法(程序),它們一起組成一個對象。在Java中,該定義(構(gòu)建對象所用的模板)本身就是一種特定類型的對象,即類。在Java中,類是類型系統(tǒng)的基礎,開發(fā)人員可以用它來描述任意復雜的對象,包括復雜的、專門的對象和行為。
與絕大多數(shù)面向?qū)ο蟮恼Z言一樣,在Java語言中,某些類型可以從其他類型繼承而來。如果一個類是從另外一個類中繼承來的,那么可以說這個類是其父類的子類(subtype或subclass),而其父類稱為超類(supertype或superclass)。有多個子類的類可以稱為這些子類的基類(base type)。
在一個類中,方法和成員變量的作用域都可以是全局的,在對象外可以通過對這個類的實例的引用來訪問他們。
以下給出了一個非常簡單的類的例子,它只有一個成員變量ctr和一個方法incr():
public class Trivial {
/* a field: its scope is the entire class */
private long ctr;
/* Modify the field */
public void incr() {
ctr++;
}
}
對象的創(chuàng)建
使用關鍵字new創(chuàng)建一個新的對象,即某個類的實例,如:
Trivial trivial = new Trivial();
在復制運算符"="的左邊定義了一個變量,名為trivial。該變量的類型是Trivial,因此只能賦給它類型為Trivial的對象。賦值符右邊為新創(chuàng)建的Trivial類的實例分配內(nèi)存,并對該實例進行實體化。賦值操作符為新創(chuàng)建的對象變量分配引用。
在Trivial這個類中,變量ctr的定義是絕對安全的,雖然沒有對它進行顯式初始化。Java會保證給ctr的初始化值為0。Java會確保所有的字段在對象創(chuàng)建時自動進行初始化。布爾值初始化為false,基本數(shù)值類型初始化為0,所有的對象類型(包括String)初始化為null。上述的初始化賦值只適用于對象的成員變量,局部變量在被引用之前必須進行初始化。
可以在定義類時,通過構(gòu)造函數(shù)更好地控制對象的初始化。構(gòu)造函數(shù)的定義看起來很像一個方法,區(qū)別在于構(gòu)造函數(shù)沒有返回類型且名字必須和類名完全相同:
public class LessTrivial {
/* a field: its scope is the entire class */
private long ctr;
/* Constructor: initialize the fields */
public LessTrivial(long initCtr) {
ctr = initCtr;
}
/* Modify the field */
public void incr() {
ctr++;
}
}
事實上,Java中的每個類都會有一個構(gòu)造函數(shù)。如果沒有顯式定義的構(gòu)造函數(shù),Java編譯器會自動創(chuàng)建一個不帶參數(shù)的構(gòu)造函數(shù)。此外,如果子類的構(gòu)造函數(shù)沒有顯式調(diào)用超類的構(gòu)造函數(shù),那么Java編譯器會自動隱式調(diào)用超類的無參數(shù)的構(gòu)造函數(shù)。前面給出了Trivial的定義(它沒有顯式地指定構(gòu)造函數(shù)),實際上Java編譯器會自動為它創(chuàng)建一個構(gòu)造函數(shù):
public Trivial() { super(); }
如上所示,由于LessTrivial類顯式定義了一個構(gòu)造函數(shù),因此Java不會再給它隱式地定義一個默認的構(gòu)造函數(shù)。這意味著如果創(chuàng)建一個沒有參數(shù)的LessTrivial對象,會出現(xiàn)錯誤:
LessTrivial fail = new LessTrivial(); // Error!!
LessTrivial ok = new LessTrivial(18); // ...works
有兩個不同的概念,需要對它們進行區(qū)分:“無參數(shù)的構(gòu)造函數(shù)”和“默認的構(gòu)造函數(shù)”。“默認的構(gòu)造函數(shù)”是沒有給一個類定義任何構(gòu)造函數(shù)時,Java隱式地創(chuàng)建的構(gòu)造函數(shù),這個默認的構(gòu)造函數(shù)剛好也是無參數(shù)的構(gòu)造函數(shù)。而“無參數(shù)的構(gòu)造函數(shù)”僅僅是沒有參數(shù)的構(gòu)造函數(shù)。Java不要求一個類包含沒有參數(shù)的構(gòu)造函數(shù),也不需要定義無參數(shù)的構(gòu)造函數(shù),除非存在某些特定的需求。
如果一個類有多個構(gòu)造函數(shù),則最好采用級聯(lián)(cascade)的方法創(chuàng)建它們,從而確保只會有一份代碼對實例進行初始化,所有其他構(gòu)造函數(shù)都調(diào)用它。為了便于說明,我們用一個例子來演示一下。為了更好地模擬常見情況,我們給LessTrivial類增加一個無參數(shù)的構(gòu)造函數(shù):
public class LessTrivial {
/* a field: its scope is the entire class */
private long ctr;
/* Constructor: init counter to 0 */
public LessTrivial() {
this(0);
}
/* Constructor: initialize the fields */
public LessTrivial(long initCtr) {
ctr = initCtr;
}
/* Modify the field */
public void incr() {
ctr++;
}
}
級聯(lián)方法(cascading method)是Java中標準的用來為一些參數(shù)賦默認值的方法。一個對象的初始化代碼應該統(tǒng)一放在一個單一、完整的方法或構(gòu)造函數(shù)中,所有其他方法或構(gòu)造函數(shù)只是簡單地調(diào)用它。在級聯(lián)方法中,在類的構(gòu)造函數(shù)中必須顯式調(diào)用其超類的構(gòu)造函數(shù)。
構(gòu)造函數(shù)應該是簡單的,而且只應該包含為對象的成員變量指定一致性的初始狀態(tài)的操作。舉個列子,設計一個對象用來表示數(shù)據(jù)庫或網(wǎng)絡連接,可能會在構(gòu)造函數(shù)中執(zhí)行連接的創(chuàng)建、初始化和可用性的驗證操作。雖然這看起來很合理,但實際上這種方法會導致代碼模塊化程度不夠,從而難以調(diào)試和修改。更好的設計是構(gòu)造函數(shù)只是簡單地把連接狀態(tài)初始化為closed,并另外創(chuàng)建一個方法來顯式地設置網(wǎng)絡連接。
對象類及其方法
Java類Object(java.lang.Object)是所有類的根類,每個Java對象都是一個Object。如果一個類在定義時沒有顯式指定其超類,它就是Object類的直接子類。Object類中定義了一組方法,這些方法是所有對象都需要的一些關鍵行為的默認實現(xiàn)。除非子類重寫了(override)這些方法,否則都會直接繼承自Object類。
Object類中的wait、notify和notifyAll方法是Java類并發(fā)支持的一部分。
toString方法是對象用來創(chuàng)建一個自我描述的字符串的方法。toString方法的一個有趣的使用方式是用于字符串連接,任何一個對象都可以和一個字符串進行連接。以下這個例子給出了輸出相同消息的兩種方法,它們的運行結(jié)果完全相同。在這兩個方法中,都為Foo類創(chuàng)建了新的實例并調(diào)用其toString方法,隨后把結(jié)果和文本字符串連接起來,隨后輸出結(jié)果:
System.out.println("This is a new foo: " + new Foo());
System.out.println("This is a new foo: ".concat((new Foo()).toString()));
在Object類中,toString方法的實現(xiàn)基于對象在堆中的位置,其返回一個沒什么用的字符串。在代碼中對toString方法進行重寫是方便后期調(diào)試良好的開端。
clone方法和finalize方法屬于歷史遺留,只有在子類中重寫finalize方法時,Java才會在運行時調(diào)用該方法。但是,當類顯式地定義了finalize方法時,對該類的對象指向垃圾回收時會調(diào)用該方法。Java不但無法保證什么時候會調(diào)用finalize方法,實際上,它甚至無法確保一定會調(diào)用這個方法。此外,調(diào)用finalize方法可能會重新激活一個對象!其中的道理很復雜。當一個對象不存在可用的引用時,Java就會自動對它執(zhí)行垃圾回收。但是finalize方法的實現(xiàn)會為這個對象“創(chuàng)建”一個新的可用的引用,例如把實現(xiàn)了finalize的對象加到某個列表中!由于這個原因,finalize方法的實現(xiàn)阻礙了對所定義的類的很多優(yōu)化。使用finalize方法,不會帶來什么好處,卻帶來了一堆壞處。
通過clone方法,可以不調(diào)用構(gòu)造函數(shù)而直接創(chuàng)建對象。雖然在Object類中定義了clone方法,但在一個對象中調(diào)用clone方法會導致異常,除非該對象實現(xiàn)了Cloneable接口。當創(chuàng)建一個對象的代價很高時,clone方法可以成為一種有用的優(yōu)化方式。雖然在某些特定情況下,使用clone方法可能是必須的,但是通過復制構(gòu)造函數(shù)(以已有的實例作為其唯一參數(shù))顯得更簡單,而且在很多情況下,其代價是可以忽略的。
Object類的最后兩個方法是hashCode和equals,通過這兩個方法,調(diào)用者可以知道一個對象是否和另一個對象相同。
在API文檔中,Object類的equals方法的定義規(guī)定了equals的實現(xiàn)準則。equals方法的實現(xiàn)應確保具有以下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方法應該是成對出現(xiàn)的,只要重寫了其中一個方法,另外一個也應該重寫。很多庫程序把hashCode方法作為判斷兩個對象是否等價的一種優(yōu)化方式。這些庫首先比較兩個對象的散列碼,如果這兩個對象的散列碼不同,那么就沒有必要執(zhí)行代價更高的比較操作,因為這兩個對象一定是不同的。散列碼算法的特點在于計算非常快速,這方法可以很好地取代equals方法。一方面,訪問大型數(shù)組的每個元素來計算其散列碼,很可能還比不上執(zhí)行真正的比較操作,而另一方面,通過散列碼計算可以非常快速地返回0值,只是可能不是非常有用。
對象、繼承和多態(tài)
Java支持多態(tài)(polymorphism),多態(tài)是面向?qū)ο缶幊痰囊粋€關鍵概念。對于某種語言,如果單一類型的對象具備不同的行為,則認為該語言具備多態(tài)性。如果某個類的子類可以被賦給其基礎類型的變量,那么就認為這個類是多態(tài)的。
在Java中,聲明子類的關鍵字是extends。Java繼承的例子如下:
public class Car {
public void drive() {
System.out.println("Going down the road!");
}
}
public class Ragtop extends Car {
// override the parent's definition
public void drive() {
System.out.println("Top dowm!");
// optionally use a superclass method
super.drive();
System.out.println("Got the radio on!");
}
}
Ragtop是Car的子類。從前面的介紹中,可以知道Car是Object的子類。Ragtop重新定義(即重寫)了Car的drive方法。Car和Ragtop都是Car類(但它們并不都是Ragtop類型),它們的drive方法有著不同的行為。
現(xiàn)在,我們來演示一個多態(tài)的例子:
Car auto = new Car();
auto.drive();
auto = new Ragtop();
auto.drive();
盡管吧Ragtop類型賦值給了Car類型的變量,但這段代碼可以編譯通過(雖然吧Ragtop類型賦值給Car類型的變量)。它還可以正確運行,并輸出如下結(jié)果:
Going down the road!
Top dowm!
Going down the road!
Got the radio on!
auto這個變量在生命的不同時期,分別指向了兩個不同的Car類型的對象引用。其中一個對象,不但是Car類型,也是其子類型Ragtop類型。auto.drive()語句的確切行為取決于該變量當前是指向基類對象的引用還是子類對象的引用,這就是所謂的多態(tài)行為。
類似很多其他的面向?qū)ο缶幊陶Z言,Java支持類型轉(zhuǎn)換,允許聲明的變量類型為多態(tài)形式下的任意一種變量類型。
Ragtop funCar;
Car auto = new Car();
funCar = (Ragtop)auto; // ERROR! auto is a Car, not a Ragtop!
auto.drive();
auto = new Ragtop();
Ragtop funCar = (Ragtop) auto; // Works! auto is a Ragtop
auto.drive();
雖然類型轉(zhuǎn)換(casting)在某種情況下是必要的,但過度使用類型轉(zhuǎn)換會使得代碼很雜亂。顯然,根據(jù)多態(tài)規(guī)則,所有的變量都可以聲明為Object類型,然后進行必要的轉(zhuǎn)換,但是這種方式違背了靜態(tài)類型(static typing)準則。
Java限制方法的參數(shù)(即真正參數(shù))是多態(tài)的,表示形參所指向的對象類型。同樣,方法的返回值也是多態(tài)的,返回聲明的對象類型。舉個例子,繼續(xù)以之前的Car為例,以下代碼片段可以正常編譯和運行:
public class JoyRide {
private Car myCar;
public void park(Car auto) {
myCar = auto;
}
public Car whatsInTheGarage() {
return myCar;
}
public void letsGo() {
park(new Ragtop());
whatsInTheGarage().drive();
}
public static void main(String[] args) {
JoyRide joyRide = new JoyRide();
joyRide.letsGo();
}
}
在方法park的聲明中,Car類型的對象是其唯一參數(shù)。但是在方法letsGo中,在調(diào)用它時傳遞的參數(shù)類型是Ragtop,即Car類型的子類。同樣,變量myCar賦值的類型為Ragtop,方法whatsInTheGarage返回類型變量myCar的值。如果一個對象是Ragtop類型,當調(diào)用drive方法時,他會輸出"Top down!"和"Got the radio on!"信息;另一方面,因為它又是Car類型,它還可以用于任何Car類型可用的方法調(diào)用中。這種子類型可取代父類型是多態(tài)的一個關鍵特征,也是其可以保證類型安全的重要因素。在編譯階段,一個對象是否和其用途兼容也已經(jīng)非常清晰。類型安全使得編譯器能夠及早發(fā)現(xiàn)錯誤,這些錯誤如果只是在運行時才發(fā)現(xiàn),那么發(fā)現(xiàn)這些錯誤的成本就會高很多。
Final聲明和Static聲明
Java有11個關鍵字可以用作聲明的修飾符,這些修飾符會改變被聲明對象的行為,有時是很重要的改變。例如,在前面的例子中使用了多次的關鍵字:public和private。這兩個修飾符的作用是控制對象的作用域和可見性。在后面的章節(jié)中會更詳細介紹它們。在本節(jié)中,我們將探討的是另外兩個修飾符,這兩個修飾符是全面理解Java類型系統(tǒng)的基礎:final和static。
如果一個對象的聲明前面包含了final修飾符,則意味著這個對象的內(nèi)容不能在被改變。類、方法、成員變量、參數(shù)和局部變量都可以是final類型。
當用final修飾類時,意味著任何為其定義子類的操作都會引發(fā)錯誤。舉個例子,String類是final類型,因為作為其內(nèi)容的字符串必須是不可改變的(也就是說,創(chuàng)建了一個字符串后,就不能夠改變它)。如果你仔細考慮一下,就會發(fā)現(xiàn),確保其內(nèi)容不被改變的唯一方式就是確保不能以String類型為基類來創(chuàng)建子類。如果能夠創(chuàng)建子類,例如DeadlyString,就可以吧DeadlyString類的實例作為參數(shù),并在驗證完其內(nèi)容后,馬上在代碼中把該實例的值從"fred"改成"';DROP TABLE contacts;"(把惡意SQL注入到你的系統(tǒng)中,對你的數(shù)據(jù)庫進行惡意修改)!
當用final修飾方法時,它表示子類不能重寫(override)這個方法。開發(fā)人員使用final方法來設計繼承性(inheritance),子類的行為必須和實現(xiàn)高度相關,而且不允許改變其實現(xiàn)。舉個例子,一個實現(xiàn)了通用的緩存機制的框架可能會定義一個基類CacheableObject,編程人員使用該框架的子類型來創(chuàng)建每個新的可緩存的對象類型。然而,為了維護框架的完整性,CacheableObject可能需要計算一個緩存鍵(cache key),該緩存鍵對于各對象類型都是一致的。在這種情況下,該緩存框架就可以把其方法computeCacheKey聲明為final類型。
當用final修飾變量——成員變量、參數(shù)和局部變量是,它表示一旦對該變量進行了賦值,就不能再改變。這個限制是由編譯器負責保障的:不但變量的值"不會"發(fā)生改變,而且編譯器必須能夠證明它"不能"發(fā)生改變。用final修飾成員變量時,表示該成員變量的賦值必須在變量的聲明或構(gòu)造函數(shù)中指定。如果沒有在變量的聲明或構(gòu)造函數(shù)中對final類型的成員變量進行初始化,或者試圖在任何其他地方對它進行賦值,都會出現(xiàn)錯誤。
當用final修飾參數(shù)時,表示在這個方法內(nèi),該參數(shù)的值一直都是在調(diào)用時傳遞進來的那個值。如果對final類型的參數(shù)進行賦值,就會出現(xiàn)錯誤。當然,由于參數(shù)值很可能是某個對象的引用,對象內(nèi)部的內(nèi)容是有可能發(fā)生變化的。用關鍵字final修飾參數(shù)時,僅僅表示該參數(shù)不能被賦值。
注意:在Java中,參數(shù)都是按值傳遞:函數(shù)的參數(shù)就是調(diào)用時所傳遞值的一個副本。另外,在Java中,在大部分情況下,變量是對象的引用,Java只是復制引用,而不是整個對象!引用就是所傳遞的值!
final類型的變量只能對其賦值一次。由于使用一個沒有初始化的變量在Java中會出現(xiàn)錯誤,因此final類型的變量只能夠被賦值一次。該賦值操作可以在函數(shù)結(jié)束之前任何時候進行,當然要是在使用該參數(shù)之前。
靜態(tài)(static)聲明可以用于類,但不能用于類的實例。和static相對應的是dynamic(動態(tài))。任何沒有聲明為static的實體,都默認的dynamic類型。任何沒有聲明為static的實體,都是默認的dynamic類型。下述例子是對這一特點的說明:
public class QuietStatic {
public static int classMember;
public int instanceMember;
}
public class StaticClient {
public static test() {
QuietStatic.classMember++;
QuietStatic.instanceMember++; // ERROR!!
QuietStatic ex = new QuietStatic();
ex.classMember++; // WARNING!!
ex.instanceMember++;
}
}
在這個例子中,QuietStatic是一個類,ex是該類的一個實例的引用。靜態(tài)成員變量classMember是QuietStatic的成員變量,可以通過類名引用它(QuietStatic.classMember)。反之,instanceMember是QuietStatic類的實例的成員變量,通過類名引用它(QuietStatic.instanceMember)就會出現(xiàn)錯誤。這種處理機制是有道理的,因為可以存在很多個名字為instanceMember的不同的變量,每個變量屬于QuietStatic類的一個實例。如果沒有顯式指定是哪個instanceMember,那么Java也不可能知道是哪個instanceMember。
正如下一組語句所示,Java確實允許通過實例引用來引用類的(靜態(tài))變量。這容易讓人產(chǎn)生誤解,被認為是不好的編程習慣。如果這么做,大多數(shù)編譯器和IDE就會生成警告。
靜態(tài)聲明和動態(tài)聲明的含義之間的區(qū)別很微妙。最容易理解的是靜態(tài)成員變量和動態(tài)成員變量之間的區(qū)別。再次說明,靜態(tài)定義在一個類中只有一份副本,而動態(tài)定義對于每個實例都有一份副本。靜態(tài)成員變量保存的是一個類的所有成員所共有的信息。
public class LoudStatic {
private static int classMember;
private int instanceMember;
public void incr() {
classMember++;
instanceMember++;
}
@Override public String toString() {
return "classMember: " + classMember
+ ", instanceMember: " + instanceMember;
}
public static void main(String[] args) {
LoudStatic ex1 = new LoudStatic();
LoudStatic ex2 = new LoudStatic();
ex1.incr();
ex2.incr();
System.out.println(ex1);
System.out.println(ex2);
}
}
該程序的輸出是:
classMember: 2, instanceMember: 1
classMember: 2, instanceMember: 1
在前面這個例子中,變量classMember的初始化值被設置為0。在兩個不同的實例ex1和ex2中,分別調(diào)用incr()方法對它們執(zhí)行遞加操作,兩個實例輸出的classMember值都是2。變量instanceMember在每個實例中,其初始化也都是被設置為0。但是,每個實例只對自己的instanceMember執(zhí)行遞加操作,因此輸出的instanceMember值都為1。
在上面兩個實例中,靜態(tài)類定義和靜態(tài)方法定義的共同點在于靜態(tài)對象在其命名空間內(nèi)都是可見的,而動態(tài)對象只能通過每個實例的引用才可見。此外,相比之下,靜態(tài)對象和動態(tài)對象的區(qū)別則更為微妙。
靜態(tài)方法和動態(tài)方法之間的一個顯著區(qū)別在于靜態(tài)方法在子類中不能重寫。舉個例子,下面的代碼在編譯時會出錯:
public class Star {
public static void twinkle() { }
}
public class Arcturus extends Star {
public void twinkle() {} // ERROE!!
}
public class Rigel {
// this one works
public void twinkle() {
Star.twinkle();
}
}
在Java中,幾乎沒有理由要使用靜態(tài)方法。在Java的早期實現(xiàn)中,動態(tài)方法調(diào)用明顯慢于靜態(tài)方法。開發(fā)人員常常傾向于使用靜態(tài)方法來“優(yōu)化”其代碼。在Android的即時編譯Dalvik環(huán)境中,不再需要這種優(yōu)化。過度使用靜態(tài)方法通常意味著架構(gòu)設計不良。
靜態(tài)類和動態(tài)類之間的區(qū)別是最微妙的。應用中的絕大部分類都是靜態(tài)的。類通常是在最高層聲明和定義的——在任何代碼塊之外。默認情況下,所有的這些聲明都是靜態(tài)的;相反,很多其他聲明,在某些類之外的代碼塊,默認情況下是動態(tài)的。雖然成員變量默認是動態(tài)的,其需要顯示地使用靜態(tài)修飾符才會是靜態(tài)的,但類默認是靜態(tài)的。
實際上,這完全符合一致性要求。根據(jù)對“靜態(tài)”的定義(屬于類但不屬于類的實例),高層聲明應該是靜態(tài)的,因為他們不屬于任何一個類。但是,如果是在代碼塊內(nèi)定義的(例如在高層類內(nèi)定義),那么類的定義默認也是動態(tài)的。因此,為了動態(tài)地聲明一個類,只需要在另一個類內(nèi)定義它(翻譯不順暢???)。
這一點也說明了靜態(tài)類和動態(tài)類之間的區(qū)別。動態(tài)類能夠訪問代碼塊內(nèi)的類(因為它屬于實例)的實例成員變量,而靜態(tài)類卻無法訪問。以下代碼是對這個特點的示例說明:
public class Outer {
public int x;
public class InnerOne {
public int fn() { return x; }
}
public static class InnerTube {
public int fn() {
return x; // ERROR!!
}
}
}
public class OuterTest {
public void test() {
new Outer.InnerOne(); // ERROR!!!
new Outer.InnerTube();
}
}
稍加思考,這段代碼就可理解。成員變量x是類Class的實例的成員變量,也就是說,可以有很多名字為x的變量,每個變量都是Outer的運行時實例的成員變量。類InnerTube是類Outer的一部分,但不屬于任何一個Outer實例。因此,在InnerTube中午飯訪問Outer的實例成員變量x。相反,由于類InnerOne是動態(tài)的,它屬于類Outer的一個實例。因此可以把類InnerOne理解成隸屬于類Outer的每個實例的獨立的類(雖然不是這個含義,但實際上就是這么實現(xiàn)的)。因此,InnerOne能夠訪問其所屬的Outer類的實例的成員變量x。
類OuterTest說明了對于成員變量,我們可以使用類名.內(nèi)部靜態(tài)類來定義,并可以使用該靜態(tài)類型的類的內(nèi)部定義Outer.InnerTube(在這個例子中,是創(chuàng)建該類的一個實例),而動態(tài)類型的類的定義只有在類的實例中才可用。
抽象類
在Java的聲明中,如果將類及其一個或者多個方法聲明為抽象類型,則允許這個類的定義中可以不包括這些方法的實現(xiàn):
public abstract class TemplatedService {
public final void service() {
// subclasses prepare in their own ways
prepareService();
// ... but they all run the same service
runService();
}
public abstract void prepareService();
private final void runService() {
// implementation of the service...
}
}
public class ConcreteService extends TemplatedService {
void prepareService() {
// set up for the service
}
}
不能對抽象類進行實例化。抽象類的子類必須提供其父類的所有抽象方法的定義,或者該子類本身也定義成抽象類。
抽象類可以用于實現(xiàn)常見的模板模式,它提供可重用的代碼塊,支持在執(zhí)行時自定義特定點。可重用代碼塊是作為抽象類實現(xiàn)的。子類通過實現(xiàn)抽象方法對模板自定義。
接口
其他編程語言(例如C++、Python和Perl)支持多繼承,即一個對象可以有多個父類。多繼承有時非常復雜,程序執(zhí)行和預期的不同(如從不同的父類中繼承兩個相同名字的成員變量)。為了方便起見,Java不支持多繼承。和C++、Python、Perl等不同,在Java中,一個類只能有一個父類。
和多繼承性不同,Java支持一個類通過接口(interface)實現(xiàn)對多種類型的繼承。
接口支持只對類型進行定義但不實現(xiàn)。可以把接口想象成一個抽象類,其所有的方法都是抽象方法。Java對一個類可以實現(xiàn)的接口的數(shù)量沒有限制。
下面這個例子是關于Java接口和實現(xiàn)該接口的類的示例:
public interface Growable {
// declare the signatrue but not the implementation
void grow(Fertilizer food, Water water);
}
public interface Eatable {
// another signature with no implementation
void munch();
}
// An implementing class must implement all interface methods
public class Bean implements Growable, Eatable {
@Override
public void grow(Fertilizer food, Water water) {
// ...
}
@Override
public void munch() {
// ...
}
}
接口只是方法的聲明,而沒有方法的實現(xiàn)。
異常
Java Collections框架
Java Collections框架是Java最強大和便捷的工具之一,它提供了可以用來表示對象的集合(collections)的對象:list、set和map。Java Collections框架庫的所有接口和實現(xiàn)都可以在java.util包中獲取。
在java.util包中,幾乎沒有什么歷史遺留類,基本都是Java Collections框架的一部分,最好記住這些類,并避免定義具有相同名字的類。這些類是Vector、Hashtable、Enumeration和Dictionary。
Collection接口類型
Java Collections庫中的5種主要對象類型都是使用接口定義的,如下所示。
Collection:這是Collections庫中所有對象的根類型。Collection表示一組對象,這些對象不一定是有序的,也不一定是可訪問的,還可能包含重復對象。在Collection中,可以增加和刪除對象,獲取其大小并對它指向遍歷(iterate)操作。
List:List是一種有序的集合。List中的對象和整數(shù)從0到length-1一一映射。在List中,可能存在重復元素。List支持Collection的所有操作。此外,在List中,可以通過get方法獲取索引對應的對象,反之,也可以通過indexOf方法獲取某個對象的索引。還可以用add(index,e)方法改變某個特定索引對應的元素。List的iterator(迭代器)按序依次返回各個元素。
Set:Set是一個無序集合,它不包含重復元素。Set也支持Collection的所有操作。但是,如果在Set中添加的是一個已經(jīng)存在的元素,則Set的大小并不會改變。
Map:Map和List類似,其區(qū)別在于List把一組整數(shù)映射到一組對象中,而Map把一組key對象映射到一組value對象。與其他集合類一樣,在Map中,可以增加和刪除key-value對(鍵值對),獲取其大小并對它執(zhí)行遍歷操作。Map的具體例子包括:把單詞和單詞定義的映射,日期和事件的映射,或URL和緩存內(nèi)容的映射等。
Iterator:Iterator(迭代器)返回集合中的元素,其通過next方法,每次返回一個元素。Iterator是對集合黃總所有元素進行操作的一種較好的方式。
Collection實現(xiàn)方法
這些接口類型有多種實現(xiàn)方式,每個都有其適用的場景。最常見的實現(xiàn)方式包括以下幾種。
ArrayList:ArrayList(數(shù)組列表)是一種支持數(shù)組特征的List。它在執(zhí)行索引查找操作時很快,但是涉及改變其大小的操作的速度很慢。
LinkedList:LinkedList(鏈表)可以快速改變大小,但是查找速度很慢。
HashSet:HashSet是一個以hash方式實現(xiàn)的set。在HashSet中,增、刪元素,判斷是否包含某個元素及獲取HashSet的大小這些操作都可以在常數(shù)級時間內(nèi)完成。HashSet可以為空。
HashMap:HashMap是使用hash表作為索引,其實現(xiàn)了Map接口。在HashMap中,增、刪元素,判斷是否包含某個元素及獲取HashMap的大小這些操作都可以在常數(shù)級時間內(nèi)完成。它最多只可以包含一個空的key值,但是可以包含任意個value值為空的元素。
TreeMap:TreeMap是一個有序的Map。如果實現(xiàn)了Comparable接口,則TreeMap中的對象是按照自然序排序;如果沒有實現(xiàn)Comparable接口,則是根據(jù)傳遞給TreeMap構(gòu)造函數(shù)的Comparator類來排序。
經(jīng)常使用Java的用戶只要可能,往往傾向于使用接口類型的聲明,而不是實現(xiàn)類型的聲明。這是一個普遍的規(guī)則,但在Java Collections框架下最易于理解其中的原因。
Java泛型
垃圾收集
Java是一種支持垃圾收集的語言,這意味著代碼不需要對內(nèi)存進行管理。相反,我們的代碼可以創(chuàng)建新的對象,可以分配內(nèi)存,當不再需要這些對象時,只是停止使用這些對象而已。Dalvik運行時會自動刪除這些對象,并適當?shù)貓?zhí)行內(nèi)存壓縮。
在不遠的過去,開發(fā)人員不得不為垃圾收集器擔心,因為垃圾收集器可能會暫停下所有的應用處理以恢復內(nèi)存,導致應用長時間、不可預測地、周期性地沒有響應。很多開發(fā)人員,早起那些使用Java以及后來使用J2ME的開發(fā)人員,都還記得那些技巧、應對方式及不成文的規(guī)則來避免由早期垃圾收集器造成的長時間停頓和內(nèi)存碎片。垃圾收集器機制在這些年有了很大改進。Dalvik明顯不存在這些問題。創(chuàng)建新的對象基本上沒有開銷,只有那些對UI響應要求非常高的應用程序(例如游戲)需要考慮垃圾收集造成的程序暫停。
作用域
作用域決定了程序中的變量、方法和其他符號的可見范圍。任何符號在其作用域外都是完全不可見的,不能被使用。
Java包
Java包提供了一種機制,它把相關類型分組到一個全局唯一的命名空間。這種分組機制可以防止在一個包的命名空間內(nèi)的標識符合其他開發(fā)人員在其他命名空間內(nèi)創(chuàng)建和使用的標識符沖突。
一個典型的Java程序有很多Java包的代碼組成。典型的Java運行時環(huán)境提供了如java.lang和java.util.這樣的包。此外,程序可能會依賴于其他通用的庫,類似于org.apache樹。傳統(tǒng)上,應用代碼(你所創(chuàng)建的代碼)在你所創(chuàng)建的包內(nèi),包名是通過反轉(zhuǎn)域名并附加程序名字生成的。因此,如果你的域名是androidhero.com,你的包所屬的樹的根是com.androidhero,則可以把代碼放到如com.androidhero.awesomeprogram和com.androidhero.geohottness.service這樣的包中。用于Android應用的典型的包在布局上會包含一個持久性包、UI包和復制應用邏輯和控制器代碼的包。
包除了定義了全局唯一的命名空間之外,包內(nèi)對象的成員(成員變量和方法)之間的可見性也不同。類的內(nèi)部變量對于在同一個包內(nèi)的類是可見的,而對于其他包內(nèi)的類則是不可見的。
聲明一個類屬于某個包的方法是,在定義類的文件的最上方,使用package這個關鍵字按照下面這個方式聲明:
package your.qualifieddomainname.fuctionalgrouping
不要過分簡化包名!因為一個快速、臨時的實現(xiàn)方式可能需要使用很多年,如果不能保證包名唯一,那么以后一定會深受其困擾。
一些大型的項目通過使用完全不同的頂級域名來實現(xiàn)公有API包和這些API的實現(xiàn)之間的隔離。舉個例子,Android API使用頂級域名包android,這些API的實現(xiàn)則在com.android包內(nèi)。Sun的Java源代碼采用的機制也類似于此。公有API在Java包內(nèi),但是這些API的實現(xiàn)則放在了sun包內(nèi)。在任意一種情況下,如果一個應用導入的是某個實現(xiàn)包,則這個應用會反復無常,因為它依賴與一些非公有的API。
雖然把代碼添加到已有的包內(nèi)是可以的,但通常認為這是一種不好的做法。通常情況下,除了命名空間,包通常是一顆源代碼樹,其至少和逆轉(zhuǎn)的域名一樣高。雖然這只是傳統(tǒng)習慣,但是Java開發(fā)人員通常會期望com.brashandroid.coolapp.ui這個包中包含了CoolApp UI的所有源代碼。如果另一顆樹的某些地方也有CoolApp UI的一些代碼,很多人會覺得不習慣。
訪問修飾符和封裝
類的成員有特殊的可見性規(guī)則。大多數(shù)Java塊中的定義是有作用域的:它們只在代碼塊本身及內(nèi)嵌于其中的代碼塊中可見。然而,類中的定義在代碼塊外也可能是可見的。Java支持類將其頂級成員(其方法和成員變量)通過訪問修飾符(access modifiers)發(fā)布給其他類的代碼。訪問修飾符關鍵字修改了聲明的可見性。
在Java中有3個訪問修飾符關鍵字:public、protected和private。共支持4種訪問級別。訪問修飾符影響的是類的成員在類外面的訪問性,但類內(nèi)部的代碼塊遵循的是普通的作用域,不需要考慮訪問修飾符的影響。
private修飾符的限制最高。帶private關鍵字的聲明在代碼塊外是不可見的。這種聲明是最安全的,它會確保僅在類的內(nèi)部還有指向這個聲明的引用。private聲明越多,類就越安全。
限制程度僅次于private修飾符的是默認的訪問限制,即package訪問。沒有任何修飾符的聲明屬于默認情況,默認的訪問可見性是指只能在同一個包中的其他類中可見。默認訪問時創(chuàng)建對象共享的一種非常便捷的方式,Java的默認聲明和C++中的friend聲明類似。
protected訪問修飾符除了支持所有的默認訪問權(quán)限之外,還允許訪問子類。任何包含protected聲明的類都能夠訪問這些聲明。
public訪問修飾符是限制條件最弱的修飾符,其允許從任何地方對它進行訪問。