- 複用代碼是Java衆多引人注目的功能之一。但要想成爲極具革命性的語言, 僅僅能夠複製代碼並對之加以改變是不夠的,它還必須能夠做更多的事情。
- 新的類是由現有類的對象所組成,這種方法稱爲 組合。
- 按照現有類型來創建新類。無需改變現有類的形式,採用現有類的形式並在其中添加新代碼。被稱爲 繼承。
組合語法
- 組合,只需將對象引用置於新類中即可。
public class WateSource {
private String s;
WateSource(){
System.out.println("WateSource");
s="Constructed";
}
@Override
public String toString() {
return s;
}
}
//第二個類
public class SprinklerSystem {
private String v1,v2,v3,v4;
private WateSource wateSource=new WateSource();
private int i;
private float f;
@Override
public String toString() {
return "SprinklerSystem{" +
"v1='" + v1 + '\'' +
", v2='" + v2 + '\'' +
", v3='" + v3 + '\'' +
", v4='" + v4 + '\'' +
", wateSource=" + wateSource +
", i=" + i +
", f=" + f +
'}';
}
public static void main(String[] args) {
SprinklerSystem system = new SprinklerSystem();
System.out.println(system);
}
//運行結果爲
WateSource
SprinklerSystem{v1='null', v2='null', v3='null', v4='null', wateSource=Constructed, i=0, f=0.0}
- 上面倆個類所定義的方法中,有一個很特殊: toString()。每一個非基本類型的對象都有一個 toString()方法,而且當編譯器需要一個String 而你卻只有一個對象時,該方法便會被調用。所以在 SprinklerSystem.toString()的表達式:
", wateSource=" + wateSource
- 編譯器將會得到你想要將一個String對象 (", wateSource=") 同 wateSource 相加。由於只能將一個String對象和另一個String對象相加,因此編譯器會告訴你: 我們將調用 toString(),把source轉換成爲一個 String 這樣做之後,它就能將倆個String 連接到一起並將結果傳遞給 System,out.println() 。每當想要使所創建的類具備這樣的行爲時,僅需要編寫一個 toString() 方法。
- 編譯器並不是簡單地爲每一個引用都創建默認對象, 這一點很有意義的, 因爲若真要那樣做的話, 就會在許多情況下增加不必要的負擔。如果想初始化這些引用,如下這樣做:
- 在定義對象的地方。意味着它們總是能夠在構造器被調用之前被初始化。
- 在類的構造器中。
- 就在正在使用這些對象之前, 這種方式被稱爲 惰性初始化。在生成對象不值得及不必每次都生成對象的情況下,這種方式可以減少額外的負擔。
繼承語法
- 繼承是所有 OOP 語言和Java 語言不可缺少的組成部分。 當創建一個類時, 總是在繼承, 因此,除非已明確指出要從其他類中繼承, 否則就是隱式地從Java 的標準根類 Object 進行繼承。
- 組合的語法比較平實, 但是繼承使用的是一種特殊的語法。
- 繼承視爲對類的重用。
- Java中用 super 關鍵字表示超類的意思, 當前類就是從超類繼承來的。
初始化基類
- 對基類子對象的正確初始化也是至關重要的, 而且也僅有一種方法來保證這一點: 在構造器中調用基類構造器來執行初始化,而基類構造器具有執行基類初始化所需要的所有知識和能力。
class Art {
Art(){
System.out.println("Art 構造器");
}
}
class Drawing extends Art{
Drawing(){
System.out.println("Drawing 構造器");
}
}
class Cartoon extends Drawing{
Cartoon(){
System.out.println("Cartoon 構造器");
}
public static void main(String[] args) {
Cartoon cartoon = new Cartoon();
}
}
//運行結果爲
Art 構造器
Drawing 構造器
Cartoon 構造器
- 你可能會發現,構建的過程是 基類 向外 擴散的, 所以基類在導出類構造器可以訪問它之前,就已經完成了初始化。即使你不爲Cartoon()創建構造器,編譯器也會爲你合成一個默認的構造器,該構造器將調用基類的構造器。
帶參數的構造器
- 如果沒有默認的基類構造器,或者想調用一個帶參數的基類構造器,就必須使用 super 關鍵字。
class Game{
Game(int i){
System.out.println("game i= "+i);
}
}
class BoardGame extends Game{
BoardGame(int i) {
super(i);
System.out.println("BoardGame");
}
}
public class Chess extends BoardGame {
Chess() {
super(123);
System.out.println("Chess");
}
public static void main(String[] args) {
Chess chess = new Chess();
}
}
- 如果不在 BoardGame 調用基類構造器,編譯器將 無法找到符合 Game 形式的構造器,會提醒你。
代理
- Java並沒有提供對它的直接支持。這是繼承與組合之間的中庸之道,
- 因爲我們將一個成員對象置於所要構建的類中(就像組合), 但與此我們再新類中暴露了該成員對象的所有方法(就像繼承)如下
public class SpaceShipControls {
void up(int velocity){}
void down(int velocity){}
void left(int velocity){}
void right(int velocity){}
}
//構建太空船的一種方式使用繼承
public class SpaceShip extends SpaceShipControls {
private String name;
SpaceShip(String name){
this.name=name;
}
@Override
public String toString() {
return name;
}
public static void main(String[] args) {
SpaceShip spaceShip = new SpaceShip("zs");
spaceShip.left(100);
}
}
- 然而,SpaceShip 並非真正地 SpaceShipControls 類型, 即便告訴你 SpaceShip 向右運動(right()),更準確地講, SpaceShip包含 SpaceShipControls ,與此同時,SpaceShipControls 的所有方法在 SpaceShip都暴露出來 ,代理解決了此問題。
public class SpaceShipDelegation {
private String name;
private SpaceShipControls spaceShipControls=new SpaceShipControls();
SpaceShipDelegation(String name){
this.name=name;
}
public void up(int velocity){
spaceShipControls.up(velocity);
}
public void left(int velocity){
spaceShipControls.left(velocity);
}
public void right(int velocity){
spaceShipControls.right(velocity);
}
public static void main(String[] args) {
SpaceShipDelegation spaceShipDelegation = new SpaceShipDelegation("zs");
spaceShipDelegation.right(100);
}
}
- 上面的方法是如何傳遞給了底層 spaceShipControls 對象,而其接口由此也就與使用繼承得到的接口相同。
- 我們使用代理時,可以擁有更多控制力,因爲我們可以選擇只提供在成員對象成員中的方法的某個子集。
- 儘管Java語言不直接支持代理,但是很多開發工具卻支持代理。如 IDEA
確保正常清理
- Java中沒有 C++中析構函數的概念。 析構函數是一種在對象被銷燬時可以自動調用的函數。
- 其原因是 在Java中,我們習慣忘掉而不是銷燬對象,並且讓垃圾回收器在必要時釋放其內存。
- try 用一組大括號括起來的範圍 是所謂的保護區,這意味着它需要被特殊處理。
- 其中一項是無論 try 是怎樣退出的, 保護區後的 finally 子句中的代碼總是要被執行的。
- 執行類的所有特定的清理動作,其順序同生成順序相反(通常這就要求基類元素仍舊存活)。
public class Shape {
Shape(int i){
System.out.println("Shape construtor");
}
void dispose(){
System.out.println(
"Shape,dispose"
);
}
}
public class Corcle extends Shape{
Corcle(int i) {
super(i);
System.out.println(
"Corcle constrctor"
);
}
@Override
void dispose() {
System.out.println("Circle dispose");
super.dispose();
}
public static void main(String[] args) {
Corcle corcle = new Corcle(1);
corcle.dispose();
}
}
//運行結果
Shape construtor
Corcle constrctor
Circle dispose
Shape,dispose
名稱屏蔽
- 如果 Java 的基類擁有某個已被多次重載的方法名稱,那麼在導出類中重新定義該方法名稱並不會屏蔽其在基類中的任何版本(這一點與C++不同)。
- 無論是在該層或者它的基類中對方法進行定義,重載機制都可以正常工作:
public class Homer {
char doh(char c){
System.out.println("char doh method");
return 'd';
}
float doh(float f){
System.out.println("doh float method");
return 1.0f;
}
}
class Milhouse{}
class Bart extends Homer{
void doh(Milhouse milhouse){
System.out.println("dob milhouse method");
}
public static void main(String[] args) {
Bart bart = new Bart();
bart.doh(1);
bart.doh('x');
bart.doh(2.2f);
bart.doh(new Milhouse());
}
}
//運行結果
doh float method
char doh method
doh float method
dob milhouse method
- 雖然 Bart 引入了一個新的重載方法,但是在 Bart 中 Homer的所有重載方法都是可用的。
- 使用與基類完全相同的特徵簽名及返回類型來覆蓋具有相同名稱的方法,是一件及其平常的事。
- Java SE5 新增了 @Override 註解 如果你重載而並非覆寫該方法,編譯器會生成一個錯誤。如下
@Override
void doh(Milhouse milhouse){
System.out.println("dob milhouse method");
}
//錯誤信息
method does not override a method from its superclass
//翻譯過來是
方法不會覆蓋其父類中的方法
- @override 註解可以防止你在不想重載時而意外地進行重載。
在組合與繼承之間選擇
- 組合和繼承都允許在新的類中放置子對象, 而繼承是隱式地做。
- 組合技術 通常用於想在 新類中使用現有類的功能那而並非它的接口這種形式。即,在新類中嵌入某個對象,讓其實現所需要的功能, 但新類的用戶看到的只是爲新類所定義的接口,而並非所嵌入對象的接口。
public class Car {
//創建發動機
public Engine engine=new Engine();
//創建輪胎
public Wheel[] wheels =new Wheel[4];
//創建門
public Door left=new Door(),
right=new Door();
Car(){
//初始化輪胎
for (int i = 0; i < 4; i++) {
wheels[i]=new Wheel();
}
}
public static void main(String[] args) {
Car car = new Car();
car.left.window.rolldown();
car.wheels[0].inflate();
}
}
//發動機
class Engine{
public void start(){}
public void stop(){}
}
//窗口
class Window{
//捲起
public void rollup(){}
//滾下來
public void rolldown(){}
}
//門
class Door{
public Window window=new Window();
public void open(){}
public void close(){}
}
//輪胎
class Wheel{
public void inflate(){}
}
- 用一個 交通工具 對象來構成一部車子是毫無意義的,因爲車子並不包含交通工具,它僅是一種交通工具 (is - a關係)。
- is-a 的關係是用集成來表達的,而 has-a 的關係則使用組合來表達的。
protected 關鍵字
- 在實際項目中,經常想要把某些事物儘可能對這個世界隱藏起來,但仍然允許導出類的成員訪問它們。
- 關鍵字 protected 就起這個作用。 但是最好的方式還是將域保持爲 private ,你應當一致保留 更改底層實現 的權利。然後通過 protected 方法來控制類的集成這的訪問權限。
public class Villain {
private String name;
protected void setName(String name){
this.name=name;
}
Villain(String name){
this.name=name;
}
@Override
public String toString() {
return "Villain"+name;
}
}
//第二個測試類
public class Orc extends Villain {
private int orcNumber;
Orc(String name,int orcNumber) {
super(name);
this.orcNumber=orcNumber;
}
public void change(String name,int orcNumber){
setName(name);
this.orcNumber=orcNumber;
}
@Override
public String toString() {
return "Orc{" +
"orcNumber=" + orcNumber +
super.toString()+
'}';
}
public static void main(String[] args) {
Orc orc = new Orc("zs", 13);
System.out.println(orc);
orc.change("hello",66);
System.out.println(orc);
}
}
//運行結果爲
Orc{orcNumber=13Villainzs}
Orc{orcNumber=66Villainhello}
- change方法 可以訪問 setName 方法,這是因爲它是 protected 的。還應注意 Orc 的toString 方法的定義方式,它根據 toString 的基類版本而定義。
向上轉型
- 爲新的類提供方法 並不是繼承技術中最重要的方面,其最重要的方面是用來表現新類和基類之間的關係。 可以用 新類是現有類的一種類型 加以概括。
public class Instrument {
public void play(){}
public static void tune(Instrument instrument){
instrument.play();
}
}
class Wind extends Instrument{
public static void main(String[] args) {
Wind wind = new Wind();
Instrument.tune(wind);
}
}
- 在 tune方法中,程序代碼可以對 Instrument 和 它所有的導出類起作用,這種將 Wind 引用轉換爲 Instrument 引用的動作,我們稱之爲 向上轉型。
- 由導出類 轉型爲 基類,在繼承圖上是向上移動的,因此一般稱爲向上轉型。
- 由於向上轉型是從一個較專用類型向通用類型轉換,所以總是很安全的。也就是說, 導出類是基類的一個超集。它可能比基類含有更多方法,但它必須至少具備基類中所含有的方法。
- 在向上轉型的過程中,類接口中唯一可能發生的事情是丟失方法,而不是獲取他們。
在論組合與繼承
- 在面向對象編程中,生成和使用程序代碼最有可能採用的方法就是直接將數據和方法包裝進一個類中,並使用該類對象。
- 也可以使用組合技術使用現有類來開發新的類, 而繼承技術其實是不太常用的。
- 儘管在教授 OOP 的過程這亞紅我們多次強調繼承,但這並不意味着要儘可能使用它。 相反,應當慎用這一技術,其使用場合僅限於確信使用該技術確實有效的情況。到底是使用組合還是用繼承,一個最清晰的判斷方法就是問一問自己是否需要從新類向基類進行向上轉型。如果必須要向上轉型,則繼承是必要的,如果不需要,則應該好好考慮一下自己是否需要繼承。
final 關鍵字
- 通常指 這是無法改變的。不想做改變可能出於兩個原因:
- 設計
- 效率
- final 數據 :許多編程語言都有某種方法,來向編譯器告知一塊數據是恆定不變的。有時數據恆定不變還是很有用的。例如
- 一個永不改變的編譯時常量。
- 一個在運行時被初始化的值,而你不希望它被改變。
- 在Java中,中類常量必須是基本數據類型,並且以 關鍵字 final表示。在對這個常量進行定義的時候,必須對其進行賦值。
- 一個即是 static 又是 final 的域只佔據一段不能改變的存儲空間。
- 當對象引用而不是基本數據類型運用 final時,其含義有一點令人迷惑。 一旦引用被初始化指向了一個對象,就無法在把它改爲指向另一對象。然而,對象自身確實可以被修改的,Java並未提供使任何對象恆定不變的途徑。
- 空白 final :空白final是指被聲明爲 final但又未給定初值的域。
class Poppet{
private int i;
Poppet(int i){
this.i=i;
}
}
public class BlankFinal {
private final int i=0;
private final int j;
private final Poppet poppet;
BlankFinal(){
j=1;
poppet=new Poppet(1);
}
BlankFinal(int i){
j=i;
poppet=new Poppet(i);
}
public static void main(String[] args) {
new BlankFinal();
new BlankFinal(33);
}
}
- 必須在域定義處或者每個構造器中用表達式對 final進行賦值,這正是final域在使用前總是被初始化的原因所在。
- final 參數
- Java允許在參數列表中以聲明的方式將參數指明爲 final。這就意味着你無法更改參數引用所指向的對象。
class Gizmo{
public void spin(){
}
}
public class FinalArgments {
void with(final Gizmo gizmo){
// !gizmo=new Gizmo(); //Illegal -- g is final
}
void without(Gizmo gizmo){
gizmo=new Gizmo();
gizmo.spin();
}
//void f(final int i){ i++; //can't change }
//you can only read from a final primtive
int g(final int i){
return i+1; //can't change
}
public static void main(String[] args) {
FinalArgments argments = new FinalArgments();
argments.with(null);
argments.without(null);
}
}
- f() 和 g() 展示了當前類型的參數被指明爲 final 時所出現的結果: 你可以讀取參數,但卻無法修改參數。
- final 方法
- 使用 final方法原因有兩個:
- 把方法鎖定,以防任何繼承類修改它的含義。
- 效率。在Java早期實現中,如果將一個方法指明爲 final,就是同意編譯器將針對該方法的所有調用都轉爲內嵌調用。 當編譯器發現 一個 final 方法調用命令時, 它會根據自己的謹慎判斷,跳過插入程序代碼這種正常方式而執行方法調用機制(將參數壓入棧,跳至方法代碼處並執行,然後跳回並清理棧中的參數,處理返回值),並且以方法體重的實際代碼的副本來替代方法的調用。這將消除方法調用的開銷。
- 當然,如一個方法很大,你的程序代碼就會膨脹,因而可能看不到內嵌帶來的任何性能提高,因爲,所帶有的性能提高會因爲話費於方法內的時間量而被縮減。
- 使用Java SE5/6時,應該讓編譯器和JVM去處理效率問題,只有在想要明確禁止覆蓋時,纔將方法設置爲 final的。
final 和 private 關鍵字
- 類中所有的 private 方法都隱式地指定爲是 final的,由於無法取用 private 方法,所以也就無法覆蓋它,可以對 private 方法添加 final 修飾詞, 但這並不能給該方法添加任何額外的意義。
final 類
- 當將某個類的整體定義爲 final 時(通過將關鍵字 final 置於它的定義之前),就表明了你不打算繼承該類,而且也不允許別人這樣做。
- 簡而言之 : 出於某種考慮, 你對該類的設計永遠不需要做任何變動,或者出於安全考慮,你不希望它有子類。
class SmallBrain{}
final class Dinosaur{
int i=7;
int j=1;
SmallBrain smallBrain=new SmallBrain();
void f(){}
}
//class Further extends Dinosaur{}
//error Cannot extend final class Dinosaur
public class Jurassic {
public static void main(String[] args) {
Dinosaur dinosaur = new Dinosaur();
dinosaur.f();
dinosaur.i=40;
dinosaur.j++;
}
}
- final 類禁止繼承,所以 final 類中所有的方法都隱式指定爲 final的,因爲無法覆蓋它們。
- 在 final 類中可以給方法添加 final 修飾詞,但這不會添加任何意義。
- 在設計類時, 將方法指明是 final 的,應該說是明智的。你可能會覺得,沒人想要覆蓋你的方法。有時這是對的。
初始化及類的加載
- 在許多傳統語言中,程序是作爲啓動過程的一部分立刻被加載的。然後是初始化,緊接着程序開始運行。
- 這些語言的初始化過程必須小心控制, 以確保定義爲 static 的東西, 其初始化順序不會造成麻煩。
- Java 採用另一個不同的加載方式。使加載時衆多變得更加容易的動作之一, 因爲Java中的所有事物都是對象。請記住 每個類的編譯代碼都存在於它自己的獨立的文件中。該文件只在需要使用程序代碼時纔會被加載。
- 初次使用之處也是 static 初始化發生之外。所有的 static對象和 static 代碼段都會在加載時依程序中的順序(即, 定義類時的書寫順序)而依次初始化。 當然, 定義爲 static 的東西只會被初始化依次。
繼承與初始化
- 瞭解包括繼承在內的初始化全過程,以對所發生的一切有個全局性的把握,如下
class Insect{
private int i=9;
protected int j;
Insect(){
System.out.println("i= "+i +"---- j= "+j);
j=39;
}
private static int x1=print("static Insect x1 initialized");
static int print(String string){
System.out.println(string);
return 22;
}
}
public class Beetle extends Insect{
private int k=print("Beetle k initialized");
Beetle(){
System.out.println("k= "+k+"----- j= "+j);
}
private static int x2=print("static Beetle x2");
public static void main(String[] args) {
System.out.println("Beetle constructor");
Beetle beetle = new Beetle();
}
}
//運行結果
static Insect x1 initialized
static Beetle x2
Beetle constructor
i= 9---- j= 0
Beetle k initialized
k= 22----- j= 39
- 在Beetle 上運行Java時,所發生的第一件事情就是試圖訪問 Beetle.main()(一個static方法),於是加載器開始啓動並找出 Beetle 類的編譯代碼(在名爲 Beetle.class的文件中)。在對它進行加載的過程中,編譯器注意到它有一個基類(這是由關鍵字 extends 得知的),於是它繼續進行加載。不管你是否打算產生一個該基類的對象,這都是要發生的(請嘗試將對象創建代碼註釋掉,以證明這一點)。
- 如果該基類還有其自身的基類,那麼第二個基類就會被加載,如此類推。
- 基類構造器和導出類的構造器一樣,以相同順序來經歷相同的過程。在基類構造器完成之後,實例變量按其次序被初始化。最後,構造器的其餘部分被執行。
總結
- 繼承和組合都能從現有類型生成新類型。組合一般是將現有類型作爲新類型底層實現的一部分來加以複用,而繼承複用的是接口。
- 在使用繼承時,由於導出類具有基類接口,因此它可以向上轉型至基類,這對多態來講至關重要。
- 儘管面向對象編程對繼承極力強調, 但在開始一個設計時,一般應優先選擇使用組合(或者可能是代理),只在確實必要時才使用繼承。因爲組合更具靈活性。此外,通過對成員類型使用繼承技術的添加技巧, 可以在運行時改變那些成員對象的類型和行爲。因此,可以在運行時改變組合而成的對象的行爲。
- 當你開始設計一個系統時,應該認識到程序開發是一種增量過程,猶如人類的學習一樣,這一點很重要。程序開發依賴於實驗,你可以盡己所能去分析,當你開始執行一個項目時,你任然無法知道所有的答案。如果將項目視作是一種有機的,進化着的生命體去培養,二步是打算像蓋摩天大樓一樣快速見效,就會獲得更多的成功和更迅速的回饋。
- 組合與繼承正是面向對象程序設計中使得你可以執行這種實驗的最基本的倆個工具。