ch8: 多态
在面向对象的程序设计语言中,多态(polymorphism)是继数据抽象和继承之后的第三个基本特征。
多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序——即无论是在项目最初创建时还是在需要添加新功能时都可以“生长”程序。
向上转型
多态一般应用最多的或者最表象的就是基类有很多实现类或者说是子类,通过基类或者是子类向上转型创建父类对象,隐藏了实现细节,同时也更具灵活性,具备更好的可扩展性。
“覆盖”私有方法
当然在这种情况下,也存在着一定的隐患,复写父类方法时,父类方法是private或者final时,子类以为覆写了该方法,实际上对于父类来说,用子类向上转型的方式创建的子类该“覆写”方法是不可见的,调用的还是父类的这个private或者final方法,所以好的习惯是:在覆写父类的方法时,最好添加
@Override
注解,以便于在编译时或者ide提示时发现。域和静态方法
多态只是针对普通的方法调用,对于域和静态是不具备多态性的。
例如,如果直接访问某个域(field),这个访问就将在编译期解析。
对于域来说,一般都不会出现这个问题,首先,各自类中的域一般是private私有的,其次一般父类和子类不会同时定义相同的名字。示例如下:
// Direct field access is determined at compile time.
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
*///:~
如果某个方法是静态的,它的行为就不具有多态性。
构造器和多态
通常,构造器不同于其他种类的方法。涉及到多态时仍是如此。构造器不具有多态性,它们实际上是static方法,只不过该static声明是隐式的。
- 构造器的调用顺序
基类的构造器总是在导出类的构造过程被调用,而且是按照继承层次逐渐向上链接,以使每个基类的构造器都能够得到调用。这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确的构造。导出类只能访问它自己的成员,不能访问基类的成员(基类成员通常是private的)。只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化。因此必须令所有构造器都得到调用,否则就不可能正确的构造完整的对象。这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因。在导出类的构造器主体中,如果没有明确指定调用某个基类构造器,它就会“默默”地调用默认构造器。如果不存在默认构造器,编译器就会报错。 - 构造器内部的多态方法行为
在一般的方法内部,动态绑定的调用是在运行时才决定的,因为对象无法知道它属于方法所在的类,还是属于那个类的导出类。
如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。然而,这个调用效果可能相当难以预料,因为被覆盖的方法在对象被完全初始化之前就会被调用。这个可能会造成一些难以发现的隐藏错误。
从概念上讲,构造器的工作实际上是创建对象。在任何构造器内部,整个对象可能只是部分形成——只知道基类对象已经进行了初始化。如果构造器只是在构建对象过程中的一个步骤,并且该对象所属的类是从这个构造器所属的类导出的,那么导出的部分在当前构造器正在被调用的时刻仍旧是没有被初始化的。然而,一个动态绑定的方法调用却会向外深入到继承层次结构内部,它可以调用导出类里的方法。所以如果在构造器内部这样做,那么就可能会调用某个方法,而这个方法所操纵的成员可能还未进行初始化——这肯定会招致灾难。如下例子:
// Constructors and polymorphism
// don't produce what you might expect.
class Glyph {
void draw() { print("Glyph.draw()");} //print方法是一个工具方法,其实就是一个System.out.println方法
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
*//:~
可以看出基类构造器中调用的子类覆盖的方法并未返回打印出默认值1,这就是上面所说的问题。
初始化的实际过程是:
1. 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的0.
2. 调用基类构造器
3. 按照声明顺序调用成员的初始化方法
4. 调用导出类的构造器主体。
综上,编写构造器时有一条有效的准则:
用尽可能简单的方法使对象进入正常状态;如果可以的话避免调用其他方法。
如果确实需要调用其他方法的话,这些方法应该是private或者final的子类无法继承覆盖的。
用继承进行设计
当考虑复用基类属性时,可以优先考虑“组合”而不是继承关系,从而提高程序的灵活性。这个“组合”的对象中又以基类对象作为域(field)最佳。
ch9: 接口
接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法。
对于接口来说,所有的域(field)都是隐式final static的,所以可以不用在前面新增这些限定符,方法(method)都是public的,不能指定为其他类型的,并且这些方法只有声明,没有具体实现,到java8以后,为了兼容老的代码,因为很多项目都是使用了这些接口,想要在在原来的接口中新增方法可以添加方法,可以使用default修饰为方法添加默认实现,这样,老的代码升级到新的jdk对于这些接口也不用动,默认方法实现可以使接口更加灵活。
接口就是高度抽象出来的超类,通过多态实现高灵活度组合调用,还有就是对于策略模式、代理模式等,大多也是利用了接口实现的。
再说一下,继承与接口的区别,接口可以多实现(implement),域和方法都是默认public static的,对于继承,只能单继承,所以很多情况下应该考虑使用接口,而不是使用类的方式作为超类