thank in java第七章 多形性(多態)

第七章 多形性(多態)

thank in java第七章 多形性(多態)

面向對象程序設計語言三大基本特徵:封裝、繼承、多態。

“多形性“(Polymorphism)從另一個角度將接口從具體的實施細節中分離出來,即實現了”是什麼“與”怎樣做“兩個模塊的分離。利用多形性的概念,代碼的組織以及可讀性君能獲得改善。此外,還能創建”易於擴展“的程序。

7.1 上溯造型

在前面章節,已經瞭解到可將一個對象作為它自己的類型使用,或者作為它的基礎類的一個對象使用。取得一個對象句柄,並將其作為基礎類句柄使用的行為叫作“上溯造型”——因為繼承樹的畫法是基礎類位於最上方。但這樣做會遇到如下問題,示例:

package package07;
class Note {
private int value;
private Note(int val){
value = val;
}
public static final Note
middleC = new Note(0),
cSharp = new Note(1),
cFlat = new Note(2);
}
package package07;
class Instrument {
public void play(Note n){
System.out.println("Instrument.play()");
}
}
package package07;

class Wind extends Instrument{
public void play(Note n){
System.out.println("Wind.play()");
}
}
package package07;
public class Music {
public static void tune(Instrument i){
i.play(Note.middleC);
}
public static void main(String[] args) {
Wind fluteWind = new Wind();
tune(fluteWind);
}
}
輸出:Wind.play()

7.2 深入理解

接受繼承基礎類的句柄,編譯器如何才能知道基礎類句柄指向的是什麼呢?編譯器無從得知,為了深入瞭解這個問題,引申出“綁定”這個主題。

7.2.1 方法調用的綁定

將一個方法調用同一個方法主題連接到一起就稱為“綁定(Bingding)“。若在程序執行前執行綁定,就稱為”早期綁定“。但是這個術語或者從未聽見,因為它在任何程序化語言裡都是不可能的。

解決的辦法就是“後期綁定”,它意味著綁定在運行期間進行,以對象的類型為基礎。後期綁定也叫作“動態綁定”或“運行期綁定”。若一種語言實現了後期綁定,同時必須提供一些機智,可在運行期間判斷對象的類型,並分別調用適當的方法。也就是說,編譯器此時依然不知道對象的類型,但方法調用機制能自己去調查,找到正確的方法主體。不同語言對後期綁定的實現方法是有所區別的。但我們至少可以這樣認為:它們都要在對象中安插某些特殊類型的信息。

Java中綁定的所有方法都是採取後期綁定技術,除非一個方法已被聲明成final。這意味著我們通常不必決定是否應進行後期綁定——它是自動發生的。

關於final聲明:由之前我們瞭解到,final聲明後,能夠防止其他人覆蓋 此方法。但也許更重要的一點,它可有效地“關閉”動態綁定,或者告訴編譯器不需要動態綁定。從而編譯器就可以為final方法調用生成更高的代碼。

7.2.2 產生正確的行為

知道java裡綁定的所有方法都是通過後期綁定具有多形性以後,就可以相應地編寫自己的代碼,令其與基礎類溝通。此時,所有的衍生類都保證能用相同的代碼正確地工作。或者換用另一種方法,我們可以“將一條消息發送給一個對象,讓對象自行判斷要做什麼事情。”

比如:shape(形狀)是一個基礎類,另外還有一些衍生類:Circle(圓形),Square(方形),Triangle(三角形)等等。其中衍生類繼承基礎類。上溯造型可用如下表示出來:

shape s = new Circle();

在這裡,我們創建了Circle對象,並將結果句柄立即賦給一個Shape。按照繼承關係這是Circle屬於Shape的一種,因此編譯器認可上述語句。


package package07;
class Shape {
void draw(){}
void erase(){}
}
package package07;
class Cricle extends Shape{
void draw(){
System.out.println("Circle.draw()");
}
void erase(){
System.out.println("Circle.erase()");
}
}
package package07;
class Square extends Shape{
void draw(){
System.out.println("Square.draw()");
}
void erase(){
System.out.println("Square.erase()");
}
}
package package07;
class Triangle extends Shape{
void draw(){
System.out.println("Triangle.draw()");
}
void erase(){
System.out.println("Triangle.erase()");
}
}
package package07;
import javafx.scene.shape.Circle;
public class Shapes {
public static Shape randShape(){
switch ((int)(Math.random()*3)) {
default:
case 0:
return new Cricle();
case 1:
return new Square();
case 2:
return new Triangle();
}
}
public static void main(String[] args) {
Shape[] s = new Shape[9];

for(int i = 0;i s[i] = randShape();
for(int i = 0;i s[i].draw();
}
}
輸出:Square.draw()
Triangle.draw()
Square.draw()
Square.draw()
Circle.draw()
Square.draw()
Square.draw()
Square.draw()
Square.draw()

針對從Shape衍生出來的所有東西,shape建立了一個通用接口——也就是說,所有形狀都可以描繪和刪除。衍生類覆蓋了這些定義,為每種特殊類型的集合形狀都提供了獨一無二的行為。

7.3 覆蓋與過載

回過頭來看本章的第一個例子。在下面這個程序中,,方法play()的接口會在被覆蓋的過程中發生變化。這意味著我們實際並沒有“覆蓋”方法,而是使其“過載”。編譯器允許我們對方法進行過載處理,使其不報告出錯。但這種行為可能並不是我們希望的。下面是這個例子:


package package07;
class NoteX {
public static final int
MIDDLE_C = 0,C_SHAPE = 1,C_FLAT = 2;
}

package package07;
class InstrumentX {
public void play(int NoteX){
System.out.println("InstrumentX.play()");
}
}
package package07;
class WindX extends InstrumentX{
public void play(NoteX n){
System.out.println("WindX.play(NoteX n)");
}
}
package package07;
public class WindError {
public static void tune(InstrumentX i){
i.play(NoteX.MIDDLE_C);
}
public static void main(String[] args) {
WindX flute = new WindX();
tune(flute);
}
}
輸出:InstrumentX.play()

“過載”是指同一樣東西在不同的地方具有多種含義;而“覆蓋”是指它隨時隨地都只有一種含義,只是原有的含義完全被後來的含義取代了。

7.4 抽象類和方法

在我們所有樂器(instrument)例子中,基礎類Instrument內的方法都是肯定是“偽”方法。若去調用這些方法,就會出現錯誤。這是由於Instrument的意圖是為從它衍生出去的所有類都創建一個通用接口。之所以創建這樣一個接口,唯一的原因就是它能為不同的子類型作出不同的表示。它為我們建立了一種基本形式,使我們能定義在所有衍生類裡”通用“的一些東西。為闡述這個觀念,另一種方法是把Instrument稱為”抽象基礎類“(簡稱抽象類)。因為Instrument的作用僅僅是用來表達接口,而不是表達一些具體的實施細節,所以創建一個Instrument對象是沒有任何意義的,而且我們通常都應禁止用戶那樣做。為達到這一目的,可令instrument內所有方法都顯示出錯消息。但這樣做會延遲信息到達期,並要求用戶那一面進行徹底、可靠的測試。無論如何,最好的方法都是在編譯期間捕捉到問題。

針對這個問題,java專門提供了一種機制,名為“抽象方法”。它屬於一種不完整的方法,只含有一個聲明,沒有主體。語法如下:

abstract void();

包含一個抽象方法的類,被稱為“抽象類”。如果一個類包含了一個或多個抽象方法,類必須指定成abstract(抽象)。否則,編譯器會報錯。當然即使不包括任何abstract方法,亦可將一個類聲明成“抽象類”。如果一個類沒必要擁有任何抽象方法,而且我們想禁止那個類的所有實例,這種能力就會顯得非常有用。

instrument類可很輕鬆地轉換成一個抽象類。只有其中一部分方法會變成抽象方法,因為使一個類抽象以後,並不會強迫我們將它所有方法都同時變成抽象。比如:


package package07;
class Wind4 extends Instrument4{
public void play(){
System.out.println("Wind4.play()");
}
public String what(){
return "Wind4";
}
public void adjust(){}
}
package package07;
class Percussion4 extends Instrument4{
public void play(){
System.out.println("Percussion4.play()");
}

public String what(){
return "Percussion4";
}
public void adjust(){}
}
package package07;
class Stringed4 extends Instrument4{
public void play(){
System.out.println("Stringed4,play()");
}
public String what(){
return "Stringed";
}
public void adjust(){}
}
package package07;
class Brass4 extends Wind4{
public void play(){
System.out.println("Brass4.play()");
}
public void adjust(){
System.out.println("Brass4.adjust()");
}
}
package package07;
public class Music4 {
static void tune(Instrument4 i){
i.play();
}
static void tuneAll(Instrument4[] e){
for (int i = 0; i < e.length; i++) {
tune(e[i]);
}
}
public static void main(String[] args) {
Instrument4[] orchestraInstrument4s = new Instrument4[5];
int i = 0;
orchestraInstrument4s[i++] = new Wind4();
orchestraInstrument4s[i++] = new Percussion4();
orchestraInstrument4s[i++] = new Stringed4();
orchestraInstrument4s[i++] = new Brass4();
orchestraInstrument4s[i++] = new Woodwind4();
tuneAll(orchestraInstrument4s);
}
}
輸出:Wind4.play()
Percussion4.play()
Stringed4,play()
Brass4.play()
Woodwind4.play()

創建抽象類和方法有時非常有用,因為它們使得一類的抽象變成明顯的事實,可明確告訴用戶和編譯器自己打算如何。

7.5 接口

“interface”(接口)關鍵字使抽象的概念更深入了一層。我們可將其想象為一個“純”抽象類。它允許創建者規定一個類的基本形式:方法名、自變量列表以及返回類型,但不規定方法主題。接口也包含了基本數據類型的數據成員,但他們都默認為static和final。接口只是提供一種形式,並不提供實施的細節。接口這樣描述自己:“對於實現我們的所有類,看起來都應該像是我現在的樣子”。因此,採用了一個特定節後的所有代碼都知道對於那個接口可能會調用什麼方法。這便是接口的全部含義。所以,幾口常用於建立類於類之間的一個“協議“。有些面向對象的程序設計語言採用一個名為“protocol”(協議)的關鍵字,它做的便是與接口相同的事情。

接口關鍵字是interface, 與類像是可以與public等屬性關鍵字聯合使用。

為了生成與一個特定的接口(或一組接口)相符的類,要使用implements(實現)關鍵字。 例子如下:


package package07;
import java.util.*;
interface Instrument5 {
int i = 5;
void play();
String what();
void adjust();
}
package package07;
class Wind5 implements Instrument5{
public void play(){
System.out.println("Wind.play()");
}
public String what(){
return "Wind5";
}
public void adjust() {
}
}
package package07;
class Percussion5 implements Instrument5{
public void play() {
System.out.println("Percussion5.play()");
}
public String what() {
return "Percussion5";
}
public void adjust() {
}
}
package package07;
class Stringed5 implements Instrument5{
public void play() {
System.out.println("Stringed.play()");
}
public String what() {
return "Stringed";
}
public void adjust() {
}
}
package package07;
class Brass5 extends Wind5{
public void play(){
System.out.println("Brass5.play()");
}
public void adjust(){
System.out.println("Brass5.adjust");
}

}
package package07;
class Woodwind5 extends Wind5{
public void play(){
System.out.println("Woodwind5.play()");
}
public String what(){
return "Woodwind";
}
}
package package07;
public class Music5 {
static void tune(Instrument5 i){
i.play();
}
static void tuneAll(Instrument5[] e){
for (int i = 0; i < e.length; i++) {
tune(e[i]);
}
}
public static void main(String[] args) {
Instrument5[] orchestra = new Instrument5[5];
int i = 0;
orchestra[i++] = new Wind5();
orchestra[i++] = new Percussion5();
orchestra[i++] = new Stringed5();
orchestra[i++] = new Brass5();
orchestra[i++] = new Woodwind5();
tuneAll(orchestra);
}
}
輸出:Wind.play()
Percussion5.play()
Stringed.play()
Brass5.play()
Woodwind5.play()

7.5.1 Java的“多重繼承”

接口只是比抽象類“更純”的一種形式。接口根本沒有具體的實施細節——也就是說,沒有與存儲空間與“接口”關聯在一起——所以沒有任何辦法可以防止多個接口合併到一起。因為我們經常都需要表達這樣一個意思:“x從屬於a,也從屬於b,也從屬於c”。在C++中,將各個類合併到一起的行動,稱作”多重繼承“,而且操作較為不方便,因為每個類都可能有一套自己的實施細節。在Java中,我們可以採用同樣的行動,但只有其中一個類擁有具體的實施細節。所以在合併多個接口的時候,C++的問題不會再Java中重演。

在一個衍生類中,我們並不一定要擁有一個抽象或具體(沒有抽象方法)的基礎類。如果確實想從一個費接口繼承,那麼只能從一個繼承。剩餘的所有基本元素都必須是“接口”。我們將所有接口名置於implement 關鍵字後面,並用逗號分隔它們。可根據需要使用多個接口,而且每個接口都會稱為一個獨立的類型,可對其進行上溯造型。下面這個例子展示了一個“具體”類同幾個接口合併的情況,它最終生成一個新類:

7.5.2 通過集成擴展接口

利用繼承技術,可方便地為一個接口添加新的方法聲明,也可以將幾個接口合併成一個新接口,在這兩種情況下最終得到的都是一個新街口,如下所示:


package package07;
interface Monster {
void menace();
}
package package07;
interface DangerousMonster extends Monster{
void destroy();
}
package package07;
interface Lethal {
void kill();
}
package package07;
class DragonZilla implements DangerousMonster{
public void menace(){}
public void destroy() {}
}

package package07;
interface Vampire extends DangerousMonster,Lethal{
void drinkBlood();
}
package package07;
class HorrorShow {
static void u(Monster b){b.menace();}
static void v(DangerousMonster d){
d.menace();
d.destroy();
}
public static void main(String[] args) {
DragonZilla if2 = new DragonZilla();
u(if2);
v(if2);
}
}

DangeroursMonster 是對Monterey的一個簡單擴展,最終生成一個新的接口。這是在DragonZilla裡實現的。

Vampire的語法僅在繼承接口是才可使用。通常,我們只能對單獨一個類應用extends(擴展)關鍵字。但由於接口可能由多個其他接口構成,所以在構建一個新接口時,extends可能應用多個基礎接口。正如大家看到的那樣,接口的名字就只是簡單的實用逗號分隔。

7.5.3 常數分組

由於置入一個接口的所有字段都自動具有static和final屬性,所以接口是對常數值進行分組的一個好工具,它具有與C或C++的enmu非常相似的效果。如下例所示:


package package07;
public interface Months {
int

JANUARY = 1,FEBRUARY=2,MARCH=3,APPIL = 4,
MAY = 5,JUNE =6,JULY = 7,AUGUST = 8,
SEPTEMBER = 9,OCTOBER = 10,MOVEMBER=11,DECEMBER=12;
}

7.6 內部類

在Java1.1中,可將一個類定義置入另一個類定義中。這就就叫作“內部類”。內部類對我們非常有用,因為利用它可對那些邏輯上相互聯繫的類進行分組,並可控制一個在另一個類裡的“可見性”。然而,我們必須認識到內部類與以前講述的“合成”方法存在著根本區別。

通常,對內部類的需要並不是特別明顯得出,至少不會立即感覺到自己需要使用內部類。創建內部類:將類定義置入一個用於封裝它的類內部。


package package07;
public class Parcell {
class Contents{
private int i = 11;
public int value(){
return i;
}
}
class Destination{
private String label;
Destination(String whereTo){
label = whereTo;
}
String readLabel(){
return label;
}
}
public void ship(String dest){
Contents c = new Contents();
Destination d = new Destination(dest);

}
public static void main(String[] args) {
Parcell p = new Parcell();
p.ship("Tanzania");
}
}

若在ship()內部使用,,內部類的使用看起來和其他任何雷度沒有什麼區別。在這裡唯一明顯的區別就是它的名字嵌套在Parcell裡面。但是這並不是唯一的區別。

7.6.1 內部類和上溯造型

迄今為止,內部類看起來仍然沒什麼特別的地方。畢竟,用它實現隱藏顯得有些大題小做。Java已經有一個非常優秀的隱藏機制——只允許類成為“友好的”(只在一個包內可見),而不是把它創建成一個內部類。然而,當我們準備上溯造型到一個基礎類(特別是到一個接口)的時候,內部類就開始發揮其關鍵作用(從用於實現的對象生成一個接口句柄具有與上溯造型至一個基礎類相同的效果)。這是由於內部類隨後完全進入不可見或不可用狀態——對任何人都將如此。所以我們可以非常方便隱藏實施細節。我們得到的全部回報就是一個基礎類或者接口的句柄,而且甚至有可能不知道準確的類型,就如下面所示:


package package07;
abstract class Contents {

abstract public int value();
}
package package07;
interface Destination {
String readLabel();
}
package package07;
public class Parcel3 {
private class PContents extends Contents{
private int i = 11;
public int value(){
return i;
}
}
protected class PDestination implements Destination{
private String Label;
private PDestination(String whereTo){
Label = whereTo;
}
public String readLabel(){
return Label;
}
}
public Contents cont(){
return new PContents();
}
}
package package07;
class Test {
public static void main(String[] args){
Parcel3 p = new Parcel3();
Contents c = p.cont();
Destination d = p.dest("Tanzania");
}
}

7.6.2方法和作用域中的內部類

至此,我們已基本理解了內部類的典型用途。那些涉及內部類的代碼,通常表達的都是“單純”的內部類,非常簡單,且極易理解。然而,內部類的設計非常全面,不可避免地會遇到他們的其他大量用法——假若我們在一個方法甚至一個任意的作用域內創建內部類。有兩方面的原因促使我們這樣做:

(1)正如前面展示的那樣,我們準備實現某種形式的接口,使自己能創建和返回一個句柄。

(2)要解決一個複雜的問題,,並希望創建一個類,用來輔助自己的程序方案。同時不願意把它公開。

7.6.3 鏈接到外部類

我們見到的內部類好像僅僅是一種名字隱藏以及代碼組織方案。儘管這些功能非常有用,但似乎並不特別引人注目。然而,我們還忽略了另一個重要的事實。創建自己的內部類時,那個類的對象同時擁有指向封裝對象(這些對象封裝或生成了內部類)的一個連接。所以他們能訪問那個封裝對象的成員——無需取得任何資格。除此之外,內部類擁有對封裝類所有的訪問權限。示例:


package package07;
interface Selestor {
boolean end();
Object current();
void next();
}
package package07;
public class Sequence {
private Object[] o;
private int next = 0;
public Sequence(int size){
o = new Object[size];
}
public void add(Object x){
if(next < o.length){

o[next] = x;
next++;
}
}
private class SSelector implements Selestor{
int i = 0;
public boolean end(){
return i = o.length;
}
public Object current(){
return o[i];
}
public void next(){
if(i < o.length)i++;
}
}
public Selector getSelector(){
return new SSelector();
}
public static void main(String[] args){
Sequence s = new Sequence(10);
for (int i = 0; i < 10; i++)
s.add(Integer.toString(i));
Selestor s1 = s.getSelector();
while (!s1.end()) {
System.out.println((String)s1.current());
s1.next();
}
}
}

一個內部類可以訪問封裝類的成員,這是如何實現的呢?內部類必須擁有對封裝類的特定對象的一個引用,而封裝類的作用就是創建這個內部類。隨後,當我們引用封裝類的一個成員時,就利用那個(隱藏)的引用來選擇那個成員。幸運的是,編譯器會幫助我們照管所有的這些細節。但我們現在也可以理解內部類的一個對象只能與封裝類的一個對象聯合創建。在這個創建過程中,要求封裝對象的句柄進行初始化。若不能訪問那個句柄,編譯器就會報錯。

總結

a. 面向對象的三大特性:封裝、繼承、多態。從一定角度來看,封裝和繼承幾乎都是為多態而準備的。這是我們最後一個概念,也是最重要的知識點。

b. 多態的定義:指允許不同類的對象對同一消息做出響應。即同一消息可以根據發送對象的不同而採用多種不同的行為方式。(發送消息就是函數調用)

c. 實現多態的技術稱為:動態綁定(dynamic binding),是指在執行期間判斷所引用對象的實際類型,根據其實際的類型調用其相應的方法。

d. 多態的作用:消除類型之間的耦合關係。

多態存在的三個必要條件一、要有繼承;二、要有重寫;三、父類引用指向子類對象。

多態的好處

1.可替換性(substitutability)。多態對已存在代碼具有可替換性。例如,多態對圓Circle類工作,對其他任何圓形幾何體,如圓環,也同樣工作。2.可擴充性(extensibility)。多態對代碼具有可擴充性。增加新的子類不影響已存在類的多態性、繼承性,以及其他特性的運行和操作。實際上新加子類更容易獲得多態功能。例如,在實現了圓錐、半圓錐以及半球體的多態基礎上,很容易增添球體類的多態性。3.接口性(interface-ability)。多態是超類通過方法簽名,向子類提供了一個共同接口,由子類來完善或者覆蓋它而實現的。如圖8.3 所示。圖中超類Shape規定了兩個實現多態的接口方法,computeArea()以及computeVolume()。子類,如Circle和Sphere為了實現多態,完善或者覆蓋這兩個接口方法。4.靈活性(flexibility)。它在應用中體現了靈活多樣的操作,提高了使用效率。5.簡化性(simplicity)。多態簡化對應用軟件的代碼編寫和修改過程,尤其在處理大量對象的運算和操作時,這個特點尤為突出和重要。

thank in java第七章 多形性(多態)


分享到:


相關文章: