Java编程思想读书笔记——第八章:多态

第八章 多态

  • 多种类型(从同一基类导出的)视为同一类型来处理
  • 同一份代码也就可以毫无差别地运行在这些不同类型之上了

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 总结

多态意味着“不同的形式”,多态可以带来很多的成效,更快的程序开发过程、更好的代码组织、更好扩展的程序以及更容易的代码维护等

猜你喜欢

转载自blog.csdn.net/pengbo6665631/article/details/82492237