天天看點

第八章:多态第八章 多态

第八章 多态

  • 多種類型(從同一基類導出的)視為同一類型來處理
  • 同一份代碼也就可以毫無差别地運作在這些不同類型之上了

8.1 再論向上轉型

  • 對象可以作為自己本身使用,也可以作為它的基類型使用
  • 把對某個對象的引用視為對基類型的引用的做法被稱作向上轉型
public class Car {
    public void move() {
        System.out.println("嘟嘟嘟~隻要是個車就能跑的嘟嘟嘟");
    }
}

public class Jeep extends Car{
    @Override
    public void move() {
        System.out.println("嘟嘟嘟~直接上山了");
    }
}

public class Person {
    public void drive(Car car) {
        car.move();
    }

    public static void main(String[] args) {
        // Jeep繼承自Car,不需要任何轉換,可以運作
        Jeep jeep = new Jeep();
        drive(jeep);
    }
}

           
8.1.1 忘記對象類型
  • 如果讓drive(Car car)方法接受Jeep的引用,看起來更直覺
  • 但是如果我新添加了若幹種車,比如:寶馬、奔馳、奧迪、比亞迪等等
  • 那麼還得再為它們寫對應的drive方法,那麼就需要更多的程式設計,做大量的工作
練習1、建立一個Cycle類,它具有子類Unicycle、Bicycle、Tricycle,示範它們都可以經由iride()方法向上轉型為Cycle
public class Cycle { } 
 
public class Unicycle extends Cycle { } 
 
public class Bicycle extends Cycle { } 
 
public class Tricycle extends Cycle { } 

public class E01_Upcasting {   
    public static void ride(Cycle c) {}
   
    public static void main(String[] args) {     
        ride(new Cycle());    // No upcasting     
        ride(new Unicycle()); // Upcast     
        ride(new Bicycle());  // Upcast     
        ride(new Tricycle()); // Upcast   
    } 
}
 
           

8.2 轉機

  • 編譯器如何知道這個Car引用指向的就是Jeep對象,而不是奧迪、寶馬等對象呢,其實編譯器無法得知
8.2.1 方法調用綁定
  • 将一個方法調用同一個方法主體關聯起來被稱作綁定
  • 程式執行前進行綁定,叫做前期綁定
  • 程式運作時進行綁定,叫做後期綁定,在對象中安置了某種資訊
  • Java中除了static和final方法(包括private)以外所有方法都是後期綁定
  • 将某個方法聲明為final,可以防止其他人覆寫該方法,“關閉”動态綁定,生成更有效的代碼
  • 大多數情況下并不會對程式的性能有什麼提升,是以最好是根據設計而不是性能來使用final
8.2.2 産生正确的行為
// 向上轉型可以這麼簡單
Car car = new Jeep();

// 如果調用一個基類的方法,可能認為是調用的父類對象,實際上是正确的調用了Jeep.move();
car.move();
           

Car基類為所有的導出類都建立了一個公共接口,所有車都可以移動,導出類通過覆寫這些定義,為每種不同的車型提供單獨的move行為

練習2、在幾何圖形的示例中添加@Override注解

練習3、在基類中添加一個新方法,導出類不覆寫,其中一個覆寫,最後都覆寫,看看發生了什麼

// 車的父類
public class Car {
	public void move() {
		System.out.println("是個車子就能動彈");
	}
}

// 人
public class Person {
	public static void main(String[] args) {
		Audi audi = new Audi();
		Jeep jeep = new Jeep();
		audi.move();
		jeep.move();
	}
}
————————————————————————————————————————————————————————————
// 奧迪不覆寫move方法
public class Audi extends Car {
//	@Override
//	public void move() {
//		System.out.println("小奧迪,嗖嗖快");
//	}
}

// Jeep不覆寫move方法
public class Jeep extends Car {
//	@Override
//	public void move() {
//		System.out.println("大Jeep,直接跑上山");
//	}
}

// 運作結果
是個車子就能動彈
是個車子就能動彈
——————————————————————————————————————————————————————————————
// 奧迪覆寫move方法
public class Audi extends Car {
	@Override
	public void move() {
		System.out.println("小奧迪,嗖嗖快");
	}
}

// Jeep中依舊不覆寫
public class Jeep extends Car {
//	@Override
//	public void move() {
//		System.out.println("大Jeep,直接跑上山");
//	}
}

// 運作結果
小奧迪,嗖嗖快
是個車子就能動彈
———————————————————————————————————————————————————————————————
// 奧迪覆寫move方法
public class Audi extends Car {
	@Override
	public void move() {
		System.out.println("小奧迪,嗖嗖快");
	}
}

// Jeep覆寫move方法
public class Jeep extends Car {
	@Override
	public void move() {
		System.out.println("大Jeep,直接跑上山");
	}
}

// 運作結果
小奧迪,嗖嗖快
大Jeep,直接跑上山
———————————————————————————————————————————————————————————————
結論:
如果不覆寫也可以調,調的是基類的方法
如果覆寫了,調的就是覆寫後的方法
           

練習4、向Shape.java中添加一個新的Shape類型,并在main()方法中驗證,多态對于新類型的作用是否和舊類型中的一樣

練習5、以練習1為基礎,在Cycle中添加wheels()方法,傳回輪子的數量,修改ride()方法,調用wheels()方法,證明多态起作用了

public class Cycle {
    public int wheels() {
        return 0;
    }
} 
 
public class Unicycle extends Cycle {
    public int wheels() {
        return 1;
    }
} 
 
public class Bicycle extends Cycle { 
    public int wheels() {
        return 2;
    }
} 
 
public class Tricycle extends Cycle {
    public int wheels() {
        return 3;
    } 
} 

public class E01_Upcasting {   
    public static void ride(Cycle c) {
        System.out.println("車輪子數為:" + c.wheels());
    }
   
    public static void main(String[] args) {     
        ride(new Cycle());    // No upcasting     
        ride(new Unicycle()); // Upcast     
        ride(new Bicycle());  // Upcast     
        ride(new Tricycle()); // Upcast   
    } 
}
// 運作結果
車輪子數為:0
車輪子數為:1
車輪子數為:2
車輪子數為:3
 
           
8.2.3 可擴充性
  • 如果上面的Car的例子中添加一個drift()漂移的方法,我們添加新的方法并不會影響drive()方法去調用move()方法
  • 我們所做的代碼修改,不會對程式中其他不應受到影響的部分産生破壞
  • 多态讓程式員将“改變的事物與未變的事物分離開來”

練習6、修改Music3.java,使what()方法成為根Object的toString()方法,并列印出Instrument對象

證明了每一個對象調用了它們自己相應的toString()方法

練習7、向Music3.java添加一個新的類型Instrument,并驗證多态性是否作用于所添加的新類型

毫無疑問,肯定作用于新類型

練習8、修改Music3.java,使其可以像Shape.Java中的方法那樣随機建立Instrument對象

練習9、建立Rodent:老鼠,鼹鼠,大頰鼠等等這樣一個繼承結構

略了,和上文提到的Car的例子一樣

練習10、建立一個包含兩個方法的基類,第一個方法中可以調用第二個方法,然後産生一個繼承自該基類的導出類,且覆寫基類中的第二個方法,為該導出類建立一個對象,将它向上轉型到基類并調用第一個方法,解釋發生的情況

public class Car {
	public void move() {
		System.out.println("是個車子就能動彈");
		brokeDown();
	}
	public void brokeDown() {
		System.out.println("車抛錨了,尼瑪币車胎炸了!!!");
	}
}

public class Jeep extends Car {
	@Override
	public void brokeDown() {
		System.out.println("大Jeep上山被大石頭給幹廢了!");
	}
}

public class Person {

	public static void main(String[] args) {	
		Jeep jeep = new Jeep();
		jeep.move();
	}

}

// 運作結果
是個車子就能動彈
大Jeep上山被大石頭給幹廢了!

// 這個例子我要專門寫一下,因為我當初并不了解是如何調用的

對于
Car car = new Jeep();
car.move();
我可以了解,一定是調用的Jeep.move()方法

而對于現在這個例子,一開始我是捉摸不透的,以為move()中調用的還是基類的brokeDown()方法,
但是明顯結果并不是,結果是調用的子類的方法
———————————————————————————————————————————————————————————————————
當時是這麼一個場景,BaseListFragment類中的上拉加載中調用了appendData()方法,
但是基類中的appendData()并不能滿足需求,需要被重寫,我當時并不了解這些基礎,
有一個疑問:父類的這個上拉加載會不會調用我重寫的appendData()方法呢?是以我陷入了困境,
當時的想法竟然是不行那我就在實作類中再寫一個上拉加載的監聽,然後調用我重寫的appendData(),
那麼這兩個方法都是在同一個類中的方法就肯定可以調用了,
那麼當時的想法真是幼稚,簡直是沒事給自己多加負擔,Java還需要你寫這麼複雜嗎?

Java總是使用派生最多的方法作為對象類型
說白了就是父類中各種調用方法,會使用你目前對象所能感覺到的最新的覆寫過的方法

           
8.2.4 缺陷:“覆寫”私有方法
  • 基類中的private方法無法被覆寫,在子類中,對于基類中的private方法,最好采用不同的名字
public class Car {
	 private void turnOnTheLight() {
		System.out.println("把燈開啦");
	}
}

public class Audi extends Car {
	public void turnOnTheLight() {
		System.out.println("小奧迪打開了個好看的大燈");
	}
}
————————————————————————————————————————————————————————————————————
// 聲明為基類的引用
public class Person {
	public static void main(String[] args) {	
            Car car = new Audi();
        // eclipse直接報錯,不能這麼寫
	    car.turnOnTheLight();  
	}
}
// 運作結果
按書中說的結果應該是去執行基類的方法,輸出:把燈開啦
但是我用eclipse編譯直接報錯,無法運作,提示你必須更改private通路權限
———————————————————————————————————————————————————————————————————
聲明為子類的引用
public class Person {
	public static void main(String[] args) {	
		Audi car = new Audi();
	        car.turnOnTheLight();  
	}
}
// 運作結果
執行了子類的方法:
小奧迪打開了個好看的大燈
           
8.2.5 缺陷:域或靜态方法
  • 域不是多态的,和方法是不一樣的,舉個例子
public class Car {
	public int price;
	
	public int getPrice() {
		return price;
	}
}

public class Audi extends Car {
	public int price = 300;
	
	public int getPrice() {
		return price;
	}
}

public class Person {

	public static void main(String[] args) {	
		Car car = new Audi();
		System.out.println("car.price=" + car.price);
		System.out.println("car.getPrice=" + car.getPrice());
		
		Audi audi = new Audi();
		System.out.println("audi.price=" + audi.price);
		System.out.println("audi.getPrice=" + audi.getPrice());
	}

}

// 運作結果
car.price=0
car.getPrice=300
audi.price=300
audi.getPrice=300

// 如果是聲明父類的引用,會調用父類的變量,由此可見域是沒有多态的,方法有多态
// 如果是直接聲明子類的引用,那麼跟父類就沒什麼關系了

// 這裡還有一個要注意的就是,聲明父類的引用,就不能直接調用子類新添加的方法了,如果實在想調,就強制轉型
           
  • 靜态方法也不具有多态性
  • 靜态屬性,靜态方法,和非靜态屬性都不具有多态性
  • 簡單說,域和靜态方法都不具有多态性
public class Car {
	
	public static void fly() {
		System.out.println("車子能不能飛,得看你是啥車");
	}
	public void move() {
		System.out.println("是個車子就能動彈");
	}

}

public class Audi extends Car {
	public static void fly() {
		System.out.println("我這個奧迪車,好像夠嗆能飛");
	}
	
	@Override
	public void move() {
		System.out.println("小奧迪跑的嗖嗖的");
	}
	
}

public class Person {

	public static void main(String[] args) {	
		Car car = new Audi();
		car.fly();
		car.move();
		
		Audi audi = new Audi();
		audi.fly();
		car.move();
	}

}
// 運作結果
車子能不能飛,得看你是啥車
小奧迪跑的嗖嗖的
我這個奧迪車,好像夠嗆能飛
小奧迪跑的嗖嗖的

// 可以看出來靜态方法沒有多态,靜态方法是與類,而非與單個對象綁定的

           

8.3 構造器和多态

8.3.1 構造器調用順序
  • 基類的構造器總是在導出類的構造過程中被調用,按照繼承層次逐漸向上連結
  • 如果沒有明确指定調用某個基類的構造器,它就會默默地調用預設構造器
class Bread {
    Bread() { print("Bread()"); }
}

class Cheese {
    Cheese() { print("Cheese()"); }
}

class Lettuce {
    Lettuce() { print("Lettuce()"); }
}

class Meal {
    Meal() { print("Meal()"); }
}

class Lunch extends Meal {
    Lunch() { print("Lunch()"); }
}

class PortableLunch extends Lunch {
    PortableLunch() { print("PortableLunch()"); }
}

class Sandwich extends PortableLunch {
    private Bread b = new Bread();
    private Cheese c = new Cheese();
    private Lettuce l = new Lettuce();

    private Sandwich() { print("Sandwich()"); }

    public static void main(String[] args) {
        new Sandwich();    
    }
}

// 運作結果
Meal()
Lunch();
PortableLunch();
Bread();
Cheese();
Lettuce();
Sandwich();

// 這題剛看的時候突然忘了繼承這一回事了,想想這章講的多态,也是醉了
           

練習11、向Sandwish().java中添加Pickle類

沒啥說的,直接看上面的例子吧

8.3.2 繼承與清理

  • 銷毀的順序應該與初始化的順序相反

練習12、修改練習9,使其能夠示範基類和導出類的初始化順序,然後向基類和導出類中添加成員對象,說明建構起見的初始化順序

練習13、在ReferenceCounting.java中添加一個finalize()方法,用來校驗終止條件

練習14、修改練習12,使得其某個成員變為具有引用計數的共享對象,并證明它可以正确運作

8.3.3 構造器内部的多态方法的行為

  • 如果構造器的内部調用正在構造的對象的某個動态綁定方法
  • 比如或基類的構造器中調用一個被覆寫的方法
  • 被覆寫的方法在對象完全構造之前就被調用,可能會造成一些難于發現的隐藏錯誤
class Glyph {
	
	Glyph() {
		print("Glyph() before draw()");
		draw();
		print("Glyph() after draw()");
	}
	
	void draw( ) { print("Glyph.draw()"); }
}

class RoundGlyph extends Glyph {
	private int radius = 1;
	public RoundGlyph(int r) {
		radius = r;
		print("RoundGlyph(),radius = " + radius);
	}
	
	void draw() {
		print("RoundGlyph.draw(),radius = " + radius);
	}
}

public class PolyConstructors {
	public static void main(String[] args) {
		new RoundGlyph(5);
	}
}
// 運作結果
Glyph() before draw()
RoundGlyph.draw(),radius = 0
Glyph() after draw()
RoundGlyph(),radies = 5;

// 這個結果導緻了對RoundGlyph的調用,看起來似乎是我們的目的,
// 但是輸出結果并不正确,出現了bug
           
  • 在其他任何事物發生之前,将配置設定給對象的存儲空間初始化成二進制的零
  • 由于步驟1的緣故,我們發現radius的值為0
  • 按照聲明的順序調用成員的初始化方法
  • 調出導出類的構造器主體

盡可能的用簡單的方法使對象進入正常狀态

避免調用其他方法,能安全調用的是final方法(private方法)

練習15、在PolyConstructors.java中添加一個RectangularGlyph,并證明會出現本節所描述的問題

8.4 協變傳回類型

這個書中說的有點繞,大體意思就是
class Shop {
    Audi buyCar() {
        return new Audi();
    }
}

Shop shop = new Shop();
Car car = shop.buyCar();

// Java SE5之前的版本必須傳回Car的對象,盡管Audi是Car的子類也不允許傳回
// 那麼現在可以了,正常shop.buyCar()傳回了一個Audi對象
           

8.5 用繼承進行設計

組合更加靈活,優先選擇組合

練習16、遵循Transmogrify,java這個例子,建立一個Starship類,包含一個AlertStatus引用,此引用可以訓示三種不同的狀态,納入一些可以改變這些狀态的方法

class AlertStatus {   
    public String getStatus() { return "None"; } 
} 
 
class RedAlertStatus extends AlertStatus { 
    public String getStatus() { return "Red"; }; 
} 
 
class YellowAlertStatus extends AlertStatus {   
    public String getStatus() { return "Yellow"; }; 
} 
 
class GreenAlertStatus extends AlertStatus {   
    public String getStatus() { return "Green"; }; 
} 
 
class Starship {   
    private AlertStatus status = new GreenAlertStatus();   
    public void setStatus(AlertStatus istatus) {     
        status = istatus;   
    }   
    public String toString() { return status.getStatus(); } 
} 
 
public class E16_Starship {   
    public static void main(String args[]) {     
        Starship eprise = new Starship();     
        System.out.println(eprise);     
        eprise.setStatus(new YellowAlertStatus());             
        System.out.println(eprise);     
        eprise.setStatus(new RedAlertStatus());         
        System.out.println(eprise);   
    } 
} 
// 運作結果
Green 
Yellow 
Red 

// 完全可以展現出盡量用組合的觀點
           
8.5.1 純繼承與擴充
  • 純繼承就是完全和基類一樣,是一個(is-a)的關系
  • 擴充就是在基類的基礎上增加額外資訊,像一個(like-a)的關系
  • 擴充導緻擴充部分不能被基類所通路
8.5.2 向下轉型
public class Car {
	public void move() {
		System.out.println("是個車子就能動彈");
	}
}

public class Audi extends Car {

	@Override
	public void move() {
		System.out.println("小奧迪跑的嗖嗖的");
	}
	
	public static void fly() {
		System.out.println("我這個奧迪車,好像夠嗆能飛");
	}
}

public class Person {

	public static void main(String[] args) {	
		Car car = new Car();
		Car audi = new Audi();
        // 轉型失敗,傳回一個ClassCastException異常
		((Audi)car).fly();
        // 轉型成功
		((Audi)audi).fly();
	}

}

// 如果是聲明父類的引用,建立子類的執行個體,那麼可以向下轉型
// 如果聲明父類的引用,建立了父類的執行個體,那麼就是一個父類的對象,無法向下轉型
           
練習17、使用練習1中的Cycle的層次結構,在Unicycle和Bicycle中添加balance()方法,而Tricycle中不添加,建立所有這三種類型的執行個體,并将它們向上轉型為Cycle數組,數組的每一個元素上都嘗試調用balance(),并觀察結果,然後将它們向下轉型,再次調用balance(),并觀察将發生什麼
public class E17_RTTI {   
    public static void main(String[] args) {     
        Cycle[] cycles = new Cycle[]{ new Unicycle(),        
                                      new Bicycle(), 
                                      new Tricycle() }; 
        // Compile time: method not found in Cycle:     
        // cycles[0].balance();     
        // cycles[1].balance();     
        // cycles[2].balance();         
        ((Unicycle)cycles[0]).balance();  // Downcast/RTTI             
        ((Bicycle)cycles[1]).balance();   // Downcast/RTTI             
        ((Unicycle)cycles[2]).balance();  // Exception thrown   
    } 
}

 
public class Unicycle extends Cycle {   public void balance() {} } 

public class Bicycle extends Cycle {   public void balance() {} }

————————————————————————————————————————————————————————————————————
// 上面是官方答案,我再用我寫的車子寫個例子

// 車的基類
public class Car { 

}
// 奧迪
public class Audi extends Car {
	public void move() {
		System.out.println("小奧迪跑的嗖嗖的");
	}
}
// Jeep
public class Jeep extends Car {
	public void move() {
		System.out.println("大Jeep直接開上山");
	}
}
// 寶馬
public class Bmw extends Car {

}

public class Person {
	public static void main(String[] args) {	
		Car[] cars = new Car[] {new Audi(),new Jeep(),new Bmw()};
//		cars[0].move();
//		cars[1].move();
//		cars[2].move();		
		((Audi)cars[0]).move();
		((Jeep)cars[1]).move();
		((Jeep)cars[2]).move();		
	}
}
// 運作結果分析

如果直接調用,因為是聲明的父類的引用,根本就找不到move()方法
cars[0]和cars[1]轉為Audi和Jeep沒問題
但是cars[2]本身是Bmw的對象,盡管聲明為父類的引用,但是想要轉成Jeep,那指定不可以
           

8.6 總結

多态意味着“不同的形式”,多态可以帶來很多的成效,更快的程式開發過程、更好的代碼組織、更好擴充的程式以及更容易的代碼維護等

繼續閱讀