热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

TJI读书笔记11多态

TJI读书笔记11-多态再说说向上转型多态的原理构造器和多态协变返回类型使用继承进行设计多态是数据抽象和继承之后的第三种基本特征.一句话说,多态分离了做什么和怎么做(再次对埃大爷佩

  • TJI读书笔记11-多态
    • 再说说向上转型
    • 多态的原理
    • 构造器和多态
    • 协变返回类型
    • 使用继承进行设计

多态是数据抽象和继承之后的第三种基本特征. 一句话说,多态分离了做什么怎么做(再次对埃大爷佩服的五体投地,简直精辟啊). 是从另外一个角度将接口和实现分离开来.
封装通过合并特征和行为来创建新的数据对象,通过私有化隐藏细节,把接口和实现分离开来. 多态则是消除类型之间的耦合关系. 继承是允许将对象视为自己本身的类型或者基类型来处理.

再说说向上转型

把某个对象的引用视为对其基类型的引用的做法被称为向上转型. 为什么老是说向上转型呢,因为在UML图中,一般基类都在子类的上面…
多态是个什么样子

class Instrument{
public void paly(Note m){
System.out.println("Instrument play()");
}
}
class Wind extends Instrument{
public void paly(Note m){
System.out.println("Wind play()");
}
}

public class Music {
public static void tune(Instrument i){
i.paly(Note.C_SHARP);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute);
}
}

这里针对tune方法,传入的类型是Instrument,而实际执行的时候传入的是Wind,不但没有报错,而且还得到了正确的结果. 这是哪位天使大姐发力了吗? 显然不是的. 这就是多态. 也就是说,我们只需要在基类中定义一个方法,在子类中如果该方法被重写了,那么,在调用相关方法的时候,编译器会自动的去调用子类中重写的方法. 这就是传说中的多态.

没有多态的时候如何实现上面的效果
如果没有多态的特性,我们想达到上面的效果也是有办法的.可以使用方法重载. 比如,可以在music类中定义多个tune方法.public static void tune(wind i),public static void tune(Stringed i),public static void tune(Brass i)等等等. 这样就可以通过方法重载来实现类似这样的效果. 但是,作为一个正常的人类,不觉得这玩意儿有点反人类吗…

多态的原理

为什么会有多态这种现象,这就要聊到程序的方法运行绑定问题了. 绑定就是将一个方法调用与方法主题关联起来的动作. 像C语言这样的程序设计语言都是用一种叫做前期绑定的方法.也就是说在程序执行之前就要做绑定. 一般都是由编译器和连接器实现的. 也就是说程序在运行的时候已经固定了方法的入参类型了. 如果有错的话,就报错呗.

后来啊 ,改革开放过后(这只是一句口头禅,没有具体时间意义),出现了一种叫做后期绑定或者叫动态绑定又或者叫运行时绑定的技术. 也就是说,存在一种在运行的时候去判断对象类型的机制. 编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用. 不同语言的实现不一样.

比如,在子类中的那个基类的子集里的方法调用的指针被更改到了子类中重写的方法. 那在执行tune方法的时候,先讲Wind类型向上转型成Instrument类型,再调用的时候,就会直接去调用Wind类型中定义的方法体了. C++就是这样实现的,每个对象都会维护一张虚表,虚表中的虚函数指针默认初始化为自身(黑色箭头所指)然后通过一个virtual关键字来指定该函数为虚函数,如果出现上述这种调用方式的时候,编译器会讲该虚函数指针指向子类中重写的方法(红色箭头所指). (这是个人的理解啊,我比较菜,还没有看到哪个权威的文档上说java是这样实现的…)

技术分享

java中static和final的方法(private的方法默认都是final的,所以也算),其他的方法都是动态绑定的. 又说到final方法了,final的方法会被关闭动态绑定. 这样的话,编译器在效率上可能会高一点. 但是不好说. 所以这不能作为使用final修饰方法的一个理由. 还是那句话,通过使用final修饰方法来优化程序是不靠谱的. final使用的唯一考量就是设计需要.

多态带来的可扩展性
有了多态机制之后,带来了一个好处,那就是在使用一个类的时候,可以只与基类的接口通信,这样也可以达到我们想要的结果. 那么程序的可扩展性就大大增强了. 多态带来的思想是,将改变的事物(方法实现)和没有改变的事物(方法签名)分离开来.

哪些方法是可以被覆盖的
首先,private的方法肯定是不可以的. private是被封装在类内部的,无法在类的外部调用. 在实例中都没有办法直接调用,那就没有多态可谈了.
其次,final的方法肯定也是不行的,final的方法不允许被重写.

关于private有一个问题,如果在子类中有一个跟基类的某个private方法名字一毛一样的方法,这算什么?什么也不算,不构成重写,只是看作是基类中的一个普通的方法而已.

最后,static的方法,static方法属于类本身. 不存在重写的问题. 也就不存在多态了. 在上面的原理中已经讲的很清楚了,多态发生在实例方法的调用中,对于静态方法是不构成多态的.

简单来说,构成多态需要两个条件.

  • 有继承关系
  • 子类中重写父类的方法.

构造器和多态

构造器是static的方法,虽然是隐式的.

class Meal{
Meal(){System.out.println("Meal()");}
}

class Bread{
Bread(){System.out.println("Bread()");}
}

class Lettuce{
Lettuce(){System.out.println("Lettuce()");}
}
class Cheese{
Cheese(){System.out.println("Cheese()");}
}
class Lunch extends Meal{
Lunch(){System.out.println("Lunch()");}
}
class ProtableLunch extends Lunch{
public 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();
}
}

一个小例子来完善一下类的初始化过程.

  • 调用基类的构造器,这个过程会一直反复递归下去,显示从根基类开始,一层层的往下找.
  • 按照声明的顺序初始化成员.
  • 调用导出类的构造器主体

继承和清理动作
前面说过,垃圾清理可能不会被调用而且仅仅清理堆上的实例对象. 有时候如果希望更多更及时的清理就需要我们自己书写一个自定义的清理方法. 那么在继承的过程中. 千万不要忘了,如果在子类中重写了这个清理方法的话,在方法内一定要通过super来调用基类的清理方法. 不然,这个方法被覆盖了,基类的清理方法中定义的动作就得不到执行.
还有一点就是,清理过程和初始化过程是相反的. 会先调用自身的清理方法,再调用基类的清理方法.

还有一个问题,如果成员对象中存在于其他一个或者多个对象共享的情况,需要显式的使用引用计数器来跟踪仍旧访问共享对象的对象的数量. 不能随意清理.
来吧,一码解千愁

class Shared{
private int refcount =0;
private static long counter = 0;
private final long id = counter++;
public Shared(){
System.out.println("Ceeating "+this);
}
public void dispose(){
if(--refcount==0){
System.out.println("dispose "+this);
}
}
public void addRef(){
refcount++;
}
public String toString(){
return "shared "+id;
}
}

class Composing{
private Shared shared;
private static long counter = 0;
private final long id = counter++;
public Composing(Shared shared){
System.out.println("Creating "+ this);
this.shared = shared;
this.shared.addRef();
}
protected void disposed(){
System.out.println("disposing "+this);
shared.dispose();
}
public String toString(){
return "Composing "+id;
}

}
public class ReferenceCounting {
public static void main(String[] args) {
Shared shared = new Shared();
Composing[] composing = {new Composing(shared),new Composing(shared),new Composing(shared),new Composing(shared)};
for(Composing c:composing){
c.disposed();
}
}
}/*out:
Ceeating shared 0
Creating Composing 0
Creating Composing 1
Creating Composing 2
Creating Composing 3
disposing Composing 0
disposing Composing 1
disposing Composing 2
disposing Composing 3
dispose shared 0
*/

这个例子里使用了引用计数器. 只有当引用计数器归零的时候,才清除shared对象.

构造器内部的多态方法的行为
按道理来说,这种代码是不应该存在的. 构造器用来创建和初始化对象. 而多态的行为是属于实例方法的. 也就是说多态的正常应用场景应该是在对象构建完成之后. 那么,如果在构造器中去调用一个实例方法,在构造的工程中应用多态会发生什么呢?

class Glyph{
void draw(){
System.out.println("Glyph.draw()");
}
Glyph(){
System.out.println("Glyph start");
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
System.out.println("Glyph end");
}
}

class RoundGlyph extends Glyph{
private int radius =1;
public RoundGlyph(int r) {
System.out.println("RoundGlyph start");
radius = r;
System.out.println("RoundGlyph.RoundGlyph().radius = "+ radius);
System.out.println("RoundGlyph end");
}
void draw(){
System.out.println("RoundGlyph.draw().radius = "+radius);
}
}

public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}/*out:
Glyph start
Glyph() before draw()
RoundGlyph.draw().radius = 0
Glyph() after draw()
Glyph end
RoundGlyph start
RoundGlyph.RoundGlyph().radius = 5
RoundGlyph end
*/

首先在基类的构造方法中调用了draw()方法,那么在new RoundGlyph实例的时候,RoundGlyph的构造器会先执行基类的构造器. 所以Glyph()被调用了.而在Glyph()里调用的drwa()方法,那么由于多态,会执行RoundGlyph类中的draw,而这个时候,RoundGlyph实例并没有构造完成. 所以得到的值与期望值不一样. 那为什么这里会是0呢?
记得之前在初始化那一节的时候说过初始化的顺序:

  • 分配内存空间并初始化为二进制的0
  • 调用基类的构造器
  • 按照声明顺序调用成员的初始化方法
  • 调用子类的构造器.

那么,在Glyph()被调用的时候,第一步的初始化已经完成了. 所以,我们看到的是个0;这么做有个好处,在任何方法(包括构造方法)执行前,所有东西都已经被初始化为”空”了. 至少保证是有初始化状态的,如果出了问题,无论是报异常还是结果出错都相对来说都比较方便定位问题.

上面这个例子可能很多人都不会想到,因为大家一直都默认一条规定:构造器中使用尽可能简单的方法完成初始化,让对象进入正常状态就可以了. 如果有可能,避免在构造器中调用其他方法.

协变返回类型

之前说的多态都只解决了一个问题,对于子类中被覆盖的基类方法,入参类型不同可以构成多态. 那多态中的方法的返回值会有什么改变吗?

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 ConvarianReturn {
public static void main(String[] args) {
Mill m = new Mill();
Grain g = new Grain();
System.out.println(g);
m = new WheatMill();
g = m.process();
System.out.println(g);
}
}/*out:
Grain
Wheat
*/

在java SE1.5之前,重写基类方法的时候 返回值必须跟基类中方法的一致. 1.5之后,放宽了这个限制. 返回值可以是基类中被覆盖方法的返回值的子类. 也就是说可以是更具体的类型了.

使用继承进行设计

之前已经说过,在设计的时候,组合的方式是首选的考量. 组合更加灵活,而且还有多态的特性可以为它动态的选择类型.而继承必须在编译是就知道确切的类型. 一般来说的准则是用继承来表达行为见的差异,并且用字段来表达状态上的变化.

状态模式

class Leg{
public void walk(){}
}

class SoberLeg extends Leg{
public void walk(){
System.out.println("walk in line");
}
}

class DrunkenLeg extends Leg{
public void walk(){
System.out.println("zzz...");
}
}

class Person{
private Leg legs= new SoberLeg();
public void change() {legs = new DrunkenLeg();}
public void walkWithLegs(){legs.walk();}
}
public class PersonTest {
public static void main(String[] args) {
Person p = new Person();
p.walkWithLegs();
p.change();
p.walkWithLegs();
}
}

这个例子的设计不太合理,但是能表示那个意思. 就是使用组合的时候,完全可以在运行时,把一个对象的引用指向另外一个对象,然后时其行为发生改变.

纯继承,扩展和向下转型
最理想的继承方式应该是,子类和基类有一毛一样的接口. 也就是说是一种纯粹的”is-a”的关系. 也可以认为这是一种纯替代.
在这种状态下多态可以处理一切的事情.
但是理想永远是人们的一种美好的可望不可及的期待,java中继承的关键字是extends,这也说明,继承更多时候需要的是扩展基类接口. 也就是说子类是基类的一个超集. 这种关系可以说是一种 “is-like-a”的关系. 在这种关系里,向上转型的话,那子类中扩展的方法是无法使用的.同时带来了向下转型的风险. 使用”()”的强转方式依旧可以向下转型. java在运行时会做类型检查,如果不是我们期望的类型,会报ClassCastException.

TJI读书笔记11-多态


推荐阅读
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文介绍了在SpringBoot中集成thymeleaf前端模版的配置步骤,包括在application.properties配置文件中添加thymeleaf的配置信息,引入thymeleaf的jar包,以及创建PageController并添加index方法。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • CentOS 6.5安装VMware Tools及共享文件夹显示问题解决方法
    本文介绍了在CentOS 6.5上安装VMware Tools及解决共享文件夹显示问题的方法。包括清空CD/DVD使用的ISO镜像文件、创建挂载目录、改变光驱设备的读写权限等步骤。最后给出了拷贝解压VMware Tools的操作。 ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
  • Week04面向对象设计与继承学习总结及作业要求
    本文总结了Week04面向对象设计与继承的重要知识点,包括对象、类、封装性、静态属性、静态方法、重载、继承和多态等。同时,还介绍了私有构造函数在类外部无法被调用、static不能访问非静态属性以及该类实例可以共享类里的static属性等内容。此外,还提到了作业要求,包括讲述一个在网上商城购物或在班级博客进行学习的故事,并使用Markdown的加粗标记和语句块标记标注关键名词和动词。最后,还提到了参考资料中关于UML类图如何绘制的范例。 ... [详细]
author-avatar
禾漾啊
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有