Java编程思想 学习笔记8

八、多态 

在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。

多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。

“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。而多态的作用则是消除类型之间的耦合关系。

继承允许将对象视为它自己本身的类型或其基类类型来加以处理。这种能力极其重要,因为它允许将多种类型视为同一类型来处理,而同一份代码也就可以毫无差别地运行在这些不同类型之上了。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一基类导出而来的。这种区别是根据方法行为的不同而表现出来的,虽然这些方法都可以通过同一个基类来调用。

1.绑定 

  ①方法调用绑定   将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行之前进行绑定(由编译器和连接程序实现),叫做前期绑定

  在运行时根据对象的类型进行绑定,叫做后期绑定(多态绑定或运行时绑定)。如果一种语言想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用适当的方法。也就是说,编译器一直不知道对象的类型,但是方法调用机制能够找到正确的方法体,并加以调用。后期绑定机制不管怎样都必须在对象中安置某种“类型信息”。

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

  ②产生正确的行为

  Java中所有方法都是通过动态绑定实现多态的。

  基类为自它那里继承而来的所有导出类建立了一个公用接口——也就是说,所有导出类都可以做出基类所有的行为。导出来通过覆盖这些行为的定义,来为每种特殊的对象提供单独的行为。

  ③可扩展性

  只与基类通信,这样的程序是可扩展的,因为可以从通用的类型继承出新的数据类型,从而增添一些功能。那些操纵基类接口的方法不需要任何改动就可以应用于新类。

  多态是一项让程序员“将改变的事物与未变的事物分离开来”的重要技术。

  ④缺陷:“覆盖”私有方法

  若我们试图这样做:

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()
*/

  我们所期望的输出是public f(),但是由于private方法被自动认为是final方法(因此是前期绑定,根据引用类型判断),而且对导出类是屏蔽的。因此,在这种情形下,Derived类中的f()方法就是一个全新的方法;既然基类中的f()方法在子类中不可见,因此甚至不能被重载。

  结论:只有非private方法才可以被覆盖。在导出类中,对于基类中的private方法,最好采用不同的名字。

  ⑤缺陷:域与静态方法

  只有普通的方法调用可以是多态的。例如,如果你直接访问某个域,这个访问就将在编译器进行解析。任何域访问操作都将由编译器解析,因此不是多态的。

  如果某个方法是静态的,它的行为就不具有多态性。静态方法是与类,而并非与单个的对象相关联的。

2.构造器和多态 

  尽管构造器并不具有多态性(它们实际上是static方法),但还是非常有必要理解构造器怎样通过多态在复杂的层次结构中运作。

  ※①构造器的调用顺序

  基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确地构造。导出类只能访问它自己的成员,不能访问基类中的成员(基类成员通常是private类型)。只有基类的构造器具有恰当的知识和权限来对自己的元素进行初始化。因此,必须令所有构造器都得到调用,否则就不可能正确构造完整对象。这正是为什么编译器要强制每个导出类都必须调用构造器的原因。在导出类的构造器主体中,如果没有明确指定调用某个基类构造器,它都会“默默”地调用默认构造器。如果不存在默认构造器,编译器就会报错(若某个类没有构造器,编译器会自动合成出一个默认构造器)。

  看下面这个例子,它展示组合、继承以及多态的构建顺序:

class Meal {
    Meal() { System.out.println("Meal()"); }
}
class Bread {
    Bread() { System.out.println("Bread()"); }
}
class Cheese {
    Cheese() { System.out.println("Cheese()"); }
}
class Lettuce {
    Lettuce() { System.out.println("Lettuce()"); }
}
class Lunch extends Meal {
    Lunch() { System.out.println("Lunch()"); }
}
class ProtableLunch extends Lunch {
    ProtableLunch() { System.out.println("ProtableLunch()"); }
}

public class Sandwich extends ProtableLunch {
    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()
*/

  上面的输出结果说明调用构造器要遵循下面的顺序:

  1)调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最低层的导出类。

  2)按声明顺序调用成员的初始化方法。

  3)调用导出类构造器的主体。

  在构造器内部,我们必须确保所要使用的成员都已经构建完成。

  ②继承与清理

  如果我们有其他作为垃圾回收一部分的特殊清理动作,就必须在导出类中覆盖dispose()方法。当覆盖被继承类的dispose()方法时,务必记住调用基类版本dispose()方法;否则基类的清理动作就不会发生。

  销毁的顺序应该与初始化顺序相反。

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

  构造器调用的层次结构带来了一个有趣的两难问题——如果在一个构造器的内部调用正在构造的对象的某个动态绑定方法,那会发生什么情况呢?

  在一般的方法内部,动态绑定的调用时在运行时才决定的,因为对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。

  如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。然而,这个调用的效果可能难以预料,因为被覆盖的方法在对象被完全构造前就会被调用,这可能会造成一些难以发现的隐藏错误。

  上面介绍的初始化顺序并不完整,而这正是解决这个问题的关键。初始化的实际过程是:

  1) 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。

  2) 如前述那样调用基类构造器。

  3) 按照声明的顺序调用成员的初始化方法。

  4) 调用导出类的构造器主体。

  这样的优点是所有东西都至少初始化成“零”。

  编写构造器时有一条有效的准则:“用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法。”在构造器中唯一能够安全调用的那些方法是基类中的final方法(也适用于private方法)。

3.协变返回类型

  在Java SE5中添加了协变返回类型,它表示导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型:

class Grain {
    public String toString() { return "Grain"; }
}
class Wheat extends Grain {
    public String toString() { return " Wheat" }
}

class Mill {
    Grain process() { return  new Wheat(); }
}
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
*/

4.用继承进行设计

  更好的方式是首先选择“组合”。组合更加灵活,因为它可以动态选择类型(因此就选择了行为);相反,继承在编译时就需要知道确切类型。

class Actor {
    public void act() {}
}
class HappyActor extends Actor {
    public void act() { System.out.println("HappyActor"); }
}
class SadActor extends Actor {
    public void act() { System.out.println("SadActor"); }
}

class Stage {
    private Actor actor = new HappyActor();
    public void change() { actor = new SadActor(); }
    public void performPlay() { actor.act(); }
}

public class Transmogrify {
    public static void main(String[] args) {
        Stage stage = new Stage();
        stage.performPlay();
        stage.change();
        stage.performPlay();
    }
}
/*Output
HappyActor
SadActor
*/

  Stage对象含有一个对Actor的引用,并可以在运行时改变实际对象,然后performPlay()产生的行为也随之改变。这样一来,我们在运行期间获得了动态灵活性(这也称为状态模式)。

  ①纯继承与扩展

  纯继承是“is-a”关系。导出类具有和基类一样的接口,且基类可以接收发送给导出类的任何信息。

  扩展是“is-like-a”关系,因为导出类就像是一个基类——它有着相同的基本接口,但是它还具有由额外方法实现的其他特性。导出类中接口的扩展部分不能被基类访问,因此,一旦我们向上转型,就不能调用那些新方法。

  ②向下转型与运行时类型识别

  由于向上转型会丢失具体的类型信息,所以我们就想,通过向下转型应该能够获取类型信息,在Java语言中,所有转型都会得到检查。如果类型不符,就会返回一个ClassCastException。这种在运行期间对类型进行检查的行为称作“运行时类型识别”(RTTI)。

5.总结   多态意味着“不同的形式”。在面向对象的程序设计中,我们持有从基类继承而来的相同接口,以及使用该接口的不同形式:不同版本的动态绑定方法。

原文地址:https://www.cnblogs.com/fht-litost/p/8410913.html

时间: 2024-10-16 04:00:59

Java编程思想 学习笔记8的相关文章

【Java编程思想--学习笔记(一)】访问控制-包

Java编程思想–学习笔记(一) 访问控制(或隐藏具体实现)与"最初的实现并不恰当"有关. 1.包:库单元 我们之所以要导入包,就是要提供一个管理名字的空间机制. 每个java文件只能有一个public类.其他私有类为主public·类提供支持. 1.1 代码组织 与编译型语言不同,java可运行程序是一组可以打包并压缩成java文档文件(JAR,使用Java的jar文档生成器)的.class文件. 使用package和import关键字,使得不会出现名称冲突问题. 注意:java包的

JAVA编程思想学习笔记——第一章 对象导论

搞了一年多java,野路子出身,发现java基础这块还是相当的薄弱!故决定学习<Java编程思想>这本书.在此把学习的知识点记录下! 面向对象的五大特性 1.万物皆为对象 2.程序是对象的集合,它们通过发送消息来告诉彼此所要做的 3.每个对象都由自己的由其它对象所构成的存储 4.每个对象都拥有其类型 5.某一特定类型的所有对象都可以接收同样的信息  单根继承结构 所有的类都继承自单一的基类,Object.在单根集成结构中的所有对象都具有一个公用接口,所以他们归根到底都是相同的基本类型.单根集成

[Java编程思想-学习笔记]第3章 操作符

3.1  更简单的打印语句 学习编程语言的通许遇到的第一个程序无非打印"Hello, world"了,然而在Java中要写成 System.out.println("Hello, world"); 我们都会感觉太冗长了,能不能简单一些呢?静态导入可以省略掉System,就像这样 import static java.lang.System.*; public class Hello { public static void main(String[] args) {

【java编程思想--学习笔记(四)】对象导论

写这篇博客的前言: 长话短说,我希望通过阅读<java编程思想>来使我的代码 简洁可用 . 目的的层次不同,首先具体的目标是,了解Java的特性和巩固Java的基础. 更抽象的目的如下: 1.期待以巩固基础的方式,使代码优美,简洁,高效. 2.使自己写的模块能够开放适度,好用. 3.形成一种对代码是否优美的审美观. 于是<Java编程思想>第一章 对象导论 由此开始. 1.1 抽象过程 java 相对于命令式语言的优势在于只针对于待解问题建模.后者所做的主要抽象要求所做问题基于计算

Java编程思想 学习笔记1

一.对象导论 1.抽象过程 Alan Kay曾经总结了第一个成功的面向对象语言.同时也是Java所基于的语言之一的Smalltalk的五个基本特性,这些特性表现了纯粹的面向对象程序设计方式 1)万物皆对象. 2)程序是对象的集合,它们通过发送消息来告知彼此所要做的.要想请求一个对象,就必须对该对象发送一条消息.更具体的说,可以把消息想象为对某个特定对象的方法的调用请求. 3)每个对象都有自己的由其他对象所构成的存储.换句话说,可以通过创建包含现有对象的包的方式来创建新类型的对象. 4)每个对象都

[Java编程思想-学习笔记]第1章 对象导论

1.1  抽象过程 Java是一门面向对象的语言,它的一个优点在于只针对待解问题抽象,而不用为具体的计算机结构而烦心,这使得Java有完美的移植性,也即Java的口号"Write Once, Run Anywhere". 所谓的抽象过程,可以理解为对待解问题建模.比如待解问题是一个人,那么我们可以对人进行建模,它的类型是人,有属性姓名.性别.年龄,还有行为吃饭.走路.Java能直接完全据此建模编码,而无需考虑具体的计算机结构.所以当我们阅读Java程序时,正如书上说的"当你在

java编程思想学习笔记

1.equals和==的区别(P65) java主要有两类数据类型: 基本数据类型(原始数据类型),主要有八种:byte,char,short,int,long,float,double,boolean   他们之间的比较用"==",比较的是他们的值 复合数据类型   若用"=="对他们进行比较,比较的是他们在内存中存放的地址,也就是他们的句柄,除非他们是同一个对象,他们"=="的比较结果为true,否则为false. "=="

【java编程思想--学习笔记(三)】访问控制-接口实现与类的访问权限

接口实现 什么是数据类型? java中将数据和方法包装在类中,隐藏具体的实现,其结果就是一种数据类型.(封装产生数据类型) 联想到八大基本数据类型,都具有上述的特点. 由于数据类型的上述特点,创造者将权限的边界划在数据类型的内部,将希望被访问的数据和方法与不希望被访问到的方法和数据分开,隐藏不希望被访问到的方法和数据,实际上就实现了接口和具体实现的分离. 也就是说,了解一个类如何去用,懂得其内部公开的变量和方法就可以. 类的访问权限 上面讲的都是将访问权限设置在类的内部,也可以将访问权限修饰词放

Java编程思想 学习笔记12

十二.通过异常处理错误  Java的基本理念是“结构不佳的代码不能运行”. Java中的异常处理的目的在于通过使用少于目前数量的代码来简化大型.可靠的程序的生成,并且通过这种方式可以使你更加自信:你的程序中没有未处理的错误. 1.概念 C以及其他早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础上,而并不属于语言的一部分.通常会返回某个特殊值或者设置某个标志,并且假定接收者将对这个返回值或标志进行检查,以判定是否发生了错误.然而,对于构造大型.健壮.可维护的程序而言,这种错误处理

【java编程思想--学习笔记(二)】访问控制-Java访问权限修饰词

如果不提供任何访问修饰词,则意味着它是"包访问权限". 2.1 包访问权限 包访问权限赋予包内的类相互访问彼此成员的权限. 应该说, 包访问权限为将类群聚在一起的行为提供了意义和理由,即建立包的目的不仅仅是为了分类和区分,更是为了是同一个包内的类可以拥有彼此的代码. 取得对某一成员访问权的途径: 1)该成员的访问修饰词为public. 2)通过不加访问权限修饰词并将目标类放在同一包内的方式. 3)继承.子类可以访问父类的public和protected修饰词的成员,但只有在父子类处于同