多态可以让你忘掉对象的类型- 一个接口接收的参数为一个基类, 那么这个基类的子类也可以作为入参传入, 这样省下很多代码,不用针对一个类写一个类

方法的调用绑定

绑定 - 将一个方法调用与一个方法主体关联起来

就是因为集成关系,有很多个同名方法,每一个不同类型的对象都对应一个方法, 把这个对象跟这个方法关联起来叫绑定

前期绑定

绑定发生在程序运行前,前期绑定的问题点在于无法判断绑定的方法是哪个,也就是对应参数不知道是哪个,不知道是父类还是哪个子类

后期绑定也叫动态绑定或运行时绑定

后期绑定就必须知道它对象的类型,从而调取正确的方法, 而java是面向对象编程, 对象自带类型,就解决了这一问题, 也就是说除了static跟final(private也是隐式的final), 其它的都是默认的后期绑定(不需要判断后期绑定是否发生,它默认会发生), 所以的java代码都是通过后期绑定来实现多态的

所以这里final就体现了另一个特点, 关闭动态绑定,它告诉编译器不需要进行动态绑定, 这可以让final的代码更高效,然而大部分情况这么做不会对整体性能有什么影响,所以final的应用还是主要用于设计,而不是性能优化

可扩展性

子类重写父类的方法, 但这个方法并不影响其父类的其他子类, 这样就实现了”将改变事物与不变事物分离”,这是多态的重要技术之一

陷阱:重写私有方法

  1. // polymorphism/PrivateOverride.java
  2. // Trying to override a private method
  3. // {java polymorphism.PrivateOverride}
  4. package polymorphism;
  5. public class PrivateOverride {
  6. private void f() {
  7. System.out.println("private f()");
  8. }
  9. public static void main(String[] args) {
  10. PrivateOverride po = new Derived();
  11. po.f();
  12. }
  13. }
  14. class Derived extends PrivateOverride {
  15. public void f() {
  16. System.out.println("public f()");
  17. }
  18. }
  19. 输出: private f()

因为private是隐形的final, 它是关闭后期绑定的, 也就是它对子类是不可见的 , 就谈不上子类重写父类了,例子中虽然这么写了相同名称的方法”f()”, 但它跟基类的”f()”没有关系,如果用@override来注释, 会报错

陷阱:属性跟静态方法

属性:属性是在编译的时候被解析,父类跟子类的属性会开辟不同的内存空间,各代表各的, 在类加载的最开始加载属性,在方法之前,因此并不会有后期绑定,也就是动态绑定只是针对(对象的)方法

静态方法:静态方法只跟类有关, 跟对象无关, 在类加载的时候加载 因此也谈不上后期绑定

构造器的调用顺序

之前说过, 在类初始化的时候,编译器会判断是否存在基类, 按基类的集成结构向上调用构造器, 这么做的目的主要是因为, 构造器的作用不光是初始化数据, 也是监视对象初始化是否正确这一职责, 而大部分类的属性是private的, 只有本类才有权限跟能力去初始化属性值, 所有就需要依次向上调用到所有的构造器, 才会使初始化正确

构造器内部多态方法的行为

// polymorphism/PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect
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);
    }

    @Override
    void draw() {
        System.out.println("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.RoundGlyph(), radius = 5

前一小节描述的初始化顺序并不十分完整,而这正是解决谜团的关键所在。初始化的实际过程是:
1.在所有事发生之前,分配给对象的存储空间会被初始化为2进制0
2.调用基类构造器, 基类构造器里要是有方法的话,会调用引起该基类被初始化的子类的重写后的方法,此时调用重写后的 draw() 方法(是的,在调用 RoundGraph 构造器之前调用),由步骤 1 可知,radius 的值为 0。
3.按声明顺序初始化成员变量
4.最终调用派生类的构造器

向下转型与运行时类型信息

向上转型是绝对安全的,因为基类里有的方法,子类都有, 相反,向下转型就不是安全的, 因为子类里有的基类不一定有, 而且一个基类可能有多个子类, 向下转型时就需要明确它向下转型成了哪个子类, 在java中, 每次转型都会被检查, 以确保它是我们希望的那种类型, 如果不是, 会跑ClassCastException 类型转换异常, 这种在运行时检查类型的行为叫做运行时类型信息