摘要:
本文結合Java的類別的複用對物件導向兩大特徵繼承和多態進行了全面的介紹。首先,我們介紹了繼承的實質和意義,並探討了繼承,組合和代理在類別的複用方面的異同。緊接著,我們根據繼承引入了多態,介紹了它的實現機制和具體應用。此外,為了更好地理解繼承和多態,我們對final關鍵字進行了全面的介紹。在此基礎上,我們介紹了Java中類別的載入及初始化順序。最後,我們對物件導向設計中三個十分重要的概念–重載、覆蓋與隱藏進行了詳細的說明。
要點:
繼承
組合,繼承,代理
多態
final 關鍵字
類別載入及初始化順序
一。繼承# 繼承是所有OOP語言不可缺少的部分,在java中,使用 extends關鍵字來表示繼承關係。 當建立一個類別時,總是在繼承,如果沒有明確指出要繼承的類,就總是隱式地從根類 Object 進行繼承。如果兩個類別存在繼承關係,子類別會自動繼承父類別的方法和變數,在子類別中可以直接呼叫父類別的方法和變數。 需要指出的是,在java中,只允許單繼承,也就是說,一個類別最多只能明確地繼承於一個父類別。但是,一個類別卻可以被多個類別繼承,也就是說,一個類別可以擁有多個子類別。
此外,我們需要特別注意以下幾點:1、
成員變數的繼承
子類別能夠繼承父類別的 public 和protected 成員變數 ,不能夠繼承父類別的 private 成員變量,但可以透過父類別對應的getter/setter方法進行存取
對於父類別的套件存取權成員變數,#如果子類別和父類別在同一個套件下,則子類別能夠繼承,否則,子類別不能夠繼承
對於子類別可以繼承的父類別成員變量,如果在子類別中出現了同名稱的成員變數,則會發生 隱藏現象,即子類的成員變數會屏蔽掉父類別的同名成員變數。如果要在子類別中存取父類別中同名成員變量,則需要使用super關鍵字來進行
引用2、 成員方法
的繼承
子類別能夠繼承父類別的 public和protected成員方法 ,不能夠繼承父類別的private成員方法
對於父類別的套件存取權成員方法,如果子類別和父類別在同一個套件下,則子類別能夠繼承,否則,子類不能夠繼承;
######對於子類別可以繼承的父類別成員方法,如果在子類別中出現了同名稱的成員方法,則稱為 覆寫,也就是子類別的成員方法會覆寫父類別的同名成員方法。如果要在子類別中存取父類別中同名成員方法,則需要使用super關鍵字來進行參考。
程式範例:
class Person { public String gentle = "Father"; }public class Student extends Person { public String gentle = "Son"; public String print(){ return super.gentle; // 在子类中访问父类中同名成员变 } public static void main(String[] args) throws ClassNotFoundException { Student student = new Student(); System.out.println("##### " + student.gentle); Person p = student; System.out.println("***** " + p.gentle); //隐藏:编译时决定,不会发生多态 System.out.println("----- " + student.print()); System.out.println("----- " + p.print()); //Error:Person 中未定义该方法 } }/* Output: ##### Son ***** Father ----- Father *///:~
#隱藏和覆寫是不同的。 隱藏是 針對成員變數和靜態方法的,而 覆寫是 針對普通方法的。
3、 基底類別的初始化與建構器
我們知道,導出類別就像是與基底類別具有相同介面的新類,或許還會有一些額外的方法和域。 但是,繼承不只是複製基底類別的介面。 當建立一個匯出類別物件時,該物件會包含一個基底類別的子物件。 這個子物件與我們用基底類別直接建立的物件是一樣的。二者的差別在於,後者來自於外部,而基底類別的子物件被包裝在導出類別物件的內部。
因此,對基類子物件的正確初始化是至關重要的,並且Java也提供了相應的方法來保證這一點: #導出類別必須在建構器中呼叫基底類別建構器來執行初始化,而基底類別建構器具有執行基底類別初始化所需的所有知識和能力。 當基底類別含有預設建構器時,Java會自動在導出類別的建構器插入對該基底類別預設建構器的調用,因為編譯器不必考慮要傳遞什麼樣的參數的問題。 但是,若父類別不含有預設建構器,或者導出類別想調用一個帶有參數的父類別建構器,那麼在導出類別的建構器中就必須使用super 關鍵字顯式的進行調用對應的基底類別的建構器,且該呼叫語句必定是導出類別建構器的第一個語句。
在Java中,組合、繼承和代理三種技術都可以實現程式碼的複用。
(1) 組合(has-a)
# 透過在新的類別中加入現有類別的對象即可實現組合。 即,新的類別是由現有類別的物件組成。 此技術通常用於想在新類別中使用現有類別的功能而非它的介面這種情況。 也就是說,在新類別中嵌入某個對象,讓其實現所需要的功能,但新類別的使用者看到的只是為新類別所定義的接口,而非所嵌入物件的介面。
(2) 繼承(is-a)
# 繼承可以讓我們依照現有類別的型別來建立新類別。 即,我們採用現有類別的形式並在其中新增程式碼。通常,這意味著我們在使用一個通用類,並為了某種特殊需求而將其特殊化。 本質上,組合和繼承都允許在新的類別中放置子對象,組合是顯式地這樣做,而繼承則是隱式地做。
(3) 代理程式(繼承與組合之間的一種中庸之道:像組合一樣使用已有類別的功能,同時像繼承一樣使用已有類別的介面)
代理程式是繼承與組合之間的一種中庸之道,Java並沒有提供對它的直接支持。在代理程式中,我們將一個成員物件置於所要建構的類別中(就像組合),但同時我們在新類別中暴露了該成員物件的介面/方法(就像繼承)。
程式範例:
// 控制模块public class SpaceShipControls { void up(int velocity) { } void down(int velocity) { } void left(int velocity) { } void right(int velocity) { } void forward(int velocity) { } void back(int velocity) { } void turboBoost() { } }
「太空船需要一個控制模組,那麼,建構太空船的一種方式是使用繼承:
public class SpaceShip extends SpaceShipControls { private String name; public SpaceShip(String name) { this.name = name; } public String toString() { return name; } public static void main(String[] args) { SpaceShip protector = new SpaceShip("NSEA Protector"); protector.forward(100); } }
然而,SpaceShip 并不是真正的 SpaceShipControls 类型,即便你可以“告诉” SpaceShip 向前运动(forward())。更准确的说,SpaceShip 包含 SpaceShipControls ,与此同时, SpaceShipControls 的所有方法在 SpaceShip 中都暴露出来。 代理(SpaceShip 的运动行为由 SpaceShipControls 代理完成) 正好可以解决这种问题:
// SpaceShip 的行为由 SpaceShipControls 代理完成public class SpaceShipDelegation { private String name; private SpaceShipControls controls = new SpaceShipControls(); public SpaceShipDelegation(String name) { this.name = name; } // 代理方法: public void back(int velocity) { controls.back(velocity); } public void down(int velocity) { controls.down(velocity); } public void forward(int velocity) { controls.forward(velocity); } public void left(int velocity) { controls.left(velocity); } public void right(int velocity) { controls.right(velocity); } public void turboBoost() { controls.turboBoost(); } public void up(int velocity) { controls.up(velocity); } public static void main(String[] args) { SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector"); protector.forward(100); } }
实际上,我们使用代理时可以拥有更多的控制力,因为我们可以选择只提供在成员对象中方法的某个子集。
许多编程语言都需要某种方法来向编译器告知一块数据是恒定不变的。有时,数据的恒定不变是很有用的,比如:
一个永不改变的编译时常量;
一个在运行时被初始化的值,而你不希望它被改变。
对于编译期常量这种情况,编译器可以将该常量值带入任何可能用到它的计算式中,也即是说,可以在编译时执行计算式,这减轻了一些运行时负担。在Java中,这类常量必须满足两个条件:
是基本类型,并且用final修饰;
在对这个常量进行定义的时候,必须对其进行赋值。
此外,当用final修饰对象引用时,final使其引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它指向另一个对象。然而,对象本身是可以被修改的,这同样适用于数组,因为它也是对象。
特别需要注意的是,我们不能因为某数据是final的,就认为在编译时就可以知道它的值。例如:
public class Test { final int i4 = rand.nextInt(20); }
1、空白final
Java允许生成 空白final , 即:声明final但又未给定初值的域。但无论什么情况,编译器都会确保空白final在使用前被初始化。但是,空白final在关键字final的使用上提供了更大的灵活性: 一个类中的 final域 就可以做到根据对象而有所不同,却又保持其恒定不变的特性。例如,
必须在域的定义处或者每个构造器中使用表达式对final进行赋值,这正是 final域 在使用前总是被初始化的原因所在。
2、final参数
final参数 主要应用于局部内部类和匿名内部类中,更多详细介绍请移步我的另一篇文章:Java 内部类综述。
3、final方法
final关键字作用域方法时,用于锁定方法,以防任何继承类修改它的含义。这是出于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。
对于成员方法,只有在明确禁止覆盖时,才将方法设为final的。
4、final类
當將某個類別定義為final時,就表示你不打算繼承該類,也不允許別人這樣做。 換句話說,出於某種考慮,你對該類別的設計永不需要做任何變動,或者出於安全考慮,你不希望它有子類。
需要注意的是,final類別的域可以根據實際情況選擇是否為final的。不論是否被定義為final,相同的規則都適用於定義final的域。 然而,由於final類別禁止繼承,所以final類別中的所有方法都隱含指定為final的,因為無法覆寫它們。在final類別中可以給方法添加final修飾,但這不會增添任何意義。
5、 final與private
類別中所有的private方法都隱含地指定為final的。 由於無法取用private方法,所以也無法覆寫它。可以對private方法添加final修飾,但這並不會為該方法添加任何額外的意義。
特別要注意的是,覆寫只有在某方法是基底類別介面的一部分時才會出現。如果一個方法是private的,它就不是基底類別介面中的一部分,而只是一些隱藏於類別中的程式碼。 但若在匯出類別中以相同的名稱產生一個非private方法,此時我們並沒有覆寫該方法,只是產生了一個新的方法。 由於private方法無法觸及並且能有效隱藏,所以除了把它看成是由於它所歸屬的類別的組織結構的原因而存在外,其他任何情況都不需要考慮它。
6、 final 與static
static 修飾變數時,其有預設值,且可改變, 且其只能修飾成員變數和成員方法。
一個 static final域只佔據一段不能改變的儲存空間,且只能在宣告時進行初始化。 因為其是 final 的,因而沒有預設值;且又是static的,因此在類別沒有實例化時,已被賦值,所以只能在宣告時初始化。
我們知道 繼承允許將物件視為它自己本身的類型或其基類型加以處理,從而使同一份程式碼可以毫無差別地運行在這些不同的類型之上。其中,多態方法呼叫允許一種類型表現出與其他相似類型之間的區別,只要這些類型由同一個基類所導出。 所以,多態的作用主要體現在兩個方面:
多態透過分離做什麼和怎麼做,從另一個角度將介面和實作分開來,從而實現將改變的事物與未變的事物分開;
##消除型別之間的耦合關係(類似的,在Java中,泛型也被用來消除類別或方法與所使用的型別之間的耦合關係)。
我們知道方法的覆寫很好的體現了多態,但是當使用一個基類引用去呼叫一個覆寫方法時,到底該呼叫哪個方法才正確呢?
將一個方法呼叫同一個方法主體關聯起來被稱為綁定。若在程式執行前進行綁定,稱為 前期綁定 。但是,顯然,這種機制並不能解決上面的問題,因為在編譯時編譯器並不知道上述基類引用到底指向哪個物件。解決的辦法是後期綁定(動態綁定/運行時綁定):在運行時根據物件的具體類型進行綁定。
事實上,在Java中,除了static方法和final方法(private方法屬於final方法)外,其他所有的方法都是後期綁定。 這樣,一個方法宣告為final後,可以防止其他人覆寫該方法,但更重要一點是:這樣做可以有效地關閉動態綁定,或者說,告訴編譯器不需要對其進行動態綁定,以便為final方法呼叫產生更有效的程式碼。
基於動態綁定機制,我們就可以編寫只與基底類別打交道的程式碼了,而這些程式碼對所有的匯出類別都可以正確運作。 或說,傳送訊息給某個對象,讓該物件去斷定該做什麼事情。
2、向下轉型與運行時類型識別
由於向上轉型會丟失具體的類型信息,所以我們可能會想,透過向下轉型也應該能夠獲取類型資訊。然而,我們知道向上轉型是安全的,因為基底類別不會具有大於導出類別的介面。因此,我們透過基類介面發送的訊息都能被接受,但是對於向下轉型,我們就無法保證了。
要解決這個問題,必須有某種方法來確保向下轉型的正確性,使我們不至於貿然轉型到一種錯誤的類型,進而發出該物件無法接受的訊息。 在Java中,執行時間類型辨識(RTTI)機制可以處理這個問題,它保證Java中所有的轉型都會被檢查。所以,即使我們只是進行一次普通的加括弧形式的類型轉換,再進入運行期時仍會對其進行檢查,以便保證它的確是我們希望的哪種類型。如果不是,我們就會得到一個型別轉換異常:ClassCastException。
3、多態的應用範例
首先,必須指出類別載入及初始化順序為:
父類別靜態程式碼區塊->子類別靜態程式碼區塊->父類別非靜態程式碼區塊->父類別
class SuperClass { private static String STR = "Super Class Static Variable"; static { System.out.println("Super Class Static Block:" + STR); } public SuperClass() { System.out.println("Super Class Constructor Method"); } { System.out.println("Super Class Block"); } }public class ObjectInit extends SuperClass { private static String STR = "Class Static Variable"; static { System.out.println("Class Static Block:" + STR); } public ObjectInit() { System.out.println("Constructor Method"); } { System.out.println("Class Block"); } public static void main(String[] args) { @SuppressWarnings("unused") ObjectInit a = new ObjectInit(); } }/* Output: Super Class Static Block:Super Class Static Variable Class Static Block:Class Static Variable Super Class Block Super Class Constructor Method Class Block Constructor Method *///:~
在运行该程序时,所发生的第一件事就是试图访问 ObjectInit.main() 方法(一个static方法),于是加载器开始启动并加载 ObjectInit类 。在对其加载时,编译器注意到它有一个基类(这由关键字extends得知),于是先进行加载其基类。如果该基类还有其自身的基类,那么先加载这个父父基类,如此类推(本例中是先加载 Object类 ,再加载 SuperClass类 ,最后加载 ObjectInit类 )。接下来,根基类中的 static域 和 static代码块 会被执行,然后是下一个导出类,以此类推这种方式很重要,因为导出类的static初始化可能会依赖于基类成员能否被正确初始化。到此为止,所有的类都已加载完毕,对象就可以创建了。首先,初始化根基类所有的普通成员变量和代码块,然后执行根基类构造器以便创建一个基对象,然后是下一个导出类,依次类推,直到初始化完成。
1、重载与覆盖
(1) 定义与区别
重载:如果在一个类中定义了多个同名的方法,但它们有不同的参数(包含三方面:参数个数、参数类型和参数顺序),则称为方法的重载。其中,不能通过访问权限、返回类型和抛出异常进行重载。
覆盖:子类中定义的某个方法与其父类中某个方法具有相同的方法签名(包含相同的名称和参数列表),则称为方法的覆盖。子类对象使用这个方法时,将调用该方法在子类中的定义,对它而言,父类中该方法的定义被屏蔽了。
总的来说,重载和覆盖是Java多态性的不同表现。前者是一个类中多态性的一种表现,后者是父类与子类之间多态性的一种表现。
(2) 实现机制
重载是一种参数多态机制,即通过方法参数的差异实现多态机制。并且,其属于一种 静态绑定机制,在编译时已经知道具体执行哪个方法。
覆盖是一种动态绑定的多态机制。即,在父类与子类中具有相同签名的方法具有不同的具体实现,至于最终执行哪个方法 根据运行时的实际情况而定。
(3) 总结
我们应该注意以下几点:
final 方法不能被覆盖;
子类不能覆盖父类的private方法,否则,只是在子类中定义了一个与父类重名的全新的方法,而不会有任何覆盖效果。
其他需要注意的地方如下图所示:
2、覆盖与隐藏
(1) 定义
覆盖:指 运行时系统调用当前对象引用 运行时类型 中定义的方法 ,属于 运行期绑定。
隐藏:指运行时系统调用当前对象引用 编译时类型 中定义的方法,即 被声明或者转换为什么类型就调用对应类型中的方法或变量,属于编译期绑定。
(2) 范围
覆蓋:只針對實例方法;
隱藏
:只針對靜態方法與成員變數.
(3) 小結
子類別的實例方法不能隱藏父類別的靜態方法,同樣地,子類別的靜態方法也不能覆寫父類別的實例方法,否則編譯出錯;
以上是Java繼承、多型與類別重複使用的詳細介紹與程式碼實例的詳細內容。更多資訊請關注PHP中文網其他相關文章!