Java 物件導向(上)

NO IMAGE

類和物件

定義類

物件導向的程式設計過程中有兩個重要概念:類(class)和物件(object,也被稱為例項,instance),其中類是某一批物件的抽象,可以把類理解成某種概念;物件才是一個具體存在的實體。


[修飾符]    class    類名
{
零個到多個構造器定義...
零個到多個成員變數...
零個到多個方法...
}

修飾符可以是public、final、abstract或者完全忽略。

如果從程式的可讀性方面來看,Java類名必須由一個或多個有意義的單詞連綴而成的,每個單詞首字母大寫,其他字母全部小寫,單詞與單詞之間不要使用任何分隔符。

構造器是一個類建立物件的根本途徑,如果一個類沒有構造器,這個類通常無法建立例項。如果程式設計師沒有為一個類編寫構造器,則系統會為該類提供一個預設的構造器。一旦程式設計師為一個類提供了構造器,系統將不再為該類提供構造器。

定義成員變數

static修飾的成員不能訪問沒有static修飾的成員。

成員變數用於定義該類或該類的例項所包含的狀態資料,方法則用於定義該類或該類的例項的行為特徵或者功能實現。構造器用於構造該類的例項,Java語言通過new關鍵字來呼叫構造器,從而返回該類的例項。

[修飾符]    型別    成員變數名 [=預設值]

修飾符:public、protected、private三個最多隻能出現其中之一,可以與static、final組合起來修飾成員變數。

型別:基本型別和引用型別

成員變數名:如果從程式的可讀性方面來看,Java類名必須由一個或多個有意義的單詞連綴而成的,第一個單詞首字母小寫,後面每個單詞首字母大寫,其他字母全部小寫,單詞與單詞之間不要使用任何分隔符。

預設值:定義成員變數還可以指定一個可選的預設值。

定義方法

[修飾符]    方法返回值型別    方法名(形參列表)
{
//由零條到多條可執行性語句組成的方法體
}

修飾符:public、protected、private三個最多隻能出現其中之一,可以與static、final組合起來修飾成員變數。

方法返回值型別:基本型別和引用型別如果宣告瞭方法返回值型別,則方法體內必須與此處宣告的型別匹配。如果一個方法沒有返回值,則必須使用void來宣告沒有返回值。

形參列表:形參列表用於定義該方法可以接受的引數,形參列表由零組到多組“引數型別 形參名”組合而成,多組引數之間以英文逗號(,)隔開,形參型別和形參名之間以英文空格隔開。

static

static是一個特殊的關鍵字,可用於修飾方法、成員變數等成員。static修飾的成員表明它屬於這個類本身,而不屬於該類的單個例項,因為通常把static修飾的成員變數和方法也稱為類變數、類方法。不使用static修飾的普通方法、成員變數則屬於該類的單個例項,而不屬於該類。因為通常把不使用static修飾的成員變數和方法也稱為例項變數、例項方法。

static修飾的成員變數和方法稱為靜態變數和靜態方法,把不使用static修飾的成員變數和方法稱為非靜態變數和非靜態方法。靜態方法不能直接訪問非靜態成員。

有static修飾的成員屬於類本身,沒有static修飾的成員屬於該類的例項。

構造器

構造器是一個特殊的方法,定義構造器的語法格式與定義方法的語法格式很像,定義構造器的語法格式如下:

[修飾符]    構造器名(形參列表)
{
//由零條到多條可執行性語句組成的構造器執行體
}

修飾符:public、protected、private
構造器名:構造器名必須和類同名
形參列表:和定義方法形參列表的格式完全相同。
構造器既不能定義返回值型別,也不能使用void宣告構造器沒有返回值。

物件的產生和使用

static修飾的方法和成員變數,既可通過類來呼叫,也可通過例項來呼叫;沒有使用static修飾的普通方法和成員變數,只可通過例項來呼叫。

物件、引用和指標

變數例項實際上是一個引用,它被存放在棧記憶體裡,指向實際的類物件;而真正的類物件則存放在堆記憶體中。棧記憶體裡的引用變數並未真正儲存物件的成員變數,物件的成員變數資料實際存放在堆記憶體裡;而引用變數只是指向該堆記憶體裡的物件。

物件的this引用

this關鍵字總是指向呼叫該方法的物件。根據this出現位置的不同,this作為物件的預設引用有兩種情形。

構造器中引用該構造器正在初始化的物件。

在方法中引用呼叫該方法的物件。

對於static修飾的方法而言,則可以使用類來直接呼叫該方法,如果在static修飾的方法中使用this關鍵字,則這個關鍵字就無法指向合適的物件。所以,static修飾的方法中不能使用this引用。由於static修飾的方法不能使用this引用,所以static修飾的方法不能訪問不使用static修飾的普通成員,因此Java語法規定:靜態成員不能直接訪問非靜態成員。

普通方法訪問其他方法、成員變數時無須使用this字首,但如果方法裡有個區域性變數和成員變數同名,但程式又需要在該方法裡訪問這個被覆蓋的成員變數,則必須使用this字首。this引用也可以用於構造器中作為預設引用,由於構造器是直接使用new關鍵字來呼叫,而不是使用物件來呼叫的,所以this在構造器中代表該構造器正在初始化的物件。

方法詳解

方法的所屬性

Java語言裡方法的所屬性主要體現在如下幾個方面:

方法不能獨立完成,方法只能在類體裡定義。

從邏輯意義上來看,方法要麼屬於該類本身,要麼屬於該類的一個物件。

永遠不能獨立執行方法,執行方法必須使用類或物件作為呼叫者。

同一個類的一個方法呼叫另一個方法時,如果被呼叫方法是普通方法,則預設使用this作為呼叫者;如果被調方法是靜態方法,則預設方法類作為呼叫者。

方法的引數傳遞機制

宣告方法時包含了形參宣告,則呼叫方法時必須給這些形參指定引數值,呼叫方法時實際傳給形參的引數值也被稱為實參。

形參個數可變的方法

Java允許定義形參個數可變的引數,從而允許為方法指定數量不確定的形參。如果在定義方法時,在最後一個形參的型別後增加三點(…),則表明該形參可以接受多個引數值,多個引數值被當成陣列傳入。

public statci void test(int a, String … books)

陣列形式的形參可以處於形參列表的任意位置,但個數可變的形參只能處於形參列表的最後。也就是說一個方法中最多只能有一個長度可變的形參。

遞迴方法

一個方法體內呼叫它自身,被稱為方法遞迴。方法遞迴包含了一種隱式的迴圈,它會重複執行某段程式碼,但這種重複執行無限迴圈控制。

希望遍歷某個路徑下的所有檔案,但這個路徑下資料夾的深度是未知的,那麼就可以使用遞迴來實現這個需求。系統可定義一個方法,該方法接受一個檔案路徑作為引數,該方法可遍歷當前路徑下的所有檔案和檔案路徑——該方法中再次呼叫該方法本身來處理該路徑下的所有檔案路徑。

方法過載

Java允許同一個類裡定義多個同名方法,只有形參列表不同就行。如果同一個類中包含了兩個或兩個以上方法的方法名相同,但形參列表不同,則被稱為方法過載。

方法過載三個因素:

呼叫者,也就是方法的所屬者,既可以是類,也可以是物件。

方法名,方法的標識。

形參列表,當呼叫方法時,系統將會根據傳入的實參列表匹配。

兩同一不同:同一個類中方法名相同,引數列表不同。方法返回值型別、修飾符等,與方法過載沒有任何關係。

成員變數和區域性變數

成員變數

clipboard.png

成員變數指的是類裡定義的變數,也就是前面所介紹的field;區域性變數指的是方法裡定義的變數。

成員變數:一個類不能定義兩個同名的成員變數。

static修飾的類變數。類變數從該類的準備階段起開始存在,直到系統完全銷燬這個類,類變數的作用域與這個類的生存範圍相同;而例項變數則從該類的例項被建立起開始存在,直到系統完全銷燬這個例項,例項變數的作用域與對應例項的生存範圍相同。

成員變數無須顯式初始化,只要為一個類定義了類變數或例項變數,系統就會在這個類的準備階段或建立該類的例項時進行預設初始化,成員變數預設初始化時的賦值規則與陣列動態初始化時陣列元素的賦值規則完全相同。

區域性變數:一個方法裡不能定義兩個同名的方法區域性變數,方法區域性變數與形參也不能同名;同一個方法中不同程式碼塊內的程式碼塊區域性變數可以同名;如果先定義程式碼塊區域性變數,後定義方法區域性變數,前面定義的程式碼塊區域性變數與後面定義的方法區域性變數也可以同名。

形參(方法簽名中定義的變數) 作用域在整個方法內有效 必須顯式初始化

方法區域性變數(在方法內定義) 作用域從定義該變數的地方生效,到該方法結束時失效 必須顯式初始化

程式碼塊區域性變數(在程式碼塊內定義) 作用域從定義該變數的地方生效,到該程式碼塊結束時失效 無須顯式初始化

Java允許區域性變數和成員變數同名,如果方法裡的區域性變數和成員變數同名,區域性變數會覆蓋成員變數,如果需要在這個方法裡引用被覆蓋的成員變數,則可使用this(對於例項變數)或類名(對於類變數)作為呼叫者來限定訪問成員變數。

成員變數的初始化和記憶體中的執行機制

當系統載入類或建立該類的例項時,系統自動為成員變數分配記憶體空間,並在分配記憶體空間後,自動為成員變數指定初始值。

區域性變數的初始化和記憶體中的執行機制

區域性變數定義後,必須經過顯式初始化後才能使用,系統不會為區域性變數執行初始化。這意味著定義區域性變數後,系統並未為這個變數分配記憶體空間,直到等到程式為這個變數賦初始值時,系統才會為區域性變數分配記憶體,並將初始值儲存到這塊記憶體中。

變數的使用規則

定義一個成員變數時,成員變數將被放置到堆記憶體中,成員變數的作用域將擴大到類存在範圍或者物件存在範圍,這種範圍的擴大有兩個害處。

增大了變數的生存時間,這將導致更大的記憶體開銷。

擴大了變數的作用域,這不利於提供程式的內聚性。

應該考慮使用成員變數的情況:

如果需要定義的變數是用於描述某個類或某個物件的固有資訊。

如果在某個類中需要以一個變數來儲存該類或者例項執行時的狀態資訊。

如果某個資訊需要在某個類的多個方法之間進行共享,則這個資訊應該使用成員變數來儲存。

即使在程式中使用區域性變數,也應該儘可能地縮小區域性變數的作用範圍,區域性變數的作用範圍越小,它在記憶體裡停留的時間就越短,程式執行效能就越好。因此,能用程式碼塊區域性變數的地方,就堅決不要使用方法區域性變數。

隱藏和封裝

理解封裝

封裝指的是將物件的狀態資訊隱藏在物件內部,不允許外部程式直接訪問物件內部資訊,二世通過該類所提供的方法來實現對內部資訊的操作和訪問。

對一個類或物件實現良好的封裝,可以實現以下目的。
隱藏類的實現細節

讓使用者只能通過事先預定的方法來訪問資料,從而可以在該方法里加入控制邏輯,限制對成員變數的不合理訪問。

可進行資料檢查,從而有利於保證物件資訊的完整性。

便於修改,提高程式碼的可維護性。

為了實現良好的封裝,需要從兩個方面考慮:

將物件的成員變數和實現細節隱藏起來,不允許外部直接訪問。

把方法暴露出來,讓方法來控制對這些成員變數進行安全的訪問和操作。

使用訪問控制符

private → default → protected → public
訪問控制級別由小到大

private(當前類訪問許可權):使用它來修飾成員變數就可以把成員變數隱藏在該類的內部。

default(包訪問許可權):defaulte訪問控制的成員或外部類可以被相同包下的其他類訪問。

protected(子類訪問許可權):成員既可以被同一包中的其他類訪問,也可以被不同包中的子類訪問。通常,如果使用protected來修飾一個方法,是希望其子類來重寫這個方法。

public(公共訪問許可權):成員或外部類可以被所以類訪問,不管訪問類和被訪問類是否處於同一包中,是否具有父子繼承關係。

             private    default    protected    public
同一個類中      √          √           √          √
同一個包中                 √           √          √
子類中                                 √          √
全域性範圍內                                        √

外部類只能有兩種訪問控制級別:public和default,外部類不能用private和protected修飾,因為外部類沒有處於任何類內部,也就沒有其所在類的內部、所在類的子類的兩個範圍。

如果一個Java類的每個例項變數都被使用private修飾,併為每個例項變數都提供了public修飾setter和getter方法,那麼這個類就是一個符號JavaBean規範的類。因此,JavaBean總是一個封裝良好的類。

進行程式設計時,應儘量避免一個模組直接操作和訪問另一個模組的資料,模組設計追求高內聚(儘可能把模組的內部資料、功能實現細節隱藏在模組內部獨立完成,不允許外部直接干預)、低耦合(僅暴露少量的方法給外部使用)。

訪問控制符的使用的基本原則:

類裡的絕大部分成員變數都應該使用private,只有一些static修飾的、類似全域性變數的成員變數,才可能考慮使用public修飾。除此之外,有些方法只用於輔助實現該類的其他方法,這些方法被稱為工具方法,工具方法也應該使用private修飾。

如果某個類主要用作其他類的父類,該類裡包含的大部分方法可能僅希望被其子類重寫,而不想被外界直接呼叫,則應該使用protected修飾這些方法。

希望暴露出來給其他類自由呼叫的方法應該使用public修飾。因此,類的構造器通過public修飾,從而允許在其他地方建立該類的例項。因為外部類通常都希望被其他類自由使用,所以大部分外部類都使用public修飾。

package、import和import static

如果希望把一個類放到指定的包結構下,應該在Java源程式的第一個非註釋行放置如下格式的程式碼:

package packageName;

一旦在Java原始檔中使用了這個package語句,就意味著該原始檔裡定義的所有類都屬於這個包。位於包中的每個類的完整類名都應該是包名和類名的組合,如果其他人需要使用該包下的類,也應該使用包名加類名的組合。

同一個包中的類不必位於相同的目錄下,例如有leePerson和LeePersonTest兩個類,它們完全可以一個位於C盤下某個位置,一個位於D盤下某個位置,只要讓CLASSPATH環境變數裡包含這兩個路徑即可。虛擬機器會自動搜尋CLASSPATH下的子路徑,把它們當成同一個包下的類來處理。也應該把Java原始檔放在與包名一致的目錄結構下。通常建議將原始檔和class檔案分開存放,以便管理。

clipboard.png

從可讀性規範角度來看,包名應該全部是小寫字母,而且應該由一個或多個有意義的單詞連綴而成。

package語句必須作為原始檔的第一條非註釋性語句,一個原始檔只能指定一個包,即只能包含一條package語句,該原始檔中可以定義多個類,則這些類將全部位於該包下。如果沒有顯式指定package語言,則處於預設包下。

同一個包下的類可以自由訪問,無須新增包字首。

父包和子包之間確實表示了某種內在的邏輯關係。但父包和子包在用法上不存在任何關係,如果父包中的類需要使用子包中的類,則必須使用子包的全名,而不能省略父包部分。

//呼叫構造器時需要在構造器前新增包字首
lee.sub.Apple a  = new lee.sub.Apple();

import關鍵字

import可以向某個Java檔案中匯入指定包層次下某個類或全部類,import語句應該出現在package語句之後(如果有)、類定義之前。一個Java原始檔只能包含一個package語句,但可以包含多個import語句,多個import語句用於匯入多個包層次下的類。java.lang包下的所有類預設匯入。

//使用import語句匯入單個類
import    package.subpackage...ClassName;
//使用import語句匯入指定包下全部
import    package.subpackage.*

星號()只能代表類,不能代表包。因此使用import.lee.;語句時,它表明匯入lee包下的所有類,而lee包下sub子包內的類則不會被匯入。如需匯入lee.sub.Apple類,則可以使用impor.lee.sub.*;語句來匯入lee.sub包下的所有類。

一旦在Java原始檔中使用import語句來匯入指定類,在該原始檔中使用這些類時就可以省略包字首,不再需要使用類全名。

靜態匯入import static

import static package.subpackage..ClassName.fieldName|methodName;
field:靜態成員變數;methodName:靜態方法。
使用import可以省略寫包名;使用import static則可以連類名都省略。

Java原始檔的大致結構:

package語句                                                    //0個或1個,必須放在檔案開始
import | import static 語句                                    //0個或多個,必須放在所有類定義之前
public classDefinition | interfaceDefinition | enumDefinition  //0個或1個public類、介面或列舉定義
classDefinition | interfaceDefinition | enumDefinition         //0個或多個普通類、介面或列舉定義

Java的常用包

Java.lang:核心類,如String、Math、System和thread類等,使用這個包下的類無須使用import語句匯入,系統會自動匯入這個包下的所有類。
java.util:Java的大量工具類/介面和集合框架類/介面,例如Arrays和List、Set等
java.net:一些Java網路程式設計相關的類/介面
java.io:一些Java輸入/輸出程式設計相關的類/介面
java.text:一些Java格式化相關的類
java.sql:Java進行JDBC資料庫程式設計的想相關類/介面
java.awt:抽象視窗工具集的相關類/介面,這些類主要用於構建圖形使用者介面GUI程式
java.swing:Swing圖形使用者介面程式設計的相關類/介面,這些類可用於構建平臺無關的GUI程式

深入構造器

構造器是一個特殊的方法,用於建立例項時執行初始化。

使用構造器執行

當建立一個物件時,系統為這個物件的例項變數進行預設初始化,這種預設的初始化把所有基本型別的例項變數設為0或false,把所有引用型別的例項變數設為Null。

如果程式設計師沒有為Java類提供任何構造器,則系統會為這個類提供一個無引數的構造器,這個構造器的執行體為空,不做任何事情。無論如何,Java類至少包含一個構造器。

通常把構造器設定為public訪問許可權,從而允許系統中任何位置的類來建立該類的物件。

構造器過載

同一個類裡具有多個構造器,多個構造器的形參列表不同,即被稱為構造器過載,構造器過載允許Java類裡包含多個初始化邏輯,從而允許使用不同的構造器來初始化Java物件。

類的繼承

繼承的特點

Java的繼承通過extends關鍵字來實現,實現繼承的類被稱為子類,被繼承的類被稱為父類。父類與子類的關係,是一種一般和特殊的關係。Java類只能有一個直接父類。

修飾符    class    SubClass    extends    SuperClass
{
//類定義部分
}

java.lang.Object類是所有類的父類,要麼是直接父類,要麼是其間接父類。因此所有的java物件都可以呼叫java.lang.Object類定義的例項方法。

重寫父類的方法

子類包含與父類同名方法你的現象被稱為方法重寫(Override),也稱為方法覆蓋。

方法的重寫遵循“兩同兩小一大”規則

“兩同”:方法名、形參列表相同;

“兩小”:子類方法返回值型別應比父類返回值型別更小或相等,子類方法宣告丟擲的異常類應比父類方法宣告丟擲的異常類更小或相等。

“一大”:子類方法的訪問許可權應比父類方法的訪問許可權更大或相等。

覆蓋方法和被覆蓋方法要麼都是類方法,要麼都是例項方法,不能一個類方法,一個是例項方法。

當子類覆蓋了父類方法後,子類的物件將無法訪問父類中被覆蓋的方法,但可以在子類方法中呼叫父類中被覆蓋的方法。如果需要在子類方法最後呼叫父類中被覆蓋的方法,則可以使用super(被覆蓋的是例項方法)或者父類類名(被覆蓋的是類方法)作為呼叫者來呼叫父類中被覆蓋的方法。

如果父類方法具有private訪問許可權,則該方法對其子類是隱藏的,因此其子類無法訪問該方法,也就是無法重寫該方法。如果子類中定義了一個與父類private方法具有相同的方法名、相同的形參列表、相同的返回值型別的方法,依然不是重寫,只是在子類中重新定義了一個新方法。

super限定

如果需要在子類方法中呼叫父類被覆蓋的例項方法,則可使用super限定來呼叫父類被覆蓋的例項方法。super用於限定該物件呼叫它從父類繼承得到的例項變數或方法。this和super均不能出現在static修飾的方法中。

如果在某個方法中訪問名為a的成員變數,但沒有顯式指定呼叫者,則系統查詢a的順序為:

查詢該方法中是否有名為a的區域性變數。

查詢當前類中是否包括名為a的成員變數。

查詢a的直接父類中是否包含名為a的成員變數,依次上溯a的所有父類,直到java.lang.Object類,如果最終不能找到名為a的成員變數,則系統出現編譯錯誤。

如果被覆蓋的是類變數,在子類的方法中則可以通過父類名作為呼叫者來訪問被覆蓋的類變數。

當程式建立一個子類物件時,系統不僅會為該類中定義的例項變數分配記憶體,也會為它從父類繼承得到的所有例項變數分配記憶體,即使子類定義了與父類中同名的例項變數。

呼叫父類構造器

在一個構造器中呼叫另一個過載的構造器使用this來完成,在子類構造器使用super呼叫來完成。this和super呼叫構造器必須出現在構造器執行體的第一行。

子類構造器呼叫父類構造器分如下幾種情況:

子類構造器執行體的第一行使用super顯式呼叫父類構造器,系統將根據super呼叫裡傳入的實參列表呼叫父類對應的構造器。

子類構造器執行體的第一行程式碼使用this顯式呼叫本類中過載構造器,系統將根據this呼叫裡傳入的實參列表呼叫本類中的另一個構造器。執行本類中另一個構造器時即會呼叫父類構造器。

子類構造器執行體中既沒有super呼叫,也沒有this呼叫,系統將會在執行子類構造器之前,隱式呼叫父類無引數的構造器。

建立任何java物件,最先執行的總是java.lang.Object類的構造器。即,建立任何物件總是從該類所在繼承樹最頂層類的構造器開始執行,然後依次向下執行,最後才執行本類的構造器。如果某個父類通過this呼叫了同類中過載的構造器,就會依次執行此父類的多個構造器。

多型

多型性

Java引用變數有兩個型別:編譯時型別,執行時型別。

編譯時型別由宣告該變數時使用的型別決定,執行時型別由實際賦給該變數的物件決定。如果編譯時型別和執行時型別不一致,就可能出現所謂的多型。

BaseClass bc = new BaseClass();
SubClass sc = new SubClass();
BaseClass polymorphicBc = new SubClass();

上面程式顯式建立了三個引用變數,對於前兩個引用變數bc和sc,它們編譯時型別和執行時型別完全相同,因此呼叫它們的成員變數和方法非常正常,完全沒有任何問題。但第三個引用變數polymorphic則比較特殊,它的編譯時型別是BaseClass,而執行時型別是SubClass,當呼叫該引用變數的test()方法(BaseClass類中定義了該方法,子類SubClass覆蓋了父類的該方法)時,實際執行的是SubClass類中覆蓋後的test()方法,這就可能出現多型了。

因為子類其實是一種特殊的父類,因此Java允許把一個子類物件直接賦給一個父類引用變數,無須任何型別轉換,或者被稱為向上轉型(upcasting),向上轉型由系統自動完成。

相同型別的變數、呼叫同一方法時呈現出多種不同的行為特徵,這就是多型。

polymorphicBc.sub();這行程式碼會在編譯時引發錯誤。雖然polymorphicBc引用變數實際上確實包含sub()方法,但因為它的編譯時型別為BaseClass,因此編譯時無法呼叫sub方法。

與方法不同的是,物件的例項變數則不具備多型性。比如上面的polymorphicBc引用變數,程式中輸出它的book例項變數時,並不是輸出SubClass類裡定義的例項變數,而是輸出BaseClass類的例項變數。

引用變數在編譯階段只能呼叫其編譯時型別所具有的方法,但執行時則執行它執行時型別所具有的方法。因此,編寫Java程式碼時,引用變數只能呼叫宣告該變數時所用類裡包含的方法。例如,通過Object p = new Person()程式碼定義了一個變數p,則這個p只能呼叫Object類的方法,而不能呼叫Person類裡定義的方法。

通過引用變數來訪問其包含的例項變數時,系統總是試圖訪問它編譯時型別所定義的成員變數,而不是它執行時型別所定義的成員變數。

引用變數的強制型別轉換

編寫Java程式時,引用變數只能呼叫它編譯時型別的方法,而不能呼叫它執行時型別的方法,即使它實際所引用的物件卻是包含該方法。如果需要讓這個引用變數呼叫它執行時型別的方法,則必須把它強制型別轉換成執行時型別,強制型別轉換需要藉助於型別轉換運算子。

當進行強制型別轉換時需要注意:

基本型別之間的轉換只能在數值型別之間進行,這裡所說的陣列型別包括整數性、字元型和浮點型。但數值型和布林型別直接不能進行型別轉換。

引用型別直接的轉換只能在具有繼承關係的兩個型別之間進行,如果是兩個沒有任何繼承關係的型別,則無法進行型別轉換,否則編譯時就會出現錯誤。如果試圖把一個父類例項轉換成子類例項,則這個物件必須實際上是子類例項才行(即編譯時型別為福型別,而執行時型別是子類型別),否則將在執行時引發ClassCastException異常。

進行強制型別轉換時可能出現異常,因此進行型別轉換之前應先通過instanceof運算子來判斷是否可以成功轉換,從而避免出現ClassCastException異常,這樣可以保證程式更加健壯。

instanceof運算子

instanceof運算子的前一個運算元通常是一個引用型別變數,後一個運算元通常是一個類(也可以是介面),它用於判斷前面的物件是否是後面的類,或者其子類、實現類的例項。如果是返回true,否則返回false。

instanceof運算子前面運算元的編譯時型別要麼與後面的類相同,要麼與後面的類具有父子繼承關係,否則會引起編譯錯誤。

instanceof和(type)是Java提供的兩個相關的運算子,通常先用instanceof判斷一個物件是否可以強制型別轉換,然後再使用(type)運算子進行強制型別轉換,從而保證程式不會出現錯誤。

繼承與組合

使用繼承的注意點

為了保證父類有良好的封裝性,不會被子類隨意改變,設計父類通常應該遵循如下規則。

儘量隱藏父類的內部結構。儘量把父類的所有變數都設定成private訪問型別,不要讓子類直接訪問父類的成員變數。

不要讓子類可以隨意訪問、修改父類的方法。父類中那些僅為輔助其他的工具方法,應該使用private訪問控制符修飾,讓子類無法訪問該方法;如果父類中的方法需要被外部類呼叫,則必須以public修飾,但又不希望子類重寫該方法,可以使用final修飾符(該修飾符後面會有更詳細的介紹)來修飾該方法。如果希望父類的某個方法被子類重寫,但不希望被其他類自由訪問,則可以使用protected來修飾該方法。

儘量不要在父類構造器中呼叫將要被子類重寫的方法。 將引發空指標異常。

如果想把某些類設定成最終類,既不能被當成父類,則可以使用final修飾這個類;使用private修飾這個類的所有構造器,從而保證子類無法呼叫該類的構造器,也就無法繼承該類的例項。

何時需要從父類派生新的子類:

子類需要額外增加屬性,而不僅僅是屬性值的改變。

子類需要增加自己獨有的行為方式(包括增加新的方法或重寫父類的方法)。

利用組合實現複用

對於繼承而已,子類可以直接獲得父類的public方法,程式使用子類時,將可以直接訪問該子類從父類那裡繼承到的方法;而組合則是把舊類物件作為新類的成員變數組合進來,用以實現新類的功能,使用者看到的是新類的方法,而不能看到被組合物件的方法。

繼承要表達的是一種“是(is-a)”的關係,而組合表達的是“有(has-a)”的關係。

初始化塊

使用初始化塊

初始化塊是Java類裡可出現的第4種成員(前面依次有成員變數、方法和構造器),一個類裡可以有多個初始化塊,相同型別的初始化塊之間有順序:前面定義的初始化塊先執行,後面定義的初始化塊後執行。

[修飾符]
{
//初始化塊的可執行性程式碼
}

初始化塊的修飾符只能是static,使用static修飾的初始化塊被稱為靜態初始化塊。初始化塊裡的程式碼可以包含任何可執行性語句,包括定義區域性變數、呼叫其他物件的方法,以及使用分支、迴圈語句等。

當建立Java物件時,系統總是先呼叫該類裡定義的初始化塊,如果一個類裡定義了2個普通初始化塊,則前面定義的初始化塊先執行,後面定義的初始化塊後執行。初始化塊只在建立Java物件時隱式執行,而且在執行構造器之前執行。

初始化塊和構造器

與構造器不同的是,初始化塊是一段固定執行的程式碼,它不能接收任何引數。

靜態初始化塊

如果定義初始化塊時使用了static修飾符,則這個初始化塊就變成了靜態初始化塊,也被稱為類初始化塊(普通初始化塊負責對物件執行初始化,類初始化塊則負責對類進行初始化)。靜態初始化塊是類相關的,系統將在類初始化階段執行靜態初始化塊,而不是在建立物件時才執行。因此靜態初始化塊總是比普通初始化塊先執行。

靜態初始化塊也被稱為類初始化塊,也屬於類的靜態成員,同樣需要遵循靜態成員不能訪問非靜態成員的規則,因此靜態初始化塊不能訪問非靜態成員,包括不能訪問例項變數和例項方法。

系統在類初始化階段執行靜態初始化塊時,不僅會執行本類的靜態初始化塊,而且還會一直上溯到java.lang.Object類(如果它包含靜態初始化塊),先執行java.lang.Object類的靜態初始化塊(如果有),然後執行其父類的靜態初始化塊······最後才執行該類的靜態初始化塊。
系統在建立一個Java物件時,不僅會執行該類的普通初始化塊和構造器,而且系統會一直上溯到java.lang.Object類,先執行java.lang.Object類的初始化塊,開始執行java.lang.Object的構造器,一次向下執行其父類的初始化塊,開始執行其父類的構造器······最後才執行該類的初始化塊和構造器,返回該類的物件。

當JVM第一次主動使用某個類時,系統會在類準備階段為該類的所有靜態成員變數分配記憶體;在初始化階段則負責初始化這些靜態成員變數,初始化靜態成員變數就是執行類初始化程式碼或者宣告類成員變數時指定的初始值,它們的執行順序與原始碼中的排列順序相同。