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.总结 多态意味着“不同的形式”。在面向对象的程序设计中,我们持有从基类继承而来的相同接口,以及使用该接口的不同形式:不同版本的动态绑定方法。
Java编程思想 学习笔记8的更多相关文章
- [Java编程思想-学习笔记]第3章 操作符
3.1 更简单的打印语句 学习编程语言的通许遇到的第一个程序无非打印"Hello, world"了,然而在Java中要写成 System.out.println("He ...
- Java编程思想 学习笔记1
一.对象导论 1.抽象过程 Alan Kay曾经总结了第一个成功的面向对象语言.同时也是Java所基于的语言之一的Smalltalk的五个基本特性,这些特性表现了纯粹的面向对象程序设计方式 1)万物皆 ...
- [Java编程思想-学习笔记]第1章 对象导论
1.1 抽象过程 Java是一门面向对象的语言,它的一个优点在于只针对待解问题抽象,而不用为具体的计算机结构而烦心,这使得Java有完美的移植性,也即Java的口号"Write Once, ...
- Java编程思想 学习笔记11
十一.持有对象 通常,程序总是根据运行时才知道的某些条件去创建新对象.在此之前,不会知道所需对象的数量,甚至不知道确切的类型. Java实用库还提供了一套相当完整的容器类来解决这个问题,其中基本的类 ...
- Java编程思想学习笔记——类型信息
前言 运行时类型信息(RTTI:Runtime Type Information)使得我们可以在程序运行时发现和使用类型信息. Java在运行时识别对象和类的信息的方式: (1)一种是RTTI,它假定 ...
- Java编程思想 学习笔记12
十二.通过异常处理错误 Java的基本理念是“结构不佳的代码不能运行”. Java中的异常处理的目的在于通过使用少于目前数量的代码来简化大型.可靠的程序的生成,并且通过这种方式可以使你更加自信:你的 ...
- Java编程思想 学习笔记10
十.内部类 可以将一个类的定义放在另一个类的定义内部,这就是内部类. 内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可视性.然而必须要了解,内部类和组合是 ...
- Java编程思想 学习笔记7
七.复用类 1.组合语法 在新的类中产生现有类的对象.由于新的类是由现有类的对象所组成,所以这种方法叫做组合. 类中域为基本类型时能够自动被初始化为零.对象引用被初始化为null. 编译器不是简单地为 ...
- Java编程思想 学习笔记5
五.初始化与清理 1.用构造器确保初始化 在Java中,通过提供构造器,类的设计者可确保每个对象都会得到初始化.创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造 ...
- Java编程思想 学习笔记4
四.控制执行流程 1.true和false 所有条件语句都利用条件表达式的真或假来决定执行路径.注意Java不允许我们将一个数字作为布尔值使用. 2.if-else 3.迭代 while.do-whi ...
随机推荐
- MySQL的Blob类型的手工编辑(manually edit)
https://dba.stackexchange.com/questions/17593/in-mysql-how-to-manually-edit-values-of-a-blob-column- ...
- JQuery插件:图片上传本地预览插件,改进案例一则。
/* *名称:图片上传本地预览插件 v1.1 *作者:周祥 *时间:2013年11月26日 *介绍:基于JQUERY扩展,图片上传预览插件 目前兼容浏览器(IE 谷歌 火狐) 不支持safari *插 ...
- Docker(十四)-Docker四种网络模式
Docker 安装时会自动在 host 上创建三个网络,我们可用 docker network ls 命令查看: none模式,使用--net=none指定,该模式关闭了容器的网络功能. host模式 ...
- String()与toString的区别
1..toString()可以将所有的的数据都转换为字符串,但是要排除null 和 undefined 代码示例: var a = null.toString()--报错 var b = underf ...
- (转)vmware下给linux虚拟机扩容
“Well, here’s another fine mess you’ve gotten me into” Let us pretend that you have an Ubuntu Server ...
- java程序在windows系统作为服务程序运行
Java程序很多情况下是作为服务程序运行的,在Un*x 平台下可以利用在命令后加“&”把程序作为后台服务运行,但在Windows下看作那个Console窗口在桌面上,你是否一直担心别的同时把你 ...
- 在腾讯云&阿里云上部署JavaWeb项目(Tomcat+MySQL)
之前做项目都是在本地跑,最近遇到需要在在云服务器(阿里云或者腾讯云都可以,差不多)上部署Java Web项目的问题,一路上遇到了好多坑,在成功部署上去之后写一下部署的步骤与过程,一是帮助自己总结记忆, ...
- matlab数据导入verilog仿真
Matlab中的fopen和fprintf函数可以生成txt格式文件,并将波形数据以 %d 整数 %e 实数:科学计算法形式 %f 实数:小数形式 %g 由系统自动选取上述两种格式之一 %s 输出字符 ...
- Spark_RDD之简单Java函数接口
函数名 实现的方法 用途 Function<T, R> R call(T) 接收一个输入值并返回一个输出值,用于类似 map() 和filter() 等操作中 Function2<T ...
- android 组件使用()
程序入口点 类似于win32程序里的WinMain函数,Android自然也有它的程序入口点.它通过在AndroidManifest.xml文件中配置来指明,可以看到名为NotesList的activ ...