- 序
* 在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。
* 多态通过分离做什么和怎么做,从另艺哥角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序。
* 封装通过合并特征和行为来创建新的数据类型。
* 实现隐藏则通过细节私有化把接口和实现分离开来。
* 多态的作用则是消除类型之间的耦合关系。
* 多态(动态绑定,后期绑定和运行时绑定)
8.1 再论向上转型
package ThinkingInJava_08;
public enum Note {
MIDDLE_C, C_SHARP, B_FLAT; // Etc.
}
1
2
3
4
5
6
7
package ThinkingInJava_08;
class Instrument {
public void play(Note n) {
System.out.println("Instrument.play()");
}
}
package ThinkingInJava_08;
public class Wind extends Instrument {
public void play(Note n) {
System.out.println("Wind.play() " + n);
}
}
/*
* Music.tune()接受艺哥Instrument引用,同时也接受任何导出自Instrument的类
* Wind从Instrument继承,所以Instrument的接口必定存在于Wind中。
*/
package ThinkingInJava_08;
public class Music {
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // Upcast
}
}
/* Output
* Wind.play() MIDDLE_C
*/
* 8.1.1 忘记对象类型
o 为什么所有都故意忘记对象的类型?如果不那么做,就需要为系统内Instrument的每种类型都编写一个新的tune()方法。
/*
* 这样做的缺点:必须为添加的每一个新Instrument类编写特定类型的方法。
* 所以我们仅接收基类作为参数,而不是那些特殊的导出类,这是多态所允许的。
*/
package ThinkingInJava_08;
class Stringed extends Instrument {
public void play (Note n) {
System.out.println("Stringed.play() " + n);
}
}
class Brass extends Instrument {
public void play (Note n) {
System.out.println("Brass.play() " + n);
}
}
public class Music2 {
public static void tune(Wind i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Stringed i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Brass i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
Stringed violin = new Stringed();
Brass frenchHorn = new Brass();
tune(flute);
tune(violin);
tune(frenchHorn);
}
}
/* Output
* Wind.play() MIDDLE_C
* Stringed.play() MIDDLE_C
* Brass.play() MIDDLE_C
*/
8.2 转机
* 8.2.1 方法调用绑定
o 将一个方法调用同一个方法主体关联起来被称作绑定。
o 如果在程序执行前进行绑定(由编译器和连接程序实现),叫做前期绑定。
o 后期绑定:在运行时根据对象的类型进行绑定。后期绑定也叫做动态绑定或运行时绑定。也就是说,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。
o Java中除了static和final方法之外,其它所有的方法都是后期绑定。可以说,后期绑定会自动发生。
o final还有一点:可以有效地关闭动态绑定,或者说,告诉编译器不需要对其进行动态绑定。这样编译器就可以为final方法调用生成更有效的代码。然而,大多数情况下,这样做对程序的整体性能不会有什么改观。所以,最好根据设计来决定是否使用final,而不是出于试图提高性能的目的来使用 final。
* 8.2.2 产生正确的行为
o 一旦知道java中所有方法都是通过动态绑定实现多态这个事实之后,我们就可以编写只于基类打交道的程序代码。
o 换种说法,发送消息给某个对象,让该对象去判断应该做什么事。
package ThinkingInJava_08_2;
/**
* Shape基类为自它继承的所有导出类建立公用接口
* @author Reid_Chan
*/
public class Shape {
public void draw() {}
public void erase() {}
}
package ThinkingInJava_08_2;
public class Circle extends Shape {
public void draw() { System.out.println("Circle.draw()"); }
public void erase() { System.out.println("Circle.erase()"); }
}
package ThinkingInJava_08_2;
public class Square extends Shape {
public void draw() { System.out.println("Square.draw()"); }
public void erase() { System.out.println("Square.erase()"); }
}
package ThinkingInJava_08_2;
public class Triangle extends Shape {
public void draw() { System.out.println("Triangle.draw()"); }
public void erase() { System.out.println("Triangle.erase()"); }
}
package ThinkingInJava_08_2;
import java.util.Random;
/**
* 每次调用next()可以随机选择生成一个Shape对象。
* @author Reid_Chan
*/
public class RandomShapeGenerator {
private Random rand = new Random(47);
public Shape next() {
switch (rand.nextInt(3)) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
}
package ThinkingInJava_08_2;
/**
* 说明编译器不需要获得任何特殊信息就能进行正确调用。
* 对draw()方法的所有调用都是通过动态绑定进行的。
* @author Reid_Chan
*/
public class Shapes {
private static RandomShapeGenerator gen = new RandomShapeGenerator();
public static void main(String[] args) {
Shape[] s = new Shape[9];
// Fill up the array with shapes:
for (int i = 0; i < s.length; i++) {
s[i] = gen.next();
}
// Make polymorphic method calls:
for (Shape shp : s) {
shp.draw();
}
}
}
/* Output
* Triangle.draw()
* Triangle.draw()
* Square.draw()
* Triangle.draw()
* Square.draw()
* Triangle.draw()
* Square.draw()
* Triangle.draw()
* Circle.draw()
*/
* 8.2.3 可扩展性
o 由于多态机制,我们可根据自己的需求对系统添加任意多的新类型,而不需要更改tune方法。
o 在一个设计良好的OOP程序中,大多数或者所有方法都会遵循tune()的模型,而且只于基类接口通信。
o 这样的程序是可扩展的。
package ThinkingInJava_08_2;
import ThinkingInJava_08.Note;
/**
* 多态是一项让程序员将改变的事物与未变分离开来的重要技术。
* @author Reid_Chan
*/
class Instrument {
void play(Note n) { System.out.println("Instrument.play()" + n); }
String what() { return "Instrument"; }
void adjust() { System.out.println("Adjusting Instrument"); }
}
class Wind extends Instrument {
void play(Note n) { System.out.println("Wind.play()" + n); }
String what() { return "Wind"; }
void adjust() { System.out.println("Adjusting Wind"); }
}
class Percussion extends Instrument {
void play(Note n) { System.out.println("Percussion.play()" + n); }
String what() { return "Percussion"; }
void adjust() { System.out.println("Adjusting Percussion"); }
}
class Stringed extends Instrument {
void play(Note n) { System.out.println("Stringed.play()" + n); }
String what() { return "Stringed"; }
void adjust() { System.out.println("Adjusting Stringed"); }
}
class Brass extends Wind {
void play(Note n) { System.out.println("Brass.play()" + n); }
void adjust() { System.out.println("Adjusting Brass"); }
}
class Woodwind extends Wind {
void play(Note n) { System.out.println("Woodwind.play()" + n); }
String what() { return "Woodwind"; }
}
public class Music3 {
// Doesn't care about type, so new types
// added to the system still work right:
public static void tune(Instrument instrument) {
// ...
instrument.play(Note.MIDDLE_C);
}
public static void tuneAll(Instrument[] e) {
for (Instrument i : e)
tune(i);
}
public static void main(String[] args) {
// Upcast during addition to the array:
Instrument[] orchestra = { new Wind(), new Percussion(), new Stringed(), new Brass(), new Woodwind() };
tuneAll(orchestra);
}
}
/* Output
* Wind.play()MIDDLE_C
* Percussion.play()MIDDLE_C
* Stringed.play()MIDDLE_C
* Brass.play()MIDDLE_C
* Woodwind.play()MIDDLE_C
*/
* 8.2.4 缺陷: 覆盖 私有方法
package ThinkingInJava_08_2;
/**
* 由于private方法就自动认为是final方法,而且对导出类是屏蔽的。
* 在这种情况下,Dervied类的f()方法就是一个全新的方法。
* 既然基类中的f()方法在子类Dervied中不课件,因此甚至不能被重载。
* 结论:只有非private方法才才可以覆盖;早导出类中,对于基类中的private方法最好采用不同名字。
* @author Reid_Chan
*/
public class PrivateOverride {
private void f() { System.out.println("private f()"); }
public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
}
}
class Derived extends PrivateOverride {
public void f() { System.out.println("public f()"); }
}
/* Output
* private f()
*/
* 8.2.5 缺陷:域与静态方法
o 只有普通的方法调用可以是多态的。
o 如果你直接访问某个域,这个访问就将在编译期进行解析。
package ThinkingInJava_08_2;
/**
* 下面例子中,当Sub对象转型为Super时,任何域访问操作都将由编译器解析,因此不是多态的。
* Super.field和Sub.field分配了不同的存储空间,所以Sub实际上包含两个称为field的域,它自己的和继承得到的。
* 然而,在引用Sub中的field所产生的默认域并非Super的field,所以为了得到Super.field必须显示指明super.field。
* @author Reid_Chan
*/
class Super {
public int field = 0;
public int getField() { return field; }
}
class Sub extends Super {
public int field = 1;
public int getField() { return field; }
public int getSuperField() { return super.field; }
}
public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub(); // Upcast
System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField());
Sub sub = new Sub();
System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField()
+ ", sub.getSuperField() = " + sub.getSuperField());
}
}
/* Output
* sup.field = 0, sup.getField() = 1
* sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
*/
* 尽管上述例子有点容易混淆。但实际很少发生。
o 首先,通常会将域设置成private,因此不能直接访问它们
o 另外,你可能不会对基类中的域和导出的域赋予相同的名字。
* 如果方法是静态的,它的行为就不具有多态性:
package ThinkingInJava_08_2;
/**
* 静态方法是与类,而并非与单个的对象相关联的。
* @author Reid_Chan
*/
class StaticSuper {
public static String staticGet() {
return "Base staticGet()";
}
public String dynamicGet() {
return "Base dynamicGet()";
}
}
class StaticSub extends StaticSuper {
public static String staticGet() {
return "Derived staticGet()";
}
public String dynamicGet() {
return "Derived dynamicGet()";
}
}
public class StaticPolymorphism {
public static void main(String[] args) {
StaticSuper sup = new StaticSub(); // Upcast
System.out.println(sup.staticGet());
System.out.println(sup.dynamicGet());
}
}
/* Output
* Base staticGet()
* Derived dynamicGet()
*/
8.3 构造器和多态
* 构造器并不具有多态性(隐式声明static),但还是非常有必要理解构造器怎样通过多态在复杂的层次结构中运行。
* 8.3.1 构造器的调用顺序
o 基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。
o 这样做是有意义的,因为构造器的任务就是检查对象是否被正确地构造。
o 且只有基类的构造器才具有恰当的知识和权限类对自己的元素进行初始化。因此,必须令所有构造器都得到调用,否在就不可能正确地构造完整对象。
o 这正是编译器为什么要强调每个导出类部分都必须调用构造器的原因。
o 在导出类的构造器主题中,如果没有明确指定调用某个基类构造器,它就会默默地调用默认构造器。如果不存在默认构造器,编译器就会报错(若某个类没有构造器,编译器会自动合成一个默认构造器)
package ThinkingInJava_08_3;
/**
* 复杂对象调用构造器顺序要遵循下面的顺序:
* 1.调用基类构造器。步骤会不断地反复递归下去,
* 首先是构造这增次结构的跟,然后是下一层导出类。知道最低层的导出类。
* 2.按声明顺序调用成员的初始化方法。
* 3.调用导出类构造器的主体。
* @author Reid_Chan
*/
class Meal {
public Meal() { System.out.println("Meal()"); }
}
class Bread {
public Bread() { System.out.println("Bread()"); }
}
class Cheese {
public Cheese() { System.out.println("Cheese()"); }
}
class Lettuce {
public Lettuce() { System.out.println("Lettuce()"); }
}
class Lunch extends Meal {
public Lunch() { System.out.println("Lunch()"); }
}
class PortableLunch extends Lunch {
public PortableLunch() { System.out.println("PortableLunch()"); }
}
public class Sandwich extends PortableLunch {
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich() { System.out.println("Sandwich()"); }
public static void main(String[] args) {
new Sandwich();
}
}
/* Output
* Meal()
* Lunch()
* PortableLunch()
* Bread()
* Cheese()
* Lettuce()
* Sandwich()
*/
* 8.3.2 继承与清理
o 通过组合和继承方法来创建新类时,永远不必担心对象的清理问题,子对象都会留给垃圾回收器进行处理。
o 如果确实遇到清理的问题,那么必须用心为新类创建dispose()方法(随便起的)
o 并且由于继承的缘故,如果我们有其他作为垃圾回收一部分的特殊清理工作,就必须在导出类中覆盖dispose()方法。当覆盖被继承类的 dispose()方法,务必记住调用基类版本dispose()方法;否则基类的清理动作就不会发生。
package ThinkingInJava_08_3;
/**
* 销毁的顺序应该和初始化顺序相反
* 对于字段,则意味着与声明顺序相反。
* 对于基类,应该首先对其导出类进行清理,然后才是基类。
* 这是因为导出类的清理可能会调用基类中的某些方法,
* 所以需要使基类中的构建仍起作用而不应过早地销毁。
* 这个例子说明,尽管通常不必执行清理工作,但一旦执行,就必须谨慎和小心。
* @author Reid_Chan
*/
class Characteristic {
private String s;
public Characteristic(String s) {
this.s = s;
System.out.println("Creating Characteristic " + s);
}
protected void dispose() {
System.out.println("disposing Characteristic " + s);
}
}
class Description {
private String s;
public Description(String s) {
this.s = s;
System.out.println("Creating Description " + s);
}
protected void dispose() {
System.out.println("disposing Description " + s);
}
}
class LivingCreature {
private Characteristic p = new Characteristic("is alive");
private Description t = new Description("Basic Living Creature");
public LivingCreature() {
System.out.println("LivingCreature()");
}
protected void dispose() {
System.out.println("LivingCreature dispose");
t.dispose();
p.dispose();
}
}
class Animal extends LivingCreature {
private Characteristic p = new Characteristic("has heart");
private Description t = new Description("Animal not Vegetable");
public Animal() {
System.out.println("Animal()");
}
protected void dispose() {
System.out.println("Animal dispose");
t.dispose();
p.dispose();
super.dispose();
}
}
class Amphibian extends Animal {
private Characteristic p = new Characteristic("can live in water");
private Description t = new Description("Both water and land");
public Amphibian() {
System.out.println("Amphibian()");
}
protected void dispose() {
System.out.println("Amphibian dispose");
t.dispose();
p.dispose();
super.dispose();
}
}
public class Frog extends Amphibian {
private Characteristic p = new Characteristic("Croaks");
private Description t = new Description("Eats Bugs");
public Frog() { System.out.println("Frog()"); }
protected void dispose() {
System.out.println("Frog dispose");
t.dispose();
p.dispose();
super.dispose();
}
public static void main(String[] args) {
Frog frog = new Frog();
System.out.println("Bye");
frog.dispose();
}
}
/* Output
* Creating Characteristic is alive
* Creating Description Basic Living Creature
* LivingCreature()
* Creating Characteristic has heart
* Creating Description Animal not Vegetable
* Animal()
* Creating Characteristic can live in water
* Creating Description Both water and land
* Amphibian()
* Creating Characteristic Croaks
* Creating Description Eats Bugs
* Frog()
* Bye
* Frog dispose
* disposing Description Eats Bugs
* disposing Characteristic Croaks
* Amphibian dispose
* disposing Description Both water and land
* disposing Characteristic can live in water
* Animal dispose
* disposing Description Animal not Vegetable
* disposing Characteristic has heart
* LivingCreature dispose
* disposing Description Basic Living Creature
* disposing Characteristic is alive
*/
* 然而,如果成员对象中存在于其他一个或多个对象共享的情况,问题就变得更加复杂了,此时不能简单地假设你可以调用dispose()。
* 这种情况下,也许就必需使用引用计数来跟踪仍旧访问着共享对象的对象数量。
package ThinkingInJava_08_3;
/**
* static long counter跟踪所创建的Shared的实例的数量,还可以为id提供数值。
* counter的类型是long而不是int,这样可以防止溢出,
* id是final的,因为不希望它的值在对象声明周期中被改变。
* 在将一个共享对象附着到类上时,必须记住调用addRef(),但是dispose()方法将跟中引用数
* 并决定何时执行清理。
* PS:使用此技术需要加倍小心。
*
* @author Reid_Chan
*/
class Shared {
private int refcount = 0;
private static long counter = 0;
private final long id = counter++;
public Shared() {
System.out.println("Creating " + this);
}
public void addRef() { refcount++; }
protected void dispose() {
if (--refcount == 0) {
System.out.println("Disposing " + this);
}
}
public String toString() { return "Shared " + id; }
}
class Composing {
private Shared shared;
private static long counter = 0;
private final long id = counter++;
public Composing(Shared shared) {
System.out.println("Creating " + this);
this.shared = shared;
this.shared.addRef();
}
protected void dispose() {
System.out.println("disposing " + this);
shared.dispose();
}
public String toString() { return "Composing " + id; }
}
public class ReferenceCounting {
public static void main(String[] args) {
Shared shared = new Shared();
Composing[] composings = { new Composing(shared), new Composing(shared), new Composing(shared),
new Composing(shared), new Composing(shared) };
for (Composing c : composings)
c.dispose();
}
}
/* Output
* Creating Shared 0
* Creating Composing 0
* Creating Composing 1
* Creating Composing 2
* Creating Composing 3
* Creating Composing 4
* disposing Composing 0
* disposing Composing 1
* disposing Composing 2
* disposing Composing 3
* disposing Composing 4
* Disposing Shared 0
*/
* 8.3.3 构造器内部的多态方法的行为
* 如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。
* 然而,这个调用的效果可能相当难于预料,因为被覆盖的方法在对象被完全构造之前就会被调用,这可能会造成一些难以发现的隐藏错误
package ThinkingInJava_08_3;
/**
* Glyph.draw()方法设计为将要被覆盖的。
* 但是Glyph构造器会调用这个方法,结果导致了RoundGlyph.draw()的调用。
* 但从输出结果可以看到,radius不是默认初始化值1,而是0。
* 所以,实际初始化的过程是:
* 1.在其它任何事物发生之前,将分配给对象的存储空间初始化为二进制的0
* 2.如上所述调用基类构造器。此时,调用被覆盖后的draw()方法,由于1,所以radius=0
* 3.按照声明顺序调用成员的初始方法。
* 4.调用导出类的构造器主题。
*
* @author Reid_Chan
*/
class Glyph {
void draw() { System.out.println("Glyph.draw()"); }
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
System.out.println("RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
/* Output
* Glyph() before draw()
* RoundGlyph.draw(), radius = 0
* Glyph() after draw()
* RoundGlyph.RoundGlyph(), radius = 5
*/
* 因此,编写构造器时有一条有效的准则: 用金可能简单的方法使对象进入正常状态;如果可以的话,避免调用其它方法 。
o 在构造器内唯一能够安全调用的那些方法是基类中的final方法。这些方法不能被覆盖,因此也就不会出现问题。
8.4 协变返回类型
* 协变返回类型:它表示在导出类中的被覆盖方法可以返回基本方法的返回类型的某种导出类型
package ThinkingInJava_08;
/**
* 在较早版本之间的主要差异就是较早的版本将强制process()的覆盖版本必须返回Grain
* 而不能返回Wheat。
* 协变返回类型允许返回更具体的Wheat类型。
*
* @author Reid_Chan
*/
class Grain {
public String toString() { return "Grain"; }
}
class Wheat extends Grain {
public String toString() { return "Wheat"; }
}
class Mill {
Grain process() { return new Grain(); }
}
class WheatMill extends Mill {
Wheat process() { return new Wheat(); }
}
public class CovariantReturn {
public static void main(String[] args) {
Mill m = new Mill();
Grain g = m.process();
System.out.println(g);
m = new WheatMill();
g = m.process();
System.out.println(g);
}
}
/* Output
* Grain
* Wheat
*/