Java SE 學習筆記03
面向物件基礎
★面向物件強調資料結構第一,演算法第二,而面向過程強調演算法第一,資料結構第二
★類之間的關係:
依賴關係Dependence (”uses–a”):如果一個類的某個方法使用了另一個類的物件,我們稱前者依賴後者,那麼這兩個類之間存在依賴關係。如Person類中有一個transport方法要求傳入一個Car類的物件,即Person要使用Car運輸,那麼Person類就依賴於Car類
聚集關係Aggregation (”has–a”) :指的是一個類中包含有另一個類的物件,如Book類裡面包含有Date類。聚集是通過把一個類定義為另一個類的屬性來實現的
繼承關係Inheritance (”is–a”):指的兩個類存在繼承關係,如Student類和Person類之間就是繼承關係
★內聚(cohesion)和耦合(coupling)
內聚和耦合是OOD(Object Oriented Design)的兩個概念。具備良好質量的OOD應該達到高內聚、低耦合的要求。高耦合、低內聚會降低軟體系統的穩定性、可擴充套件性、可複用性
★內聚——一個類內部組成具有單一、明確目標的程度。高內聚是一種理想的狀態,其中類成員支援單一、明確的角色或職責
★耦合——一個類和另一個類聯絡的緊密程度。低耦合是一種理想的狀態,通過設計良好封裝的類,把類與類之間的相互吸引減至最低,一個類內部的改變不會波及到其他類的變動
★封裝(Encapsulation)
就是對屬性和方法的載體類的訪問只能通過其提供的介面(方法)來訪問,把實現細節隱藏起來,也就是說,具體實現對程式設計師來說是透明的
★封裝的好處在於對類內部的改變不會影響到其他程式碼
★封裝的實現:將成員變數定義為private,而訪問這些變數的方法定義為public
★封裝是程式重用(reuse)和可靠 (reliability)的關鍵
★繼承(Inheritance)
★當繼承一個類的時候,不僅可以繼承行為(方法),還可以繼承資料(成員變數)。私有的資料成員和方法不能被繼承
★JAVA中所有的類都是Object類的子類
★雖然子類繼承了父類的成員變數,但是對於父類私有的成員變數,子類並不能直接訪問
★物件(Objects):物件具有三個特徵——行為、狀態和特性
注意:Date date = new Date();這裡的date不是物件,而只是物件的引用
★構造方法(Constructors)
★構造方法和類名相同
★構造方法在一個類中可以有一個或多個(即使不寫構造方法,系統會有預設的無參構造方法,但如果自定義了帶有引數的構造方法,系統不會再建立無參構造器)
★構造方法可以包含0個或多個引數
★構造方法沒有返回型別
★構造方法只有new關鍵字能呼叫
★構造方法不能被繼承
★構造方法可以丟擲異常,如果父類的預設無參構造方法丟擲了異常,那麼子類的預設無參構造方法必須處理父類丟擲的異常
★對於子類的構造方法,如果沒有呼叫子類的其他構造方法,那麼其必須呼叫父類的構造方法,預設會呼叫父類的無參構造方法,如果父類沒有無參構造方法,那麼子類必須顯式呼叫父類的其他構造方法(super),否則編譯出錯
★構造方法的執行過程為:構造器直接呼叫其父類構造器,其父類的構造器再呼叫它的父類的構造器,直到呼叫Object類的構造器為止
★介面沒有構造器,抽象類的構造器在其實現類例項化時被呼叫
★this關鍵字
★this關鍵字可以用在構造器中,表示呼叫該類的其他構造器,以避免程式碼重複。語法形式為this(...)。其必須在構造器的第一行。但是不能呼叫自身
★this關鍵字用於構造方法時,不能同時使用super關鍵字
★this關鍵字也可以用於呼叫本類的其他方法。this.方法名
★this關鍵字不能用於static方法內
★super關鍵字
★super關鍵字代表了父類物件,即super關鍵字引用的是當前建立的類例項物件的父類的控制代碼
★super關鍵字可以用在構造器中,表示呼叫父類的構造器(如果子類沒有呼叫其他的構造器,也沒有顯式呼叫父類的構造器,其預設呼叫的是父類的無參構造器,如果父類沒有無參構造,那麼編譯出錯)
語法形式為super(...)。其必須位於構造器的第一行
★super關鍵字用於構造方法時,不能同時使用this關鍵字
★super關鍵字也可以用於呼叫父類的普通方法。super.方法名
★多型(polymorphism):指子類和父類具有同名的重寫方法,並且允許父類引用指向子類物件,即允許把一個子類物件賦予一個父類的引用型變數,在執行期由系統自動判定應該呼叫子類的方法還是父類的方法。多型是執行期行為
動態繫結:Automatically selecting the appropriate method at runtime is called dynamic binding
動態繫結的執行過程:
編譯器首先會找到子類與父類中所有的和被呼叫方法的方法簽名相同的方法。(注意,父類中只找public的)
編譯器會自動進行過載方法判斷(如果存在過載方法)
JVM在執行時根據實際的物件型別呼叫其方法
靜態繫結:If the method is private, static, final, or a constructor, then the compiler knows exactly which method to call. This is called static binding.
注意:多型性只存在於方法之上,如果父類物件的引用指向子類,呼叫方法時呼叫的是子類的重寫方法,呼叫屬性時呼叫的是父類的屬性(指的是父類與子類存在同名的屬性時,不過這樣的可能性不大,因為只有屬性為public時才能測試通過)
注意:當父類引用指向子類物件時,只能呼叫父類中已有的方法,不能呼叫子類中新新增的方法,雖然其實際型別是子類
這裡有個經典的例子:假設有Employee類,其有子類Manager類
Manager[] managers = new Manager[2];
Employee[] employees = managers;
Employees[1] = new Employee();
編譯通過,執行時會拋ArrayStroeException。因為多型是執行期行為。但這樣是正確的:
Employee[] employees = new Employee[2];
Employees[0] = new Manager();
因為兩個陣列其實指向的都是同一個Manager陣列,這個陣列中存放的是Manager物件
★JavaBean元件
JavaBean元件是具有屬性的JAVA類,其屬性一般為private的。因為屬性是私有的,因此從類外訪問屬性的唯一方法就是通過類提供的非私有方法
JavaBean元件中更改屬性值的方法成為setter方法,獲取屬性的方法成為getter方法
命名原則:public void set+屬性名首字母大寫(引數)
public (set方法引數的型別) get+屬性名首字母大寫(引數)
布林型別,可設定為is+屬性名首字母大寫
public是必須的
擴充套件:對於JAVA中Listener的命名規則:對於新增Listener addXXXListener(XXX監聽器);對於刪除Listener removeXXXListener(XXX監聽器型別);
★abstract關鍵字
★abstract關鍵字只能修飾類和方法,不能修飾變數,因為該關鍵字表明的是未實現的含義,而屬性不存在未實現
★abstract關鍵字修飾的方法為抽象方法,其不包含方法體
★abstract關鍵字修飾的類為抽象類。抽象類可以不包含抽象方法,但是含有抽象方法類一定要宣告為抽象類,抽象類也可以包含非抽象方法
★子類繼承抽象類,必須完全實現其所有的抽象方法,否則子類只能宣告為抽象類。當一個類實現一個介面時,必須完全實現介面內所有的抽象方法,否則也必須宣告為抽象類
★抽象類有構造方法,但是不能直接呼叫,否則編譯出錯。抽象類不能被例項化。根據多型性,抽象類可以指向其實現類
★抽象類不能被final修飾符修飾,因為抽象類需要子類繼承實現,而final修飾的類是不能被繼承的
★抽象方法不能被宣告為final的,也不能被private修飾符修飾,因為抽象方法需要被子類繼承而實現,而final修飾的方法和private修飾的方法是不能被繼承的
★final關鍵字
★final關鍵字可以修飾類、屬性、方法,表示該修飾的物件具有不可變性
★final修飾基本資料型別的變數為一個常量,其值是隻讀的。final修飾的變數必須在使用前被初始化,三種初始化方法:聲明後直接賦值;構造方法中賦值;靜態語句塊中賦值(只針對static final)
★final修飾引用變數,表示該變數一旦分配給一個值就不能再改變,即該變數一旦引用了一個物件後,再也不能引用其他物件,更不能將其他物件的控制代碼賦值給它。但是可以改變final變數所引用物件的內容
★final修飾符位於方法前,表示該方法不能被子類重寫,但是可以被過載
★final修飾符位於類前,表示該類不能被繼承,宣告為final的類,其所有的方法預設為final的,但是屬性不是final的
★final修飾符是唯一可以用於方法區域性變數的修飾符。
如果存在多個構造器,需要同時初始化final變數
★native關鍵字
★native關鍵字只能用於修飾方法。一個在方法名前加上native關鍵字的方法為本地方法,其目的是利用本地資源來擴充套件JAVA功能,與JAVA的機制無關
★本地方法如同抽象方法,沒有方法體,因為對本地方法的實現,不是JAVA語言,而是採用依賴於本地平臺的其他語言編寫的程式碼來實現的
它們位於JVM之外,可以通過System.loadLibrary()方法載入,其通常放在靜態程式碼塊中來裝入本地方法,如果System.loadLibrary()方法沒有載入成功,會丟擲UnsatisfiedLinkError
★Object類中的clone和notify方法都是本地方法
★本地方法可以被private修飾符修飾
★static關鍵字
★static關鍵字不能修飾頂層類,可以修飾內部類。可以修飾屬性、方法、程式碼塊。該修飾符表明所修飾的物件屬於類範疇,而非類例項
★static關鍵字修飾的類屬性有兩種引用方式,類名.屬性名;或者物件名.屬性名。推薦使用前者,因為後者可讀性不好
★類的靜態屬性被整個類所共享,因此,任何一個類例項改變了屬性的值,所有類的例項擁有的這個屬性值都會同步改變,其不能被序列化
★方法中宣告的變數不能是靜態的,因為方法中宣告的變數是區域性的
★static修飾的方法除了可以訪問其內部定義的變數外,只能訪問被static修飾符所修飾的靜態變數,如果需要訪問非靜態的屬性,則必須通過 物件.屬性 的方式
★對於靜態方法和非靜態方法的呼叫方式是不同的。對於非靜態方法的呼叫是在執行期決定的,而對靜態方法的呼叫是在編譯期決定的
★靜態程式碼塊主要用於初始化,該程式碼塊只被執行一次,即首次裝載類時被執行,如果存在多個靜態程式碼塊,那麼按照出現的先後順序被執行。相反,非靜態程式碼塊每次建立新物件時均會執行
辨析:靜態方法和非靜態方法
靜態方法:可以直接訪問本類的靜態屬性,其他的靜態方法,可以在其內部宣告非靜態變數,但不能宣告靜態變數,靜態方法中不能使用this和super關鍵字
非靜態方法:不能宣告靜態變數,可以宣告非靜態變數,可以呼叫本類的所有方法(包括靜態和非靜態),可以呼叫本類的所有屬性(包括靜態和非靜態)
經典程式碼:
public class HelloWorld{
static{
System.out.println("Hello, World");
System.exit(0);
}
}
★介面(interface)
★介面是一個純的抽象類。介面的語法為interface interfaceName{}
★介面預設的宣告為public abstract,因為其必須要由實現類實現
★介面中包含的方法全部為public abstract修飾的方法,其均沒有方法體
★介面中定義的屬性全部為public static final修飾的屬性,其均為常量
★介面實現通過implements關鍵字。實現類必須重寫介面中所有的方法,否則只能將介面的實現類必須宣告為抽象類
★介面可以繼承介面,通過extends關鍵字實現,且可以繼承多個介面,逗號分開
★介面中的方法不能被static、native、strictfp、final關鍵字修飾。
★介面不能繼承其他的類
注意:介面中的方法和常量可以有下面的宣告方式(全部合法)
常量宣告:(常量一旦宣告不能重新賦值)
public static final int x = 1;
public int x = 1;
int x = 1;
static int x = 1;
final int x = 1;
public static int x = 1;
public final int x = 1;
static final int x = 1;
方法宣告:
void method();
public void method1();
abstract void method1();
public abstract void method1();
abstract public void method1();
★方法重寫(override)和方法過載(overload)
★重寫:所謂方法重寫是指父類中的一個方法在子類中獲得重新定義,方法名、引數列表、返回型別均不改變
規則:
重寫方法必須和被重寫方法具有相同的方法名、引數列表和返回值型別
引數列表中要求數目、型別、順序必須完全一致
只有繼承存在的情況下才能重寫方法
JAVASE5開始允許協變返回型別,即重寫方法返回的型別是可以是被重寫方法返回型別的子類
重寫方法的訪問控制修飾符的範圍不能小於被重寫方法
重寫方法丟擲的異常必須和被重寫方法一致或是其子類,或者不丟擲
注意:對於介面中的方法重寫,其修飾符只能是public的
普通的成員方法的重寫是一種多型行為,而靜態方法實在類編譯的時候就確定了,也就是說靜態方法繫結到類,成員方法繫結到物件,即靜態方法不能重寫
對於父類私有的方法來說,子類中如果有同樣的方法不會構成重寫。(即私有方法不能被重寫,因為重寫依賴於繼承,而私有方法不能被繼承)
★過載:所謂方法過載是指一個類中定義多個方法名相同,引數列表不同的方法,所謂引數列表不同指的是引數的型別、數目、順序不同(三者之間有一個符合即構成方法過載)
規則:
過載方法必須擁有不同的引數列表,數目、型別、排列順序三者有一即可
過載方法在同一個類中定義
過載方法的返回型別、訪問修飾符以及丟擲的異常均不能作為過載的判斷標準
過載方法之間可以相互呼叫(指的是引數型別相同、數目不同的過載方法)
static關鍵字不能作為方法過載判定的標準
是否丟擲異常不能作為方法過載的判定標準
★訪問許可權修飾符(public、protected、default、private)[不考慮內部類]
★訪問許可權修飾符不能修飾區域性變數,只有final可以修飾區域性變數
★一個屬性或類只能有一個訪問許可權修飾符
★public修飾符可以修飾類、屬性、方法、構造方法
★public修飾符訪問許可權最大,其修飾的類、屬性、方法可以在任意位置被訪問
★protected修飾符可以修飾屬性、方法、構造方法
★protected修飾符修飾的屬性、方法只能被本類或本類的子類訪問,即使子類在不同的包中(同一個包中的類也可以訪問)
★default修飾符(預設沒有修飾符)可以修飾類、屬性、方法、構造方法、程式碼塊
★default修飾符修飾的類、屬性、方法等只能在本包中被訪問
★private修飾符可以修飾屬性、方法、構造方法
★private修飾的屬性、方法只能被本類訪問
注意:訪問範圍控制從類到成員,如果類不可見,其成員不管被任何訪問修飾符所修飾,均不可見。即要確定成員的可見性,首先要確定類的可見性
如果類A和類B在不同的包中且訪問修飾符均為default,假設A是B的父類,即使匯入了A,編譯依然不會通過。而且也不能宣告返回型別為A的方法或A型別的變數(A和B無繼承時也成立)
關於protected的奇怪之處:
package parent
public class Parent{
protected int x = 3;
}
package child
public class Child extends Parent{
public void test(){
System.out.println("x is " + x);--->正確
Parent parent = new Parent();--->正確
System.out.println("parent's x is" + p.x);-->不正確
}
}
結論:protected訪問許可權只允許子類跨包訪問,在子類中不允許使用父類的引用呼叫父類中的protected修飾的變數或方法(但靜態屬性和方法可以被呼叫)
即子類可跨包訪問,而父類遵守的是default訪問許可權(即從包外訪問protected修飾的變數和方法只能通過繼承實現)
此外,如果有一個類(Other)和子類處於同一個包中,其中有子類物件的引用,子類物件的引用也不能訪問父類中protected修飾的變數或方法,這時的protected許可權在子類中其實變成了private許可權,因此類外是不能訪問的
★protected = package + subclass
★Object類
★Object類是所有Java類的父類,如果一個類沒有使用關鍵字extends繼承一個類,其預設繼承了Object類
★equals()方法。equals()方法預設執行的是==的比較,參看Object類的原始碼可知。因此要想實現自定義類的比較需要重寫此方法自定義比較的規則。在API中有幾個類已重寫了equals()方法,如String、Date和封裝類
equals()方法遵循一些原則:
自反性 如果x不為null,x.equals(x)為true
對稱性 x.equals(y)為true,那y.equals(x)為true
傳遞性 x.equals(y)為true,y.equals(z)為true,那x.equals(z)返回值也為true
一致性 多次呼叫x.equals(y)結果均為true
非空性 如果x取值不為null,那麼x.equals(null)為false
equals()方法的一般寫法:(以 Person類為例)
public boolean equals(Object object){引數只能是Object類
if(this == object)return true;
if(object == null)return false;
if(this.getClass() != object.getClass())return false;
Person person = (Person)Object;
return ((this.name).equals(person.name)) && (this.age == person.age);
}
上面程式碼中的getClass()可以用instanceof測試。但不推薦使用instanceof。假設有一個Employee類和一個Manager類,後者繼承自前者,分別有兩個類的物件e和m,假設他們有相同的名字、薪水和入職日期。現在有e.equals(m),如果在equals方法中用(m instanceof Employee)來判斷,肯定為true,但是根據equals對稱性原則,要求m.equals(e)的返回結果為true,這時的equals方法中(e instanceof Manager)返回的要麼是false要麼丟擲異常
如果子類有自己判斷equals相等的原則,那麼對稱性原則要求強制使用getClass();
如果子類使用父類判斷equals相等的原則,那麼為了使不同子類之間的equals比較能夠進行,可以使用instanceof來判定。instanceof superclass
public boolean equals(Person person)這個不叫方法重寫。
陣列的equals()和hashCode()方法:呼叫Arrays.equals(type[] a, type[] b)和Arrays.hashCode(type[] a);
★hashCode()方法,其主要作用是配合equals()方法來使用
重寫equals()方法是必須重寫hashCode()方法。因為兩個物件如果用equals()方法比較相等的話,那麼他們的hashCode()方法返回值也必須相等
如果兩個物件的hashCode()方法返回值不相等,則兩個物件的equals()方法比較返回false,即兩個物件一定不相等
如果兩個物件equal()方法返回值為false,則兩個物件的hashCode()方法返回值並不一定要求一定不相等(即其hashCode可以相等)
hashCode()方法遵循原則:
多次呼叫x.hashCode()的返回值均是相等整數
hashCode()方法在重寫是根據equals()方法比較的內容重寫:
如Person的hashCode方法重寫為:
public int hashCode(){
return 7 * name.hashCode() + 13 * new Integer(age).hashCode();
}
★toString()方法:返回的是該物件的字串表示
只要物件和一個字串通過 + 號進行連線的話,會自動呼叫該物件的toString()方法
★對於一維陣列物件,呼叫的是Arrays.toString(arrayName)方法進行列印
★對於多維陣列物件,呼叫的是Arrays.deepToString(arrayName)方法進行列印
子類中對於父類的equals()、hashCode()、toString()方法的重寫是通過super呼叫父類中的equals()、hashCode()、toString()方法再加上子類自己新增的內容(這裡假設父類已重寫了Object的equals()hashCode()和toString()方法)
★clone()方法:protected修飾,用於拷貝物件,所有使用clone()方法的類都應該實現Cloneable介面(陣列除外)
淺克隆:如果一個類中不包含引用型別的屬性(如Date型別的屬性),那麼使用super.clone()方法可以實現該類物件的拷貝
深克隆:如果一個類中包含引用型別的屬性,那麼該類的每個引用型別的屬性也必須分別呼叫clone()方法才能保證對該類物件實現正確的拷貝
假如有一個員工類:其有String型別的name屬性和Date型別的hireDate屬性。那麼如何對其進行clone呢?首先Employee類必須實現Cloneable介面,然後重寫裡面的clone方法
public Employee clone() throws CloneNotSupportedException {
Employee cloned = new Employee();
cloned = (Employee) super.clone();
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
對於任何陣列物件,其clone方法都是public的
int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 };
int[] cloned = (int[]) luckyNumbers.clone();
cloned[5] = 12; // doesn't change luckyNumbers[5]
注意:如果原始物件中包含類似於String這樣的不可變物件,淺克隆將不會有任何問題