Java基礎1——深入理解Java面向物件三大特性
深入理解Java面向物件三大特性
Java的三大特性:封裝、繼承和多型
封裝:
通常認為封裝是把資料和操作資料的方法繫結起來,對資料的訪問只 能通過已定義的介面。面向物件的本質就是將現實世界描繪成一系列完全自 治、封閉的物件。我們在類中編寫的方法就是對實現細節的一種封裝;我們 編寫一個類就是對資料和資料操作的封裝。可以說,封裝就是隱藏一切可隱 藏的東西,只向外界提供最簡單的程式設計介面。總結:封裝主要體現在兩個方 面,類是對資料和資料操作的封裝,類中的方法是對實現細節的封裝。
封裝主要是因為Java有訪問許可權的控制。public>protected(繼承訪問許可權)>package = default(包訪問許可權) > private。封裝可以保護類中的資訊,只提供想要被外界訪問的資訊。
注意:Java中外部類的修飾只能是public或預設,類的成員(包括內部類)的 修飾可以是以上四種。
如果不希望別人擁有訪問該類的許可權,可以把所有的構造器指定為private,(可以在該類的static成員內部進行建立)
如何使用這個類:採用單例模式(P122 JAVA程式設計思想)
**問題:在專案中的哪些地方使用過protected修飾符? **
答:Object類中對clone方法的宣告即用到了protected訪問修飾符,這是因為 Object類的clone方法只能實現淺克隆,而並不能實現常使用的深克隆,這就要 求子類在需要克隆物件時儘量重寫clone方法,此時即宣告為protected的,以 保證在需要克隆物件時,必須要求待克隆物件所在的類實現Cloneable介面並 重寫clone方法。
繼承:
從已有類得到繼承資訊建立新類的過程。提供繼承資訊的類被稱為父 類(超類、基類);得到繼承資訊的類被稱為子類(派生類)。繼承讓變化 中的軟體系統有了一定的延續性,同時繼承也是封裝程式中可變因素的重要 手段。
- 為了繼承一般的規則是將所有的資料成員都指定為private,而將所有方法定義為public;
- 從超類中繼承方法,同名要使用關鍵字super;
- 繼承是is-a關係,而組合是has-a關係
問題:java中關鍵字 final的理解
final可以用在三種情況:資料、方法和類。
修飾資料:表示變數一次賦值以後值不能被修改(常量),其名稱通常大寫。而用於物件的引用,final使得引用恆定不變,一旦引用被初始化指向一個物件,就無法更改,但是物件的自身可以修改的。
修飾方法
修飾類:表示該類不能被繼承,以提高程式的安全性和可讀性,如String、 System、StringBuffer類等。
final int COUNT = 1;
// COUNT = 2; // cannot assign value to final variable 'COUNT'
final Employee emp = new Employee();
emp.name = "丙子先生"; // 所引用物件的屬性可變
如何初始化final所修飾的成員變數?
關鍵字final修飾的成員變數沒有預設初始化值,其初始化方式主要有:
1.在宣告時,直接對其進行顯式初始化。
2.宣告完後,在程式碼塊中對其顯式初始化。
3.宣告完後,在構造器中對其顯式初始化,但注意需要在所有構造器中均對其 進行初始化操作。
基本原則:保證在物件建立之前即需要對final修飾的屬性進行初始化操作。
多型:
多型:指允許不同子型別的物件對同一訊息作出不同的響應,簡單來說就是 用同一物件引用呼叫同一方法卻做了不同的事情。
方法過載(overload)實現的是編譯時的多型性(也稱為前繫結),而方法重寫(override)實現的是執行時的多型性(也稱為後繫結)。
- 過載發生在一個類中,==同名的方法如果有不同的引數列表(引數型別不同、引數 個數不同或者二者都不同)==則視為過載;(不能根據根據返回型別來區分過載,因為在呼叫方法時並不會判斷方法的 返回值型別是什麼,如果根據返回值型別來區分過載,則程式會不知道去呼叫哪 個方法。)
- 重寫發生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的返回型別,比父類被重寫方法更好訪問,不能比父類被重寫方法宣告更多的異常(里氏代換原則)。
程式設計模式————狀態模式:
用繼承表達行為間的差異,並用欄位表達狀態上的變化。(java程式設計思想 P165)
問題: 在專案中哪些地方使用過多型?
實驗室預約軟體包含學生、教師和管理員三種角色,三者都有login 方法,但三者登入後進入的頁面卻是不同的,即在登入時會有不同的操作。三種 角色都繼承父類的login方法,但對不同的物件卻有不同的行為。
解析和分派:
Class 檔案的編譯過程中不包含傳統編譯中的連線步驟,一切方法呼叫在 Class 檔案裡面儲存的都只是符號引用,而不是方法在實際執行時記憶體佈局中的入口地址。
解析呼叫:
一定是個靜態過程,在編譯期間就完全確定,在類載入的解析階段就會把涉及的符號引用轉化為可確定的直接引用,不會延遲到執行期再去完成。(解析階段是虛擬機器將常量池中的符號引用替換為直接引用的過程)
能在編譯之前確定被呼叫方法的版本的方法:
靜態方法 //invokestatic 構造器方法 //invokespecial 私有方法 //invokespecial 父類方法 //invokespecial
分派呼叫:
則可能是靜態的也可能是動態的,根據分派依據的宗量數(方法的呼叫者和方法的引數統稱為方法的宗量)又可分為單分派和多分派。兩類分派方式兩兩組合便構成了靜態單分派、靜態多分派、動態單分派、動態多分派四種分派情況。
- 靜態分派
靜態分派的最典型應用就是多型性中的方法過載。靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機器來執行的。
class Human{
}
class Man extends Human{
}
class Woman extends Human{
}
public class StaticPai{
public void say(Human hum){
System.out.println("I am human");
}
public void say(Man hum){
System.out.println("I am man");
}
public void say(Woman hum){
System.out.println("I am woman");
}
public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();
StaticPai sp = new StaticPai();
sp.say(man);
sp.say(woman);
}
}
上面程式碼的執行結果如下:
I am human
I am human
分析原因:
“Human”稱為變數的靜態型別,後面的“Man”稱為變數的實際型別。在過載時是通過引數的靜態型別而不是實際型別作為判定依據的。並且靜態型別是編譯期可知的,所以在編譯階段,javac 編譯器就根據引數的靜態型別決定使用哪個過載版本。
- 動態分派
與靜態分派不同,動態分派最經典的案例是重寫,執行期根據方法接收者的實際型別來選擇方法。
class Eat{
}
class Drink{
}
class Father{
public void doSomething(Eat arg){
System.out.println("爸爸在吃飯");
}
public void doSomething(Drink arg){
System.out.println("爸爸在喝水");
}
}
class Child extends Father{
public void doSomething(Eat arg){
System.out.println("兒子在吃飯");
}
public void doSomething(Drink arg){
System.out.println("兒子在喝水");
}
}
public class SingleDoublePai{
public static void main(String[] args){
Father father = new Father();
Father child = new Child();
father.doSomething(new Eat());
child.doSomething(new Drink());
}
}
上面程式碼的執行結果如下:
爸爸在吃飯
兒子在喝水
分析原因:
編譯階段編譯器的選擇過程,即靜態分派過程。這時候選擇目標方法的依據有兩點:一是方法的接受者(即呼叫者)的靜態型別是 Father 還是 Child,二是方法引數型別是 Eat 還是 Drink。因為是根據兩個宗量進行選擇,所以 Java 語言的靜態分派屬於多分派型別。
執行階段虛擬機器的選擇,即動態分派過程。由於編譯期已經了確定了目標方法的引數型別(編譯期根據引數的靜態型別進行靜態分派),因此唯一可以影響到虛擬機器選擇的因素只有此方法的接受者的實際型別是 Father 還是 Child。
目前的 Java 語言(JDK1.6)是一門靜態多分派、動態單分派的語言。
方法過載優先順序匹配:
public static void main(String[] args) {
方法過載優先順序匹配 a = new 方法過載優先順序匹配();
//普通的過載一般就是同名方法不同引數。
//這裡我們來討論當同名方法只有一個引數時的情況。
//此時會呼叫char引數的方法。
//當沒有char引數的方法。會呼叫int型別的方法,如果沒有int就呼叫long
//即存在一個呼叫順序char -> int -> long ->double -> ..。
//當沒有基本型別對應的方法時,先自動裝箱,呼叫包裝類方法。
//如果沒有包裝類方法,則呼叫包裝類實現的介面的方法。
//最後再呼叫持有多個引數的char...方法。
a.eat('a');
a.eat('a','c','b');
}
public void eat(short i) {
System.out.println("short");
}
public void eat(int i) {
System.out.println("int");
}
public void eat(double i) {
System.out.println("double");
}
public void eat(long i) {
System.out.println("long");
}
public void eat(Character c) {
System.out.println("Character");
}
public void eat(Comparable c) {
System.out.println("Comparable");
}
public void eat(char ... c) {
System.out.println(Arrays.toString(c));
System.out.println("...");
}
// public void eat(char i) {
// System.out.println("char");
// }
向上轉型和向下轉型的解釋 :
public static void main(String[] args) {
Son son = new Son();
//首先先明確一點,轉型指的是左側引用的改變。
//father引用型別是Father,指向Son例項,就是向上轉型,既可以使用子類的方法,也可以使用父類的方法。
//向上轉型,此時執行father的方法
Father father = son;
father.smoke();
//不能使用子類獨有的方法。
// father.play();編譯會報錯
father.drive();
//Son型別的引用指向Father的例項,所以是向下轉型,不能使用子類非重寫的方法,可以使用父類的方法。
//向下轉型,此時運行了son的方法
Son son1 = (Son) father;
//轉型後就是一個正常的Son例項
son1.play();
son1.drive();
son1.smoke();
//因為向下轉型之前必須先經歷向上轉型。
//在向下轉型過程中,分為兩種情況:
//情況一:如果父類引用的物件如果引用的是指向的子類物件,
//那麼在向下轉型的過程中是安全的。也就是編譯是不會出錯誤的。
//因為執行期Son例項確實有這些方法
Father f1 = new Son();
Son s1 = (Son) f1;
s1.smoke();
s1.drive();
s1.play();
//情況二:如果父類引用的物件是父類本身,那麼在向下轉型的過程中是不安全的,編譯不會出錯,
//但是執行時會出現java.lang.ClassCastException錯誤。它可以使用instanceof來避免出錯此類錯誤。
//因為執行期Father例項並沒有這些方法。
Father f2 = new Father();
Son s2 = (Son) f2;
s2.drive();
s2.smoke();
s2.play();
//向下轉型和向上轉型的應用,有些人覺得這個操作沒意義,何必先向上轉型再向下轉型呢,不是多此一舉麼。其實可以用於方法引數中的型別聚合,然後具體操作再進行分解。
//比如add方法用List引用型別作為引數傳入,傳入具體類時經歷了向下轉型
add(new LinkedList());
add(new ArrayList());
//總結
//向上轉型和向下轉型都是針對引用的轉型,是編譯期進行的轉型,根據引用型別來判斷使用哪個方法
//並且在傳入方法時會自動進行轉型(有需要的話)。執行期將引用指向例項,如果是不安全的轉型則會報錯。
//若安全則繼續執行方法。
}
public static void add(List list) {
System.out.println(list);
//在操作具體集合時又經歷了向上轉型
// ArrayList arr = (ArrayList) list;
// LinkedList link = (LinkedList) list;
}
注意: father引用型別是Father,指向Son例項,就是向上轉型,既可以使用子類的方法,也可以使用父類的方法。father.play();編譯會報錯;
如果父類引用的物件是父類本身,那麼在向下轉型的過程中是不安全的。
編譯不會出錯,但是執行時會出java.lang.ClassCastException異常。它可以使用instanceof來避免出錯此類錯誤。
關於instanceof 關鍵字理解:
java 中的 instanceof 運算子是用來在執行時指出物件是否是特定類的一個例項。instanceof 通過返回一個布林值來指出,這個物件是否是這個特定類或者是它的子類的一個例項。
用法:
boolean result = object instanceof class (向下轉型)
引數:
Result:布林型別。
Object:必選項。任意物件表示式。
Class:必選項。任意已定義的物件類。
說明:
如果 object 是 class 的一個例項,則 instanceof 運算子返回 true。如果 object 不是指定類的一個例項,或者 object 是 null,則返回 false。