1、封裝
Thinking in java中說道,“封裝”通過合并特征和行為來創(chuàng)建新的數(shù)據(jù)類型?!皩?shí)現(xiàn)隱藏”則通過將細(xì)節(jié)“私有化”把接口和實(shí)現(xiàn)分離開來。
因此,我們可以這樣來解釋封裝,字面上的意思就是包裝的意思,專業(yè)一點(diǎn)就是信息隱藏,是指利用抽象數(shù)據(jù)類型將數(shù)據(jù)以及基于這些數(shù)據(jù)的操作封裝在一起,成為一個(gè)不可分割的獨(dú)立實(shí)體。
外界不能直接訪問數(shù)據(jù),只能通過包裹在數(shù)據(jù)之外的已授權(quán)的操作進(jìn)行交流和交互。數(shù)據(jù)被保護(hù)在抽象數(shù)據(jù)類型的內(nèi)部,盡可能地隱藏內(nèi)部的實(shí)現(xiàn)細(xì)節(jié),只提供一些可以對(duì)其進(jìn)行訪問的公共的方式來與外部發(fā)生聯(lián)系。
抽象數(shù)據(jù)類型(ADT)是指一個(gè)數(shù)學(xué)模型及定義在該模型上的一組操作。 事實(shí)上,抽象數(shù)據(jù)類型體現(xiàn)了程序設(shè)計(jì)中問題分解和信息隱藏的特征。它把問題分解為多個(gè)規(guī)模較小且容易處理的問題,然后把每個(gè)功能模塊的實(shí)現(xiàn)為一個(gè)獨(dú)立單元,通過一次或多次調(diào)用來實(shí)現(xiàn)整個(gè)問題。
對(duì)于封裝而言,一個(gè)對(duì)象它所封裝的就是自己的屬性和方法,所以它不需要依賴任何對(duì)象就能完成自己的操作。
通常情況下,封裝方式有兩種:
- 將某一功能、屬性抽離出來,獨(dú)立寫成單獨(dú)的方法或類
- 設(shè)置訪問權(quán)限,類:public(公共的) 、default(默認(rèn)的,不寫就默認(rèn)是它);類中成員:public、protected、default(默認(rèn)的)、private
封裝的好處
- 減少耦合度,提高代碼的復(fù)用性
- 隱藏信息,實(shí)現(xiàn)細(xì)節(jié)
- 類內(nèi)部的結(jié)構(gòu)可以自由修改。即讓我們更容易修改類的內(nèi)部實(shí)現(xiàn),而無需修改使用了該類的客戶代碼。
首先我們來看下面這個(gè)類:Student.java
public class Student{
/*
* 對(duì)屬性的封裝
* 一個(gè)人的姓名、性別、年齡、妻子都是這個(gè)人的私有屬性
*/
private String name ;
private String sex ;
private int age ;
/*
* setter()、getter()是該對(duì)象對(duì)外開發(fā)的接口
*/
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;
}
}
例如:有一天需要修改該類時(shí),例如將age屬性修改為String類型?倘若你只有一處使用了這個(gè)類還好,如果你有幾十個(gè)甚至上百個(gè)這樣地方,你是不是要改到崩潰?如果使用了封裝,我們完全可以不需要做任何修改,只需要稍微改變下該類的setAge()方法即可。
//將age屬性由int類型轉(zhuǎn)變成String類型
public void setAge(int age) {
//轉(zhuǎn)換即可
this.age = String.valueOf(age);
}
- 可以對(duì)成員進(jìn)行更精確的控制例子
例如:如果有時(shí)候腦袋犯渾,不小心把年齡設(shè)置為300歲,麻煩就大了。但是使用封裝我們就可以避免這個(gè)問題,我們對(duì)age的訪問入口做一些控制(setter)如:
public void setAge(int age) {
if(age > 120){
System.out.println("ERROR:error age input...."); //提示錯(cuò)誤信息
}else{
this.age = age;
}
}
2、繼承
講解之前我們先看一個(gè)例子
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兩個(gè)類除了各自的Student、Teacher外其余部分全部相同,這樣子就造成了重復(fù)的代碼。盡可能地復(fù)用代碼是程序員一直追求的,而繼承就是復(fù)用代碼的方式之一。
這個(gè)例子我們可以發(fā)現(xiàn)不管是學(xué)生還是老師,他們都是人,他們都擁有人的屬性和行為,同時(shí)也是從人那里繼承來的這些屬性和行為的。
因此代碼可以如下改進(jìn):
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;
}
可以看出這個(gè)例子使用繼承后,除了代碼量的減少我們還能夠非常明顯的看到他們的關(guān)系。
繼承所描述的是“is-a”的關(guān)系,實(shí)際上繼承者是被繼承者的特殊化,它除了擁有被繼承者的特性外,還擁有自己獨(dú)有的特性。
例如貓有抓老鼠、爬樹等其他動(dòng)物沒有的特性。同時(shí)在繼承關(guān)系中,繼承者完全可以替換被繼承者,反之則不可以,例如我們可以說貓是動(dòng)物,但不能說動(dòng)物是貓就是這個(gè)道理,這樣將貓看做動(dòng)物稱之為“向上轉(zhuǎn)型”。
向上轉(zhuǎn)型:將子類轉(zhuǎn)換成父類,在繼承關(guān)系上面是向上移動(dòng)的,所以一般稱之為向上轉(zhuǎn)型。由于向上轉(zhuǎn)型是從一個(gè)叫專用類型向較通用類型轉(zhuǎn)換,所以它總是安全的,唯一發(fā)生變化的可能就是屬性和方法的丟失。這就是為什么編譯器在“未曾明確表示轉(zhuǎn)型”活“未曾指定特殊標(biāo)記”的情況下,仍然允許向上轉(zhuǎn)型的原因。
誠(chéng)然,繼承定義了類如何相互關(guān)聯(lián),共享特性。對(duì)于若干個(gè)相同或者相識(shí)的類,我們可以抽象出他們共有的行為或者屬相并將其定義成一個(gè)父類或者超類,然后用這些類繼承該父類,他們不僅可以擁有父類的屬性、方法還可以定義自己獨(dú)有的屬性或者方法。
同時(shí)在使用繼承時(shí)需要記住三句話:
- 子類擁有父類非private的屬性和方法。
- 子類可以擁有自己屬性和方法,即子類可以對(duì)父類進(jìn)行擴(kuò)展。
- 子類可以用自己的方式實(shí)現(xiàn)父類的方法。
綜上所述,使用繼承確實(shí)有許多的優(yōu)點(diǎn),除了將所有子類的共同屬性放入父類,實(shí)現(xiàn)代碼共享,避免重復(fù)外,還可以使得修改擴(kuò)展繼承而來的實(shí)現(xiàn)比較簡(jiǎn)單。
組合和繼承
組合和繼承是兩種復(fù)用代碼的方法:
組合:只需要在新的類中產(chǎn)生現(xiàn)有類的對(duì)象,由于新的類是由現(xiàn)有類的對(duì)象組成的,所以這個(gè)方法稱為組合,該方法只是復(fù)用了現(xiàn)有程序代碼的功能,而并非它的形式。
繼承:按照現(xiàn)有的類的類型進(jìn)行創(chuàng)建新類。無需改變現(xiàn)有類的形式,采用現(xiàn)有類的形式并在其中添加新的代碼,稱之為繼承。
總得來說,繼承表達(dá)的是“is-a"(是一個(gè))的關(guān)系,而組合表達(dá)的是“has-a”(有一個(gè))的關(guān)系。
在面向?qū)ο缶幊讨?,生成和使用程序最有可能采用的方法就是直接將?shù)據(jù)和方法包裝進(jìn)一個(gè)類中,并使用該類的對(duì)象。也可以運(yùn)用組合技術(shù)使用現(xiàn)有類來開發(fā)新的類;而繼承技術(shù)其實(shí)是不太常用的。
謹(jǐn)慎繼承
上面講了繼承所帶來的諸多好處,那我們是不是就可以大肆地使用繼承呢?送你一句話:慎用繼承。
首先我們需要明確,繼承存在如下缺陷:
- 父類變,子類就必須變。
- 繼承破壞了封裝,對(duì)于父類而言,它的實(shí)現(xiàn)細(xì)節(jié)對(duì)與子類來說都是透明的。
- 繼承是一種強(qiáng)耦合關(guān)系。
所以說當(dāng)我們使用繼承的時(shí)候,我們需要確信使用繼承確實(shí)是有效可行的辦法。那么到底要不要使用繼承呢?《Thinking in java》中提供了解決辦法:?jiǎn)栆粏栕约菏欠裥枰獜淖宇愊蚋割愡M(jìn)行向上轉(zhuǎn)型。如果必須向上轉(zhuǎn)型,則繼承是必要的,但是如果不需要,則應(yīng)當(dāng)好好考慮自己是否需要繼承。
多態(tài)
1、概念定義
一個(gè)引用變量究竟會(huì)指向哪一個(gè)實(shí)例對(duì)象,該引用變量發(fā)出的方法調(diào)用究竟是哪一個(gè)類的實(shí)現(xiàn)方法,必須由程序運(yùn)行期間才能決定。
好處:不用修改源程序代碼,就可以讓引用變量綁定到各種不同的類實(shí)現(xiàn)上,從而導(dǎo)致該引用調(diào)用的具體方法隨之改變,即不修改程序代碼就可以改變程序運(yùn)行時(shí)所綁定的具體代碼,讓程序可以選擇多個(gè)運(yùn)行狀態(tài),這就是多態(tài)性。
指向子類的父類引用:既能引用父類的共性,也能使用子類強(qiáng)大的實(shí)現(xiàn)。該引用既可以處理父類對(duì)象,也可以處理子類對(duì)象。當(dāng)相同的消息發(fā)送給子類或者父類的對(duì)象時(shí),該對(duì)象會(huì)根據(jù)自己所屬的引用來執(zhí)行不用的方法。
向上轉(zhuǎn)型:只能使用父類的屬性和方法。對(duì)于子類中存在而父類中不存在的方法,該方法是不能引用的,例如重載;對(duì)于子類重寫父類的方法,調(diào)用這些方法時(shí),使用子類定義的方法。
編譯時(shí)多態(tài)(靜態(tài),運(yùn)行時(shí)不是多態(tài)):重載
運(yùn)行時(shí)多態(tài)(動(dòng)態(tài)綁定,運(yùn)行時(shí)多態(tài)):重寫
2、多態(tài)的實(shí)現(xiàn)
2.1 實(shí)現(xiàn)條件(三個(gè)):繼承、重寫、向上轉(zhuǎn)型(詳講)。
向上轉(zhuǎn)型:由導(dǎo)出類轉(zhuǎn)型為基類,即在多態(tài)中需要將子類的引用賦給父類對(duì)象(指向子類的父類引用),只有這樣該引用才能夠具備技能調(diào)用父類的方法和子類的方法。這是從較專用類型向較通用類型轉(zhuǎn)換,所以總是很安全的。
例子:
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); // 向上轉(zhuǎn)型
}
}
/* Output:
* Wind.play() MIDDLE_C
*/
分析: Music.tune()方法接受一個(gè)Instrument引用,同時(shí)也接受任何導(dǎo)出自Instrument的類。在main方法中,當(dāng)一個(gè)Wind引用傳遞到tune()方法時(shí),就會(huì)出現(xiàn)這種情況,而不需要任何類型轉(zhuǎn)換。這樣做是允許的,因?yàn)閃ind是從Instrument繼承而來,所以Instrument的接口必定存在于Wind中。從Wind向上轉(zhuǎn)型到Instrument可能會(huì)“縮小”接口,但不會(huì)比Instrument的全部接口更窄。
忘記對(duì)象類型
Music.Java看起來似乎有些奇怪。為什么所有人都故意忘記對(duì)象的類型呢?在進(jìn)行向上轉(zhuǎn)型時(shí),就會(huì)產(chǎn)生這種情況;并且如果讓tune()方法直接接受一個(gè)Wind引用作為自己的參數(shù),似乎會(huì)更為直觀。但這樣引發(fā)的一個(gè)重要問題是:如果那樣做,就需要?jiǎng)?chuàng)建多個(gè)tune()方法來接受不同類型的參數(shù)。假設(shè)按這種推理,現(xiàn)在再加入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); //沒有向上轉(zhuǎn)型
tune(violin);
}
} /* Output:
Wind.play() MIDDLE_C
Stringed.play() MIDDLE_C
*/
這樣做行得通,但是有一個(gè)主要缺點(diǎn):必須為添加的每一個(gè)新Instrument類編寫特定類型的方法,這意味著在開始時(shí)就需要更多的編程。
如果我們只寫這樣一個(gè)簡(jiǎn)單的方法,它僅接受基類作為參數(shù),而不是那些特殊的導(dǎo)出類,這樣做情況就會(huì)變得更好,也就是說,如果我們不管導(dǎo)出類的存在,編寫的代碼只是與基類打交道,是更好的方式。這也正是多態(tài)多允許的。
2.2 轉(zhuǎn)機(jī)
運(yùn)行這個(gè)程序后,我們便會(huì)發(fā)現(xiàn)Music.java的難點(diǎn)所在。
public static void tune(Instrument i){
//...
i.play(Note.MIDDLE_C);
tune()方法它接受一個(gè)Instrument引用。那么在這種情況下,編譯器怎么樣才能知道這個(gè)Instrument引用指向的是Wind對(duì)象,而不是Brass對(duì)象或Stringed對(duì)象呢?實(shí)際上編譯器無法得知。為了理解這么問題,有必要說明下有關(guān)綁定的問題。
2.2.1 方法調(diào)用綁定
將一個(gè)方法調(diào)用同一個(gè)方法主體關(guān)聯(lián)起來被稱為綁定。若在程序執(zhí)行前進(jìn)行綁定,叫作前期綁定。在運(yùn)行時(shí)根據(jù)對(duì)象的類型進(jìn)行綁定,叫作后期綁定(也叫作動(dòng)態(tài)綁定或運(yùn)行時(shí)綁定)。
Java中除了static方法和final方法(private方法屬于final方法)之外,其他所有的方法都是后期綁定。這意味著通常情況下,我們不必判定是否應(yīng)該進(jìn)行后期綁定,它會(huì)自動(dòng)發(fā)生。
注意:只有非public的方法才可以被覆蓋。只有在導(dǎo)出類中是覆蓋了基類的方法這種情況時(shí),才會(huì)有所謂的基類引用調(diào)用指向的導(dǎo)出類的方法。
為什么要將某個(gè)方法聲明為final呢?它可以防止其他人覆蓋該方法,但更重要的一點(diǎn)或許是:這樣做可以有效地“關(guān)閉”動(dòng)態(tài)綁定,或者說,告訴編譯器不需要對(duì)其進(jìn)行動(dòng)態(tài)綁定。
2.2.2 產(chǎn)生正確的行為
一旦知道Java中所有方法都是通過動(dòng)態(tài)綁定實(shí)現(xiàn)多態(tài)這個(gè)事實(shí)之后,我們就可以編寫只與基類打交道的代碼了,并且這些代碼對(duì)所有的導(dǎo)出類都可以正確運(yùn)行。或者換一種說法,發(fā)送消息給某個(gè)對(duì)象,讓該對(duì)象去斷定應(yīng)該做什么事。
面向?qū)ο蟪绦蛟O(shè)計(jì)中,有一個(gè)經(jīng)典例子就是“幾何形狀”。這個(gè)例子中,有一個(gè)基類Sharp,以及多個(gè)導(dǎo)出類,如Circle、Square、Triangle等。

向上轉(zhuǎn)型可以像下面這條語句這么簡(jiǎn)單:Sharp s = new Cicle();這里創(chuàng)建了一個(gè)Circle對(duì)象,并把得到的引用立即賦值給Sharp,這樣做看似錯(cuò)誤(將一種類型賦值給另一種類型);但實(shí)際上是沒問題的,因?yàn)橥ㄟ^繼承,Circle就是一種Shape。因此,編譯器認(rèn)可這條語句。
2.3 構(gòu)造器和多態(tài)
通常,構(gòu)造器不用于其他種類的方法。涉及到多態(tài)時(shí)仍是如此。盡管構(gòu)造器并不具有多態(tài)性(它們實(shí)際上是static方法,只不過該static聲明是隱式的),但還是非常有必要理解構(gòu)造器怎樣通過多態(tài)在復(fù)雜的層次結(jié)構(gòu)中運(yùn)作。
構(gòu)造器的調(diào)用順序
基類的構(gòu)造器總是在導(dǎo)出類的構(gòu)造過程中被調(diào)用,而且按照繼承層次逐漸向上鏈接,以便每個(gè)基類的構(gòu)造器都能得到調(diào)用。這樣做是有意義的,因?yàn)闃?gòu)造器具有一項(xiàng)特殊任務(wù):檢查對(duì)象是否被正確的構(gòu)造。
導(dǎo)出類只能訪問它自己的成員,不能訪問基類中的成員(基類成員通常是private類型)。只有基類的構(gòu)造器才具有恰當(dāng)?shù)闹R(shí)和權(quán)限來對(duì)自己的元素進(jìn)行初始化。因此,必須令所有構(gòu)造器都得到調(diào)用,否則就不可能正確構(gòu)造完整對(duì)象。這正是編譯器為什么要強(qiáng)制每個(gè)導(dǎo)出類部分都必須調(diào)用構(gòu)造器的原因。
在導(dǎo)出類的構(gòu)造器主體中,如果沒有明確指定調(diào)用某個(gè)基類構(gòu)造器,它就會(huì)“默默”調(diào)用默認(rèn)構(gòu)造器。如果不存在默認(rèn)構(gòu)造器,編譯器就會(huì)報(bào)錯(cuò)(若某個(gè)類沒有構(gòu)造器,編譯器會(huì)自動(dòng)合成出一個(gè)默認(rèn)構(gòu)造器)。
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()
*/
這也表明了這一復(fù)雜對(duì)象調(diào)用構(gòu)造器要遵照下面的順序:
- 在其他任何事物發(fā)生之前,將分配給對(duì)象的存儲(chǔ)空間初始化成二進(jìn)制的零。(防止如果在一個(gè)構(gòu)造器的內(nèi)部調(diào)用正在構(gòu)造的某個(gè)對(duì)象的某個(gè)動(dòng)態(tài)綁定方法,而這個(gè)方法所操作的成員可能還未初始化的災(zāi)難)
- 調(diào)用基類構(gòu)造器。這個(gè)步驟會(huì)不斷的反復(fù)遞歸下去,首先是構(gòu)造這種層次結(jié)構(gòu)的根,然后是下一層導(dǎo)出類,等等,直到最低層的導(dǎo)出類。
- 按聲明順序調(diào)用成員的初始化方法。
- 調(diào)用導(dǎo)出類構(gòu)造器的主體。
2.4 實(shí)現(xiàn)方法
基于繼承的多態(tài):對(duì)于引用子類的父類類型,在處理該引用時(shí),它適用于繼承該父類的所有子類,子類對(duì)象的不同,對(duì)方法的實(shí)現(xiàn)也就不同,執(zhí)行相同動(dòng)作產(chǎn)生的行為也就不同。即當(dāng)子類重寫父類的方法被調(diào)用時(shí),只有對(duì)象繼承鏈中的最末端的方法才會(huì)被調(diào)用。
基于接口的多態(tài): 在接口的多態(tài)中,指向接口的引用必須是指定這實(shí)現(xiàn)了該接口的一個(gè)類的實(shí)例程序,在運(yùn)行時(shí),根據(jù)對(duì)象引用的實(shí)際類型來執(zhí)行對(duì)應(yīng)的方法。
2.5 多態(tài)機(jī)制遵循的原則
當(dāng)父類對(duì)象引用變量 引用 子類對(duì)象時(shí),被引用對(duì)象的類型(子類)而不是引用變量的類型(父類)決定了調(diào)用誰的成員方法,但是這個(gè)被調(diào)用的方法必須是在父類中定義過的,也就是說被子類覆蓋的方法(重寫)。