1、封裝
Thinking in java中說道,“封裝”通過合并特征和行為來創建新的數據類型。“實現隱藏”則通過將細節“私有化”把接口和實現分離開來。
因此,我們可以這樣來解釋封裝,字面上的意思就是包裝的意思,專業一點就是信息隱藏,是指利用抽象數據類型將數據以及基于這些數據的操作封裝在一起,成為一個不可分割的獨立實體。
外界不能直接訪問數據,只能通過包裹在數據之外的已授權的操作進行交流和交互。數據被保護在抽象數據類型的內部,盡可能地隱藏內部的實現細節,只提供一些可以對其進行訪問的公共的方式來與外部發生聯系。
抽象數據類型(ADT)是指一個數學模型及定義在該模型上的一組操作。 事實上,抽象數據類型體現了程序設計中問題分解和信息隱藏的特征。它把問題分解為多個規模較小且容易處理的問題,然后把每個功能模塊的實現為一個獨立單元,通過一次或多次調用來實現整個問題。
對于封裝而言,一個對象它所封裝的就是自己的屬性和方法,所以它不需要依賴任何對象就能完成自己的操作。
通常情況下,封裝方式有兩種:
- 將某一功能、屬性抽離出來,獨立寫成單獨的方法或類
- 設置訪問權限,類:public(公共的) 、default(默認的,不寫就默認是它);類中成員:public、protected、default(默認的)、private
封裝的好處
- 減少耦合度,提高代碼的復用性
- 隱藏信息,實現細節
- 類內部的結構可以自由修改。即讓我們更容易修改類的內部實現,而無需修改使用了該類的客戶代碼。
首先我們來看下面這個類:Student.java
public class Student{
/*
* 對屬性的封裝
* 一個人的姓名、性別、年齡、妻子都是這個人的私有屬性
*/
private String name ;
private String sex ;
private int age ;
/*
* setter()、getter()是該對象對外開發的接口
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
例如:有一天需要修改該類時,例如將age屬性修改為String類型?倘若你只有一處使用了這個類還好,如果你有幾十個甚至上百個這樣地方,你是不是要改到崩潰?如果使用了封裝,我們完全可以不需要做任何修改,只需要稍微改變下該類的setAge()方法即可。
//將age屬性由int類型轉變成String類型
public void setAge(int age) {
//轉換即可
this.age = String.valueOf(age);
}
- 可以對成員進行更精確的控制例子
例如:如果有時候腦袋犯渾,不小心把年齡設置為300歲,麻煩就大了。但是使用封裝我們就可以避免這個問題,我們對age的訪問入口做一些控制(setter)如:
public void setAge(int age) {
if(age > 120){
System.out.println("ERROR:error age input...."); //提示錯誤信息
}else{
this.age = age;
}
}
2、繼承
講解之前我們先看一個例子
public class Student{
private String name ;
private String sex ;
private int age ;
private Teacher teacher;
}
public class Teacher{
private String name ;
private String sex ;
private int age ;
private Student student;
}
從這里我們可以看出,Student、Teacher兩個類除了各自的Student、Teacher外其余部分全部相同,這樣子就造成了重復的代碼。盡可能地復用代碼是程序員一直追求的,而繼承就是復用代碼的方式之一。
這個例子我們可以發現不管是學生還是老師,他們都是人,他們都擁有人的屬性和行為,同時也是從人那里繼承來的這些屬性和行為的。
因此代碼可以如下改進:
public class Person{
private String name ;
private String sex ;
private int age ;
}
public class Teacher extends Person{
private Student student;
}
public class Student extends Person{
private Teacher teacher;
}
可以看出這個例子使用繼承后,除了代碼量的減少我們還能夠非常明顯的看到他們的關系。
繼承所描述的是“is-a”的關系,實際上繼承者是被繼承者的特殊化,它除了擁有被繼承者的特性外,還擁有自己獨有的特性。
例如貓有抓老鼠、爬樹等其他動物沒有的特性。同時在繼承關系中,繼承者完全可以替換被繼承者,反之則不可以,例如我們可以說貓是動物,但不能說動物是貓就是這個道理,這樣將貓看做動物稱之為“向上轉型”。
向上轉型:將子類轉換成父類,在繼承關系上面是向上移動的,所以一般稱之為向上轉型。由于向上轉型是從一個叫專用類型向較通用類型轉換,所以它總是安全的,唯一發生變化的可能就是屬性和方法的丟失。這就是為什么編譯器在“未曾明確表示轉型”活“未曾指定特殊標記”的情況下,仍然允許向上轉型的原因。
誠然,繼承定義了類如何相互關聯,共享特性。對于若干個相同或者相識的類,我們可以抽象出他們共有的行為或者屬相并將其定義成一個父類或者超類,然后用這些類繼承該父類,他們不僅可以擁有父類的屬性、方法還可以定義自己獨有的屬性或者方法。
同時在使用繼承時需要記住三句話:
- 子類擁有父類非private的屬性和方法。
- 子類可以擁有自己屬性和方法,即子類可以對父類進行擴展。
- 子類可以用自己的方式實現父類的方法。
綜上所述,使用繼承確實有許多的優點,除了將所有子類的共同屬性放入父類,實現代碼共享,避免重復外,還可以使得修改擴展繼承而來的實現比較簡單。
組合和繼承
組合和繼承是兩種復用代碼的方法:
組合:只需要在新的類中產生現有類的對象,由于新的類是由現有類的對象組成的,所以這個方法稱為組合,該方法只是復用了現有程序代碼的功能,而并非它的形式。
繼承:按照現有的類的類型進行創建新類。無需改變現有類的形式,采用現有類的形式并在其中添加新的代碼,稱之為繼承。
總得來說,繼承表達的是“is-a"(是一個)的關系,而組合表達的是“has-a”(有一個)的關系。
在面向對象編程中,生成和使用程序最有可能采用的方法就是直接將數據和方法包裝進一個類中,并使用該類的對象。也可以運用組合技術使用現有類來開發新的類;而繼承技術其實是不太常用的。
謹慎繼承
上面講了繼承所帶來的諸多好處,那我們是不是就可以大肆地使用繼承呢?送你一句話:慎用繼承。
首先我們需要明確,繼承存在如下缺陷:
- 父類變,子類就必須變。
- 繼承破壞了封裝,對于父類而言,它的實現細節對與子類來說都是透明的。
- 繼承是一種強耦合關系。
所以說當我們使用繼承的時候,我們需要確信使用繼承確實是有效可行的辦法。那么到底要不要使用繼承呢?《Thinking in java》中提供了解決辦法:問一問自己是否需要從子類向父類進行向上轉型。如果必須向上轉型,則繼承是必要的,但是如果不需要,則應當好好考慮自己是否需要繼承。
多態
1、概念定義
一個引用變量究竟會指向哪一個實例對象,該引用變量發出的方法調用究竟是哪一個類的實現方法,必須由程序運行期間才能決定。
好處:不用修改源程序代碼,就可以讓引用變量綁定到各種不同的類實現上,從而導致該引用調用的具體方法隨之改變,即不修改程序代碼就可以改變程序運行時所綁定的具體代碼,讓程序可以選擇多個運行狀態,這就是多態性。
指向子類的父類引用:既能引用父類的共性,也能使用子類強大的實現。該引用既可以處理父類對象,也可以處理子類對象。當相同的消息發送給子類或者父類的對象時,該對象會根據自己所屬的引用來執行不用的方法。
向上轉型:只能使用父類的屬性和方法。對于子類中存在而父類中不存在的方法,該方法是不能引用的,例如重載;對于子類重寫父類的方法,調用這些方法時,使用子類定義的方法。
編譯時多態(靜態,運行時不是多態):重載
運行時多態(動態綁定,運行時多態):重寫
2、多態的實現
2.1 實現條件(三個):繼承、重寫、向上轉型(詳講)。
向上轉型:由導出類轉型為基類,即在多態中需要將子類的引用賦給父類對象(指向子類的父類引用),只有這樣該引用才能夠具備技能調用父類的方法和子類的方法。這是從較專用類型向較通用類型轉換,所以總是很安全的。
例子:
public enum Note {
MIDDLE_C, C_SHARP, B_FLAT; // Etc.
}
class Instrument {
public void play(Note n) {
print("Instrument.play()");
}
}
//繼承
public class Wind extends Instrument {
// 重寫:
public void play(Note n) {
System.out.println("Wind.play() " + n);
}
}
public class Music {
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // 向上轉型
}
}
/* Output:
* Wind.play() MIDDLE_C
*/
分析: Music.tune()方法接受一個Instrument引用,同時也接受任何導出自Instrument的類。在main方法中,當一個Wind引用傳遞到tune()方法時,就會出現這種情況,而不需要任何類型轉換。這樣做是允許的,因為Wind是從Instrument繼承而來,所以Instrument的接口必定存在于Wind中。從Wind向上轉型到Instrument可能會“縮小”接口,但不會比Instrument的全部接口更窄。
忘記對象類型
Music.Java看起來似乎有些奇怪。為什么所有人都故意忘記對象的類型呢?在進行向上轉型時,就會產生這種情況;并且如果讓tune()方法直接接受一個Wind引用作為自己的參數,似乎會更為直觀。但這樣引發的一個重要問題是:如果那樣做,就需要創建多個tune()方法來接受不同類型的參數。假設按這種推理,現在再加入Stringed(弦樂)這種Instrument(樂器):
class Stringed extends Instrument {
public void play(Note n) {
print("Stringed.play() " + n);
}
}
public class Music2 {
public static void tune(Wind i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Stringed i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
Stringed violin = new Stringed();
tune(flute); //沒有向上轉型
tune(violin);
}
} /* Output:
Wind.play() MIDDLE_C
Stringed.play() MIDDLE_C
*/
這樣做行得通,但是有一個主要缺點:必須為添加的每一個新Instrument類編寫特定類型的方法,這意味著在開始時就需要更多的編程。
如果我們只寫這樣一個簡單的方法,它僅接受基類作為參數,而不是那些特殊的導出類,這樣做情況就會變得更好,也就是說,如果我們不管導出類的存在,編寫的代碼只是與基類打交道,是更好的方式。這也正是多態多允許的。
2.2 轉機
運行這個程序后,我們便會發現Music.java的難點所在。
public static void tune(Instrument i){
//...
i.play(Note.MIDDLE_C);
tune()方法它接受一個Instrument引用。那么在這種情況下,編譯器怎么樣才能知道這個Instrument引用指向的是Wind對象,而不是Brass對象或Stringed對象呢?實際上編譯器無法得知。為了理解這么問題,有必要說明下有關綁定的問題。
2.2.1 方法調用綁定
將一個方法調用同一個方法主體關聯起來被稱為綁定。若在程序執行前進行綁定,叫作前期綁定。在運行時根據對象的類型進行綁定,叫作后期綁定(也叫作動態綁定或運行時綁定)。
Java中除了static方法和final方法(private方法屬于final方法)之外,其他所有的方法都是后期綁定。這意味著通常情況下,我們不必判定是否應該進行后期綁定,它會自動發生。
注意:只有非public的方法才可以被覆蓋。只有在導出類中是覆蓋了基類的方法這種情況時,才會有所謂的基類引用調用指向的導出類的方法。
為什么要將某個方法聲明為final呢?它可以防止其他人覆蓋該方法,但更重要的一點或許是:這樣做可以有效地“關閉”動態綁定,或者說,告訴編譯器不需要對其進行動態綁定。
2.2.2 產生正確的行為
一旦知道Java中所有方法都是通過動態綁定實現多態這個事實之后,我們就可以編寫只與基類打交道的代碼了,并且這些代碼對所有的導出類都可以正確運行。或者換一種說法,發送消息給某個對象,讓該對象去斷定應該做什么事。
面向對象程序設計中,有一個經典例子就是“幾何形狀”。這個例子中,有一個基類Sharp,以及多個導出類,如Circle、Square、Triangle等。
向上轉型可以像下面這條語句這么簡單:Sharp s = new Cicle();這里創建了一個Circle對象,并把得到的引用立即賦值給Sharp,這樣做看似錯誤(將一種類型賦值給另一種類型);但實際上是沒問題的,因為通過繼承,Circle就是一種Shape。因此,編譯器認可這條語句。
2.3 構造器和多態
通常,構造器不用于其他種類的方法。涉及到多態時仍是如此。盡管構造器并不具有多態性(它們實際上是static方法,只不過該static聲明是隱式的),但還是非常有必要理解構造器怎樣通過多態在復雜的層次結構中運作。
構造器的調用順序
基類的構造器總是在導出類的構造過程中被調用,而且按照繼承層次逐漸向上鏈接,以便每個基類的構造器都能得到調用。這樣做是有意義的,因為構造器具有一項特殊任務:檢查對象是否被正確的構造。
導出類只能訪問它自己的成員,不能訪問基類中的成員(基類成員通常是private類型)。只有基類的構造器才具有恰當的知識和權限來對自己的元素進行初始化。因此,必須令所有構造器都得到調用,否則就不可能正確構造完整對象。這正是編譯器為什么要強制每個導出類部分都必須調用構造器的原因。
在導出類的構造器主體中,如果沒有明確指定調用某個基類構造器,它就會“默默”調用默認構造器。如果不存在默認構造器,編譯器就會報錯(若某個類沒有構造器,編譯器會自動合成出一個默認構造器)。
class Meal {
Meal() { print("Meal()"); }
}
class Bread {
Bread() { print("Bread()"); }
}
class Cheese {
Cheese() { print("Cheese()"); }
}
class Lunch extends Meal {
Lunch() { print("Lunch()"); }
}
class PortableLunch extends Lunch {
PortableLunch() { print("PortableLunch()");}
}
public class Sandwich extends PortableLunch {
private Bread b = new Bread();
private Cheese c = new Cheese();
public Sandwich() { print("Sandwich()"); }
public static void main(String[] args) {
new Sandwich();
}
} /* Output:
* Meal()
* Lunch()
* PortableLunch()
* Bread()
* Cheese()
* Sandwich()
*/
這也表明了這一復雜對象調用構造器要遵照下面的順序:
- 在其他任何事物發生之前,將分配給對象的存儲空間初始化成二進制的零。(防止如果在一個構造器的內部調用正在構造的某個對象的某個動態綁定方法,而這個方法所操作的成員可能還未初始化的災難)
- 調用基類構造器。這個步驟會不斷的反復遞歸下去,首先是構造這種層次結構的根,然后是下一層導出類,等等,直到最低層的導出類。
- 按聲明順序調用成員的初始化方法。
- 調用導出類構造器的主體。
2.4 實現方法
基于繼承的多態:對于引用子類的父類類型,在處理該引用時,它適用于繼承該父類的所有子類,子類對象的不同,對方法的實現也就不同,執行相同動作產生的行為也就不同。即當子類重寫父類的方法被調用時,只有對象繼承鏈中的最末端的方法才會被調用。
基于接口的多態: 在接口的多態中,指向接口的引用必須是指定這實現了該接口的一個類的實例程序,在運行時,根據對象引用的實際類型來執行對應的方法。
2.5 多態機制遵循的原則
當父類對象引用變量 引用 子類對象時,被引用對象的類型(子類)而不是引用變量的類型(父類)決定了調用誰的成員方法,但是這個被調用的方法必須是在父類中定義過的,也就是說被子類覆蓋的方法(重寫)。