多态
多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来
改善代码的组织结构和可读性,还能创建课扩展的程序—可以写出可以生长的程序
消除类型之间的耦合关系
为所有的导出类提供了一个统一的接口,导出类通过重写继承的接口来完成自己的需求
继承允许将对象视为自己本身的类型或基类型来加以处理
不使用多态,必须为添加的每一个新的类编写特定类型的方法.如果我们只编写与基类打交道的代码,会不会更好
绑定
将一个方法调用同方法主体关联起来被称作绑定
绑定分为前期绑定和后期绑定(也称为动态绑定或运行时绑定–就是在运行时根据对象的类型进行绑定)
如果一种语言向实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法—也就是说编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用.
后期绑定必须在对象中安置某种类型信息
在java中,除了static方法和final方法(包括private方法)之外,其他所有的方法都是后期绑定—会自动发生
声明为final的方法除了可以防止其他人覆盖该方法外,还可以有效的”关闭”动态绑定或者说告诉编译器不需要对其进行动态绑定,这样编译器就可以生成更有效的代码.最好根据设计来决定是否使用final,而不是出于试图通过性能
因为有后期绑定,所以在继承了父类后,子类重写了父类的方法,当发生向上转型的时候,实际调用的方法(之前重写的方法)是由对象的类型决定的—–这些都是java中后期绑定实现的
在一个设计良好的oop程序中,大多数或者所有的方法都只与基类的接口通信.这样的程序是可扩展的.多态使得我们所做的代码修改,不会对程序中其他不应受到影响的部分产生破坏
多态是一项程序员”将改变的事物与未变的事物分离开来”的重要技术
缺陷:”覆盖”私有方法
public class PrivateOverride {
private void f() { print("private f()"); }
public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
}
}
class Derived extends PrivateOverride {
public void f() { print("public f()"); }
} /* Output:
private f()
*///:~
此处PrivateOverride中的f()是private方法(默认是final的,即对子类屏蔽,对子类不可见,因此不能重载),在这种情况下Derived中的f()是一个全新的方法
只要非private方法才能被覆盖
在导出类中对基于基类的private方法,最好采用不同的名字
缺陷:”覆盖”私有方法
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
*///:~
如果你直接访问某个域,这个访问就将在编译期进行解析
在本例中,为Super.field和Sub.field 分配了不同存储空间
在引用Sub中的field时默认产生的并非Super版本的域,因此为了得到Super.field,必须显式的调用super.field
构造器的调用顺序
- 基类的构造器总是在导出类的构造过程中被调用,按类的层次结构往上,以使每个基类的构造器得到调用
- 构造器的特殊任务:检查对象是否被正确的构造.导出类只能访问自己的成员,不能访问基类的成员(基类的成员通常是private类型)
- 只有基类的构造器才具有访问恰当的知识和权限来对自己的元素进行初始化.因此必须令所有的构造器都得到调用,否者就不可能正确构造对象.这正是编译器为什么要强制每个导出类都必须调用构造器的原因.在导出类构造器主体中,如果没有明确指定调用某个构造器,它会调用默认构造器.如果不存在默认构造器,编译器就会报错
- 构造器的调用顺序:
- 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制零
- 调用基类的构造器.这个步骤会反复递归下去,首先是构造这种层次结构的根,然后是下一层的导出类,等等,直到最低层的导出类
- 按声明顺序调用成员的初始化方法
- 调用导出类的构造器主体 .
- 构造器的调用顺序是很重要.当进行继承的时候,我已经知道基类的一切,并且可以访问基类中任何声明为public和protected的成员.这意味着在在导出类中,必须假定基类的所有成员都是有效的.
- 一种标准的方法是,我们必须确保所要使用的成员都已经构建完毕.为确保这一目的,唯一的方法是先调用基类的构造器,那么进入导出类构造器时,在基类中可供我们访问成员都已经得到初始化.当成员对象在类内定义的时候,只要有可能就应该对他们进行初始化.若遵循这一规则,那么就能保证所有的基类成员以及当前对象的成员对象都被初始化
继承与清理
- 通过继承和组合创建新类的时候,永远不必担心对象的创建问题,子对象通常都会留给垃圾回收器进行处理
- 如果遇到清理问题必须为新类创建dispose()方法,并且由于继承的缘故,如果我们有其他作为垃圾回收一部分的特殊清理操作,就必须在导出类中覆盖dispose()方法
- 当覆盖被继承类的dispose()方法时,务必调用基类版本的dispose()方法,否则基类的清理动作不会发生.
- 销毁的顺序应和初始化的顺序相反
- 对于基类,应该先对导出类进行处理再对基类进行清理,然后才是基类.这是因为导出类的清理可能会调用基类中的某些方法,所以要使基类中的构件起作用不应过早的销毁它们
- 尽管通常不必执行清理动作,但是一旦选择执行,就必须谨慎小心
- We initialize the base class first, starting with the member objects in order of definition, then the derived class, starting with its m ember objects.
构造器内部的多态方法的行为
- 如果在一个构造器的内部调用正在构造对象的的某个动态绑定方法.会发生什么样的情况呢?
- 在一般方法的内部,动态绑定的调用是在运行时才决定的,因为对象不知道它是属于方法所在的那个类,还是属于那个类的导出类.
- 如果调用构造器内部的一个动态绑定方法,就要用到那个方法被覆盖后的定义
class Glyph {
void draw() { print("Glyph.draw()"); }
Glyph() {
print("Glyph() before draw()");
draw();
print("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
print("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
print("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
*///:~
- The java virtual machine zeroes the bits of the object after it allocates storage,producing a default value for i before any other initialization occurs. The code calls the base-class constructor before running the derived-class initialization, so we see the zeroed value of i as the initial output.
- The danger of calling a method inside a constructor is when that method depends on a derived initialization. Before the derived-class constructor is called, the object may be in an unexpected state (in Java, at least that state is defined; this is not true with all languages – C++, for example).
- The safest approach is to set the object into a known good state as simply as possible, and then perform any other operations outside the constructor
- 编写构造器时有一条有效的原则:用尽可能简单的方法使对象进入正常的状态,如果可以的话避免调用其他方法
- 在构造器中唯一能够安全使用的是是基类中的final方法
- 导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型
用继承进行设计
- 当我们使用现成的类来建立新类时,如果首先考虑使用继承技术,反倒会加重我们的设计负担,使得事情变得复杂
- 更好的方式是首先选择”组合”,尤其是不确定应该使用哪种方式时,组合不会强制我们的程序设计进入继承的层次结构.而且,组合更加灵活
- 通过继承表达行为间的差异,用字段表达状态上的变化