Android開發之Java必備基礎


Android開發之Java必備基礎

Java類型系統

Java語言基礎數據類型有兩種:對象和基本類型(Primitives)。Java通過強制使用靜態類型來確保類型安全,要求每個變量在使用之前必須先聲明。

這種機制和非靜態類型的語言有很大差別,非靜態語言不要求對變量進行聲明。雖然顯式類型聲明看起來較繁瑣,但其有助於編譯器對很多編程錯誤的預防,例如,由於變量名拼寫錯誤導致創建了沒有用的變量,調用了不存在的方法等。顯式聲明可以徹底防止這些錯誤被生成到運行代碼中。關於Java類型系統的詳細說明可以在Java語言規范(Java Language Specification)中找到。

基本類型

Java的基本類型不是對象,它們不支持對象相關的操作。基本數據類型只能通過一些預定義的操作符來修改它們。Java中的基本類型如下:

  • boolean(布爾型):值為true或false
  • 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():

public class Trivial {
    /* a field: its scope is the entire class */
    private long ctr;

    /* Modify the field */
    public void incr() {
        ctr++;
    }    
}

對象的創建

使用關鍵字new創建一個新的對象,即某個類的實例,如:

Trivial trivial = new Trivial();

在復制運算符"="的左邊定義了一個變量,名為trivial。該變量的類型是Trivial,因此只能賦給它類型為Trivial的對象。賦值符右邊為新創建的Trivial類的實例分配內存,並對該實例進行實體化。賦值操作符為新創建的對象變量分配引用。

在Trivial這個類中,變量ctr的定義是絕對安全的,雖然沒有對它進行顯式初始化。Java會保證給ctr的初始化值為0。Java會確保所有的字段在對象創建時自動進行初始化。布爾值初始化為false,基本數值類型初始化為0,所有的對象類型(包括String)初始化為null。上述的初始化賦值只適用於對象的成員變量,局部變量在被引用之前必須進行初始化。

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

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中的每個類都會有一個構造函數。如果沒有顯式定義的構造函數,Java編譯器會自動創建一個不帶參數的構造函數。此外,如果子類的構造函數沒有顯式調用超類的構造函數,那么Java編譯器會自動隱式調用超類的無參數的構造函數。前面給出了Trivial的定義(它沒有顯式地指定構造函數),實際上Java編譯器會自動為它創建一個構造函數:

public Trivial() { super(); } 

如上所示,由於LessTrivial類顯式定義了一個構造函數,因此Java不會再給它隱式地定義一個默認的構造函數。這意味着如果創建一個沒有參數的LessTrivial對象,會出現錯誤:

LessTrivial fail = new LessTrivial(); // Error!!

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

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

如果一個類有多個構造函數,則最好采用級聯(cascade)的方法創建它們,從而確保只會有一份代碼對實例進行初始化,所有其他構造函數都調用它。為了便於說明,我們用一個例子來演示一下。為了更好地模擬常見情況,我們給LessTrivial類增加一個無參數的構造函數:

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++;
    }    
}

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

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

對象類及其方法

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

Object類中的wait、notify和notifyAll方法是Java類並發支持的一部分。

toString方法是對象用來創建一個自我描述的字符串的方法。toString方法的一個有趣的使用方式是用於字符串連接,任何一個對象都可以和一個字符串進行連接。以下這個例子給出了輸出相同消息的兩種方法,它們的運行結果完全相同。在這兩個方法中,都為Foo類創建了新的實例並調用其toString方法,隨后把結果和文本字符串連接起來,隨后輸出結果:

System.out.println("This is a new foo: " + new Foo());
System.out.println("This is a new foo: ".concat((new Foo()).toString()));

在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繼承的例子如下:

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方法有着不同的行為。

現在,我們來演示一個多態的例子:

Car auto = new Car();
auto.drive();
auto = new Ragtop();
auto.drive();

盡管吧Ragtop類型賦值給了Car類型的變量,但這段代碼可以編譯通過(雖然吧Ragtop類型賦值給Car類型的變量)。它還可以正確運行,並輸出如下結果:

Going down the road!
Top dowm!
Going down the road!
Got the radio on!

auto這個變量在生命的不同時期,分別指向了兩個不同的Car類型的對象引用。其中一個對象,不但是Car類型,也是其子類型Ragtop類型。auto.drive()語句的確切行為取決於該變量當前是指向基類對象的引用還是子類對象的引用,這就是所謂的多態行為

類似很多其他的面向對象編程語言,Java支持類型轉換,允許聲明的變量類型為多態形式下的任意一種變量類型。

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();

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

Java限制方法的參數(即真正參數)是多態的,表示形參所指向的對象類型。同樣,方法的返回值也是多態的,返回聲明的對象類型。舉個例子,繼續以之前的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類型的對象是其唯一參數。但是在方法letsGo中,在調用它時傳遞的參數類型是Ragtop,即Car類型的子類。同樣,變量myCar賦值的類型為Ragtop,方法whatsInTheGarage返回類型變量myCar的值。如果一個對象是Ragtop類型,當調用drive方法時,他會輸出"Top down!"和"Got the radio on!"信息;另一方面,因為它又是Car類型,它還可以用於任何Car類型可用的方法調用中。這種子類型可取代父類型是多態的一個關鍵特征,也是其可以保證類型安全的重要因素。在編譯階段,一個對象是否和其用途兼容也已經非常清晰。類型安全使得編譯器能夠及早發現錯誤,這些錯誤如果只是在運行時才發現,那么發現這些錯誤的成本就會高很多。

Final聲明和Static聲明

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

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

  • 當用final修飾類時,意味着任何為其定義子類的操作都會引發錯誤。舉個例子,String類是final類型,因為作為其內容的字符串必須是不可改變的(也就是說,創建了一個字符串后,就不能夠改變它)。如果你仔細考慮一下,就會發現,確保其內容不被改變的唯一方式就是確保不能以String類型為基類來創建子類。如果能夠創建子類,例如DeadlyString,就可以吧DeadlyString類的實例作為參數,並在驗證完其內容后,馬上在代碼中把該實例的值從"fred"改成"';DROP TABLE contacts;"(把惡意SQL注入到你的系統中,對你的數據庫進行惡意修改)!
  • 當用final修飾方法時,它表示子類不能重寫(override)這個方法。開發人員使用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類型。任何沒有聲明為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是該類的一個實例的引用。靜態成員變量classMember是QuietStatic的成員變量,可以通過類名引用它(QuietStatic.classMember)。反之,instanceMember是QuietStatic類的實例的成員變量,通過類名引用它(QuietStatic.instanceMember)就會出現錯誤。這種處理機制是有道理的,因為可以存在很多個名字為instanceMember的不同的變量,每個變量屬於QuietStatic類的一個實例。如果沒有顯式指定是哪個instanceMember,那么Java也不可能知道是哪個instanceMember。

正如下一組語句所示,Java確實允許通過實例引用來引用類的(靜態)變量。這容易讓人產生誤解,被認為是不好的編程習慣。如果這么做,大多數編譯器和IDE就會生成警告。

靜態聲明和動態聲明的含義之間的區別很微妙。最容易理解的是靜態成員變量和動態成員變量之間的區別。再次說明,靜態定義在一個類中只有一份副本,而動態定義對於每個實例都有一份副本。靜態成員變量保存的是一個類的所有成員所共有的信息。

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中,分別調用incr()方法對它們執行遞加操作,兩個實例輸出的classMember值都是2。變量instanceMember在每個實例中,其初始化也都是被設置為0。但是,每個實例只對自己的instanceMember執行遞加操作,因此輸出的instanceMember值都為1。

在上面兩個實例中,靜態類定義和靜態方法定義的共同點在於靜態對象在其命名空間內都是可見的,而動態對象只能通過每個實例的引用才可見。此外,相比之下,靜態對象和動態對象的區別則更為微妙。

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

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中,幾乎沒有理由要使用靜態方法。在Java的早期實現中,動態方法調用明顯慢於靜態方法。開發人員常常傾向於使用靜態方法來“優化”其代碼。在Android的即時編譯Dalvik環境中,不再需要這種優化。過度使用靜態方法通常意味着架構設計不良。 

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

實際上,這完全符合一致性要求。根據對“靜態”的定義(屬於類但不屬於類的實例),高層聲明應該是靜態的,因為他們不屬於任何一個類。但是,如果是在代碼塊內定義的(例如在高層類內定義),那么類的定義默認也是動態的。因此,為了動態地聲明一個類,只需要在另一個類內定義它(翻譯不順暢???)。

這一點也說明了靜態類和動態類之間的區別。動態類能夠訪問代碼塊內的類(因為它屬於實例)的實例成員變量,而靜態類卻無法訪問。以下代碼是對這個特點的示例說明:

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是動態的,它屬於類Outer的一個實例。因此可以把類InnerOne理解成隸屬於類Outer的每個實例的獨立的類(雖然不是這個含義,但實際上就是這么實現的)。因此,InnerOne能夠訪問其所屬的Outer類的實例的成員變量x。

類OuterTest說明了對於成員變量,我們可以使用類名.內部靜態類來定義,並可以使用該靜態類型的類的內部定義Outer.InnerTube(在這個例子中,是創建該類的一個實例),而動態類型的類的定義只有在類的實例中才可用

抽象類

在Java的聲明中,如果將類及其一個或者多個方法聲明為抽象類型,則允許這個類的定義中可以不包括這些方法的實現

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    
    }    
} 

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

抽象類可以用於實現常見的模板模式,它提供可重用的代碼塊,支持在執行時自定義特定點。可重用代碼塊是作為抽象類實現的。子類通過實現抽象方法對模板自定義。

接口

其他編程語言(例如C++、Python和Perl)支持多繼承,即一個對象可以有多個父類。多繼承有時非常復雜,程序執行和預期的不同(如從不同的父類中繼承兩個相同名字的成員變量)。為了方便起見,Java不支持多繼承。和C++、Python、Perl等不同,在Java中,一個類只能有一個父類

和多繼承性不同,Java支持一個類通過接口(interface)實現對多種類型的繼承

接口支持只對類型進行定義但不實現。可以把接口想象成一個抽象類,其所有的方法都是抽象方法。Java對一個類可以實現的接口的數量沒有限制。

下面這個例子是關於Java接口和實現該接口的類的示例:

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() {
        // ...    
    }
}

接口只是方法的聲明,而沒有方法的實現。

異常

 

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)操作。
  • 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是對集合黃總所有元素進行操作的一種較好的方式。

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框架下最易於理解其中的原因。

Java泛型

 

垃圾收集

Java是一種支持垃圾收集的語言,這意味着代碼不需要對內存進行管理。相反,我們的代碼可以創建新的對象,可以分配內存,當不再需要這些對象時,只是停止使用這些對象而已。Dalvik運行時會自動刪除這些對象,並適當地執行內存壓縮。

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

作用域

作用域決定了程序中的變量、方法和其他符號的可見范圍。任何符號在其作用域外都是完全不可見的,不能被使用。

Java包

Java包提供了一種機制,它把相關類型分組到一個全局唯一的命名空間。這種分組機制可以防止在一個包的命名空間內的標識符合其他開發人員在其他命名空間內創建和使用的標識符沖突。

一個典型的Java程序有很多Java包的代碼組成。典型的Java運行時環境提供了如java.lang和java.util.這樣的包。此外,程序可能會依賴於其他通用的庫,類似於org.apache樹。傳統上,應用代碼(你所創建的代碼)在你所創建的包內,包名是通過反轉域名並附加程序名字生成的。因此,如果你的域名是androidhero.com,你的包所屬的樹的根是com.androidhero,則可以把代碼放到如com.androidhero.awesomeprogram和com.androidhero.geohottness.service這樣的包中。用於Android應用的典型的包在布局上會包含一個持久性包、UI包和復制應用邏輯和控制器代碼的包。

包除了定義了全局唯一的命名空間之外,包內對象的成員(成員變量和方法)之間的可見性也不同。類的內部變量對於在同一個包內的類是可見的,而對於其他包內的類則是不可見的。

聲明一個類屬於某個包的方法是,在定義類的文件的最上方,使用package這個關鍵字按照下面這個方式聲明

package your.qualifieddomainname.fuctionalgrouping

不要過分簡化包名!因為一個快速、臨時的實現方式可能需要使用很多年,如果不能保證包名唯一,那么以后一定會深受其困擾。

一些大型的項目通過使用完全不同的頂級域名來實現公有API包和這些API的實現之間的隔離。舉個例子,Android API使用頂級域名包android,這些API的實現則在com.android包內。Sun的Java源代碼采用的機制也類似於此。公有API在Java包內,但是這些API的實現則放在了sun包內。在任意一種情況下,如果一個應用導入的是某個實現包,則這個應用會反復無常,因為它依賴與一些非公有的API。

雖然把代碼添加到已有的包內是可以的,但通常認為這是一種不好的做法。通常情況下,除了命名空間,包通常是一顆源代碼樹,其至少和逆轉的域名一樣高。雖然這只是傳統習慣,但是Java開發人員通常會期望com.brashandroid.coolapp.ui這個包中包含了CoolApp UI的所有源代碼。如果另一顆樹的某些地方也有CoolApp UI的一些代碼,很多人會覺得不習慣。

訪問修飾符和封裝

類的成員有特殊的可見性規則。大多數Java塊中的定義是有作用域的:它們只在代碼塊本身及內嵌於其中的代碼塊中可見。然而,類中的定義在代碼塊外也可能是可見的。Java支持類將其頂級成員(其方法和成員變量)通過訪問修飾符(access modifiers)發布給其他類的代碼。訪問修飾符關鍵字修改了聲明的可見性。

在Java中有3個訪問修飾符關鍵字:public、protected和private。共支持4種訪問級別。訪問修飾符影響的是類的成員在類外面的訪問性,但類內部的代碼塊遵循的是普通的作用域,不需要考慮訪問修飾符的影響。

  • private修飾符的限制最高。帶private關鍵字的聲明在代碼塊外是不可見的。這種聲明是最安全的,它會確保僅在類的內部還有指向這個聲明的引用。private聲明越多,類就越安全。
  • 限制程度僅次於private修飾符的是默認的訪問限制,即package訪問。沒有任何修飾符的聲明屬於默認情況,默認的訪問可見性是指只能在同一個包中的其他類中可見。默認訪問時創建對象共享的一種非常便捷的方式,Java的默認聲明和C++中的friend聲明類似。
  • protected訪問修飾符除了支持所有的默認訪問權限之外,還允許訪問子類。任何包含protected聲明的類都能夠訪問這些聲明。
  • public訪問修飾符是限制條件最弱的修飾符,其允許從任何地方對它進行訪問。

 

摘自:《Android程序設計》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM