Thinking In Java笔记(第八章 多态)

第八章 多态

在面向对象的程序设计语言中,多态是继抽象和技能之后的第三种基本特征。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序。

多态的作用是用来消除类型之间的耦合关系。

8.1 再论向上转型

将某个对象的引用视为对其基类对象的做法被称作向上转型。但是这样做也有问题。看如下的例子:

public enum Note {

MIDDLE_C, C_SHARP, B_FlAT;

}

class Instrument {
    public void play(Note n) {
        System.out.println("Instrument.play()");
    }
}

class Wind extends Insrument {
    public void play(Note n) {
        System.out.println("wind.play() + " n);
    }
}

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);
    }
}

output: wind.play() MIDDLE_C

Music.tune()方法接受一个Instrument引用,同时也接受任何导出自Instrument类。当tune()方法中接受到一个wind引用时,会自动向上转型,这样做时允许的,因为instrument中的接口再wind中都存在。从wind向上转型会缩小接口,但是不会小于Instrument的接口。

8.1.1 忘记对象类型

如果让tune()方法直接接受一个wind引用作为自己的参数,表面上更加直观,但是每加入一种新的乐器(也就是Instrument的子类),相应的tune方法就需要重载一次。但是如果利用了多态,将instrument的多个导出类引用看作时instrument的引用,就会轻松很多。

8.2 转机

在运行上面的程序之后,传入instrument的导出类引用给tune方法,它也能识别到底是哪个导出类,很多人都会奇怪,编译器怎么知道的呢?其实编译器根本就不知道。。。。

8.2.1 方法调用绑定

将一个方法调用同一个方法主体关联起来被称作绑定。绑定分为前期绑定和后期绑定。

  • 前期绑定:面向过程中不需要选择就默认绑定的方式。
  • 后期绑定:运行时根据对象的类型进行绑定。也称作动态绑定或者运行时绑定。

Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定的。

8.2.2 产生正确的行为

Java中所有方法都是通过动态绑定实现多态,我们可以只编写与基类打交道的程序代码了,并且这些代码都可以对所有导出类正确的运行。例如:

class Shape {
    public void draw() {}
    public void erase() {}
}

class Circle extends Shape {
    public void draw() {
        System.out.println("Circle.draw()");
    }

    public void erase() {
        System.out.println("Circle.erase()");
    }
}

class Square extends Shape {
    public void draw() {
        System.out.println("Square.draw()");
    }
    public void erase() {
        System.out.println("Square.erase()");
    }
}

class Triangle extends Shape {
    public void draw() {
        System.out.println("Triangle.draw()");
    }
    public void erase() {
        System.out.println("Triangle.erase()");
    }
}

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();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        RandomShapeGenerator rsg = new RandomShapeGenerator();
        Shape[] s = new Shape[9];
        for(int i = 0; i < s.length; i++) {
            s[i] = rsg.next();
        }
        for(Shape shp : s)
            shp.draw();
    }
}

输出:
Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()

上面的例子中,向上转型发生在return中,每次返回Circle、Square、Triangle三个其中的一个引用,但是我们通过next()方法获得的总是一个通用的Shape引用,再来利用动态绑定实现多态。

8.2.3 可扩展性

针对上面的两种方法(draw和erase),任何Shape的导出类,覆盖了这两个接口,利用动态绑定,我们都能够很好的对其进行使用,不用管每个导出类是如何实现的。

8.2.4 缺陷:对私有方法的覆盖

例如:

public class PrivateOverride {
    private void f() {
        System.out.println("private f()");
    }
    public static void main(String[] args) {
        Private Override po = new Deived();
        po.f();
    }
}

class Derived extends PrivateOverride {
    public void f() {
        System.out.println("public f()");
    }
}

像上面那样做,我们期待的结果是”public f()”, 但是结果为”private f()”,因为private属于final类方法,其不可以被覆盖,而且属于前期绑定,而非动态绑定,因此无法实现动态绑定。

8.2.5 缺陷:域与静态方法

一旦了解了多态机制,可能会认为所有事物都可以多态的发生,然而只有普通方法才表现出多态。如果直接访问某个域,将不表现为多态。例如:

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 JavaTest{
    public static void main(String[] args) {

        Super sup = new Sub(); //向上转型
        //这里在直接访问域的时候,并没有出现理想中的多态
        System.out.println("sup.field: " + sup.field + "\nsup.getField: " + sup.getField() + "\n");

        Sub sub = new Sub();
        System.out.println("sub.field: " + sub.field + "\nsub.getField: " + sub.getField() + "\nsub.getSuperField():" + sub.getSuperField());

    }
}
输出为:
sup.field: 0
sup.getField: 1

sub.field: 1
sub.getField: 1
sub.getSuperField():0

当Sub对象转型为Super引用时,任何域访问操作都将由编译器解析,由于多态采用的是动态绑定,而不是靠编译器,所以无法完成多态。

在上面的例子中,一个sub对象中有两个叫做field的域(Super.field和Sub.field),然而在引用sub中的field时所产生的默认域并非Super版本的field,因此必须显示的指明super.field。

8.3 构造器和多态

通常构造器不同于其他的方法,涉及到多态的时候也是如此。尽管构造器并不具有多态性(实际上构造器是static方法,只不过是隐式的static声明)。

8.3.1 构造器的调用顺序

在之前的[学习笔记](“http://blog.csdn.net/jing_unique_da/article/details/45530563“)中也提到过了初始化的顺序,调用的顺序是:

  1. 调用基类的构造器。这个步骤一直递归下去,直至递归到根类,再开始从根向导出类开始初始化。
  2. 按声明顺序调用成员的初始化方法。
  3. 调用导出类构造器的主体。

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

现在来分析在一个构造器内部调用正在构造的对象的某个动态绑定方法时,会发生什么事情。例如:

class Super {
    public int field;
    public Super() {
        System.out.println("Super() Before");
    getField();
        System.out.println("Super() after");
    }

    public int getField() {
        System.out.println("Super() " + field);
    return field;
    }
}
class Sub extends Super {
    public int field = 1;
    public Sub(int i) {
        field = i;
        System.out.println("Sub() " + field);
    }
    public int getField() {
        System.out.println("Sub() " + field);
        return field;
    }
}
public class JavaTest{
    public static void main(String[] args) {
        new Sub(5);
    }
}
输出为:
Super() Before
Sub() 0
Super() after
Sub() 5

这里的getField()方法被覆盖了,但是在Super类的构造器中调用的getField方法并不是Super的,而是覆盖之后的方法,而且结果也不是5,而是0。原因在于前面讲述的构造顺序不完整。初始化的实际过程是:

  1. 在其他任何事情发生之前,先将分配给对象的存储空间初始化成二进制的零。
  2. 递归调用基类的构造器。
  3. 按照声明的顺序调用成员的初始化方法。
  4. 调用导出类的构造器主体。

8.4 协变返回类型

Java SE5中添加了协变返回类型,也就是在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型,话有些绕口,直接上例子:

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 JavaTest{
    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);
    }
}
结果:
Grain
Wheat
时间: 2024-10-31 03:11:57

Thinking In Java笔记(第八章 多态)的相关文章

《JAVA编程思想》学习笔记——第八章 多态

在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征 多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来.多态不但能够 方法调用绑定 将一个方法调用同一个方法主体关联起来被称作绑定.若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定. 后期绑定:在运行时根据对象的类型进行绑定.后期绑定也叫做动态绑定或运行时绑定. Java中除了static方法和final方法(private方法属于final方法)之外,其它所有的方法都是后期绑定.这意味着通常情

《Java编程思想》笔记 第八章 多态

1.向上转型 把子类引用当作父类引用.(子类对象赋值给父类引用) 2.绑定 确定方法属于哪个类. 3.前期绑定 程序执行前绑定. 4.后期绑定也叫动态绑定 程序运行时绑定. 5.构造器和多态 5.1 域 与静态方法(包括构造器)不具有多态性. 5.2 构造器内可以多态,调用子类中被覆写的方法,但不安全. 5.3 构造器内可以安全调用的方法只有基类的final方法. 知识点 私有方法被覆盖后和基类无关了,属于子类的新方法,只是和基类私有方法同名而已. 清理顺序和初始化顺序相反. this 代表此对

java笔记 第八章

1  数组的定义: 是一组相关变量的集合. 2  数组的要素: 标识符: 首先,和变量一样,在计算机中,数组也要有一个名称,称为标识符,用于区分不同的数组. 数组元素: 当给出了数组名称,即数组标识符后,要向数组中存放数据,这些数据就称为数组元素. 数组下标: 在数组中,为了正确地得到数组的元素,需要对它们进行编号,这样计算机才能根据编号去存取,这个编号就称为数组下标. 元素类型: 存储在数组中的数组元素应该是同一数据类型,如可以把学员的成绩存储在数组中,而每一个学员的成绩可以用整型变量存储,因

5.3-全栈Java笔记:面向对象特征(二)封装、多态

封装(encapsulation) 封装的作用和含义 我要看电视,只需要按一下开关和换台就可以了.有必要了解电视机内部的结构吗?有必要碰碰显像管吗?制造厂家为了方便我们使用电视,把复杂的内部细节全部封装起来,只给我们暴露简单的接口,比如:电源开关.具体怎么内部实现的,我们不需要操心. 需要让用户知道的暴露出来,不需要让用户了解的全部隐藏起来.这就是封装. 我们程序设计要追求"高内聚,低耦合". 高内聚就是类的内部数据操作细节自己完成,不允许外部干涉:低耦合:仅暴露少量的方法给外部使用,

Java编程思想---第八章 多态(上)

第八章  多态(上) 在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征. 多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来,多态不但能改善代码的组织结构和可读性,还能够创建可扩展的程序,无论在项目最初创建时还是在需要添加新功能时都可以生长程序.封装通过合并特征和行为来创建新的数据类型. 8.1 再论向上转型 在上一章中我们已经知道,对象可以作为它本身的类型使用,也可以作为它的基类使用,而这种把对某个对象的引用视为对其基类的引用的做法被称为向上转型. //: dem

JAVA笔记六

JAVA笔记总结六 把大象放入冰箱的几步: 面向对象的三个特征:封装,继承,多态 JAVA开发实际上就是找对象使用,没有对象就创建一个对象 找对象,建立对象,维护对象的关系 类和对象的关系:类是对现实生活中实物的描述: 对象就是这类事物,实实在在存在的个体 匿名对象:匿名对象可以作为参数进行传递也可以作为对象方法进行一次调用

Thinking in java 笔记三

第八章 多态 多态是继数据抽象和继承之后的第三种基本特征 8.1 再论向上转型 8.2 转机 方法调用绑定,java除了static和final(private方法属于final方法)外其他所有方法都是后期绑定 多态是一项将改变的事物与未变的事物分离开来的重要技术 不要试图覆盖基类private方法,只是在之类中产生了一个全新的方法 域的访问操作由编译器解析不是多态的,静态方法也不具有多态性 8.3 构造器和多态 构造器调用顺序 P159 如果需要清理,在子类覆盖基类清理方法,清理完自身后调用基

4.1-全栈Java笔记:对象的进化史

面向对象和面向过程的区别 面向过程编程思想思考问题时,我们首先思考"怎么按步骤实现?"并将步骤对应成方法,一步一步,最终完成. 这个适合简单任务,不需要过多协作的情况下.比如,如何开车?我们很容易就列出实现步骤: 1. 发动车 2. 挂挡 3.踩油门 4. 走,你 面向过程适合简单.不需要协作的事务. 如果,我们需要思考"如何造车?",你就会发现列出1234这样的步骤,是不可能的.那是因为,造车太复杂,需要很多协作才能完成. 面向对象(Object)编程,更契合人的

5.1-全栈Java笔记:面向对象的特征(一)继承 | 上

JAVA面向对象进阶 本章重点针对面向对象的三大特征:继承.封装.多态,进行详细的讲解.很多概念对于初学者来说,更多的是先进行语法性质的了解,不要期望,通过本章学习就"搞透面向对象".本章只是面向对象的起点,后面所有的章节说白了都是面向对象这一章的应用. 老鸟建议 建议大家,学习本章,莫停留!学完以后,迅速开展后面的章节.可以说这么说,以后所有的编程都是"面向对象"的应用而已! 继承(extend) 继承的实现 继承让我们更加容易实现类的扩展. 比如,我们定义了人类