个人主页:十二月的猫-CSDN博客
 系列专栏: 面向对象修炼手册

 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光 

目录

前言

消息传递

1 基本概念

1.1 概念

 2 消息表达式

3 消息机制特性

4 java中的消息传递

动/静态类型语言

1 动态类型语言

2 静态类型语言

3 区别与联系

伪变量 

继承

1 子类与父类

1.1 子类与父类的关系限制

1.2 子类与父类的替换原则

1.3 改写(重写)(覆盖)

1.4 继承形式

1.4.1 特化继承

1.4.2 规范继承

1.4.3 构造继承

1.4.4  泛化继承

1.4.5  扩展继承

1.4.6  限制继承

1.4.7  变体继承

1.4.8  合并继承

1.5 构造函数在继承中

1.5.1 构造函数的调用

1.5.2 构造函数的继承

1.5.3 子类与父类的构造函数

1.5.4 子类、父类构造例题:

总结


前言

上文介绍了类和对象的基础概念,然后深入研究了面向对象编程这个词语的深层含义。

简单复习一下:

面向对象方法的本质:

  • 找代理
  • 消息传递给代理
  • 代理解决问题
  • 我的问题就解决了

面向对象:只关注对象,不关注过程;对象包括:对象和实例对象

对于C语言这种面向过程的语言,在编程中,程序员关注的重点是过程、是解决问题的方法、是一个个函数

面向对象语言,在编程中,程序员关注的重点转变为对象

面向对象编程:通过对象间的消息传递解决任务,完成编程

编程的目的是解决问题;

面向过程的语言直接关注解决问题的方法解决问题

面向对象的语言关注解决问题的对象,通过消息传递解决问题

本篇重点来讲消息机制继承

消息机制:细心的小伙伴应该已经发现:消息机制上节课就已经提到过了!没错,消息机制就是面向对象编程中实现相应功能的方法(面向对象编程完成功能都靠对象,而对象完成功能需要消息的传递)。没有消息传递,再有手段的对象也无法完成功能(因为它压根不知道要完成什么功能、有什么要求)

继承:继承自然也是非常重要的。面向对象编程有许多的优点,其中最重要的一点就是代码复用,能够重复使用一段完成特定功能的代码。进一步思考:为什么代理人解决就比全部自己解决更容易?深层次的原因在于:使用代理人解决问题,代表这一类的问题都可以用代理人来解决,不需要每次换算法都要从头实现每一个方法(代码复用性很好)。

继承作为C++三大特性之一,其就体现了代码复用的思想(例如:男人、女人都继承自人类,那么男女人都有作为人的特征。因此男女人编程时可以直接使用人中的特性和方法,不需要改动原本的代码细节(仅仅加入人这一类的各个方法接口即可)

消息传递

消息传递:一个对象通知另一个已经完成声明的对象需要进行的特定行为的机制

本质:调用一个类的方法,在方法中给需要传递的形参

1 基本概念

1.1 概念

对象间相互请求相互协作完成某一任务的途径

  • 对象接收多个消息,响应不同
  • 同一消息给多个对象,响应不同

 2 消息表达式

  1. 消息接受者:消息传递的目的函数,消息的接受对象
  2. 消息选择器:消息传递系统中的过滤器,用于让消息接收者接受并处理特定性的消息
  3. 参数(消息内容) :实际传递给接收者的信息(需要经过选择器的过滤和限定)

3 消息机制特性

封装:

封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法。保护类受到不必要的修改

解耦合:

解除了消息发送者对任务完成过程的耦合,消息发送者不需要具体知道任务是如何完成的。仅仅需要负责发送消息并等待消息接收者的响应即可

消息传递中的重点概念辨析:

1、消息发送的对象是:消息接收器

2、消息接受器过滤后送给消息接收者

3、过程调用是非面向对象编程中的复用方法,其没有接收器

4、 响应消息所执行的行为并不是一成不变的,他们根据接收器类的不同而不同。(响应行为随接收器不同而不同) 

4 java中的消息传递

aCard.flip ();
aCard.setFaceUp(true);
aGame.displayCard(aCard, 45, 56);

/静态类型语言

动/静态类型语言的区别就在对于:变量类型是动态的还是静态的

动态:在运行时绑定

静态:在编译时绑定 

1 动态类型语言

定义:数据类型是动态的(动态绑定、运行时绑定、自动匹配)

  • 类型的检查是在运行时做的;
  • 变量不需要声明类型,程序在运行时自动确定
  • 优点是方便阅读
  • 缺点是不利于调试

2 静态类型语言

定义:数据类型是静态的(静态绑定、编译时绑定、人工分配)

  • 类型检查是编译时确定
  • 变量需要明确类型
  • 优点是结构规范,利于调试
  • 缺点是代码不够简洁

3 区别与联系

  • 动态类型语言与静态类型语言之间的差异在于变量或数值是否具备类型这种特性。
  • 静态类型语言在编译时做出内存分配,动态类型语言在运行时才分配内存

伪变量 

大多数面向对象语言中,接收器并不出现在方法的参数列表中,而是隐藏于方法的定义之中。只有当必须从方法体内部去存取接收器的数值时,才会使用伪变量(pseudo-variable)

Java,C++:this

伪变量在使用时就好像作为类的一个实例

在理解上应该认为this是:接收器留给所有选择器的接口

Java:构造函数中,使用this区分参数和数据成员

public class ThisTest {
    private int i=0;
    //第一个构造器:有一个int型形参
    ThisTest(int i){
       this.i=i+1;//此时this表示引用成员变量i,而非函数参数i
       System.out.println("Int constructor i——this.i:  "+i+"——"+this.i);
       System.out.println("i-1:"+(i-1)+"this.i+1:"+(this.i+1));
       //从两个输出结果充分证明了i和this.i是不一样的!
    }
}

继承

1 子类与父类

1.1 子类与父类的关系限制

  • 子类实例拥有父类的所有数据成员。(私有成员变量也会拥有,但是无法访问)
  • 子类的实例必须至少通过继承实现父类所定义的所有功能

1.2 子类与父类的替换原则

指如果类B是类A的子类,那么在任何情况下都可以用类B来替换类A,而外界毫无察觉(这里强调的是语法上可行,和里氏替换原则有一定的重合)

这里的子类是指符合替换原则的子类关系(形式上继承,行为上也继承

1.3 改写(重写)(覆盖)

子类有时为了避免继承父类的行为,需要对其进行改写

语法上:子类定义一个与父类有着相同名称且类型签名相同的方法

运行时:变量声明为一个类,它所包含的值来自于子类,与给定消息相对应的方法同时出现于父类和子类

改写发生下的实际调用: 

改写发生在:

1、父子类之间

2、同姓名、同标签方法上

1.4 继承形式

继承的目的就是复用代码,但是继承的形式存在区别,存在:1、特殊化;2、规范化;3、构造化;4、泛化两种类型

1.4.1 特化继承

很多情况下,都是为了特殊化才使用继承。在这种形式下,新类是基类的一种特定类型,它能满足基类的所有规范。 用这种方式创建的总是子类型,并明显符合可替换性原则。与规范化继承一起,这两种方式构成了继承最理想的方式,也是一个好的设计所应追求的目标。
例如:
从马派生出白马:从抽象的马,到具有具体颜色(白色)的马,增加了颜色这个属性。
从人派生出男人:从抽象的人,到具有具体性别(男性)的人,增加了性别这个属性。

1.4.2 规范继承

规范化继承用于保证派生类和基类具有某个共同的接口,即所有的派生类实现了具有相同方法界面的方法。基类中既有已实现的方法,也有只定义了方法接口、留待派生类去实现的方法。派生类只是实现了那些定义在基类却又没有实现的方法

在Java中,关键字abstract确保了必须要构建派生类。声明为abstract的类必须被派生类化,不可能用new运算符创建这种类的实例。除此之外,方法也能被声明为abstract,同样在创建实例之前,必须覆盖类中所有的抽象方法。规范化继承可以通过以下方式辨认:基类中只是提供了方法界面,并没有实现具体的行为,具体的行为必须在派生类中实现。GraphicalObject没有实现关于描绘对象的方法,因此它是一个抽象类。其子类Ball,Wall和Hole通过规范子类化实现这些方法

最理想的继承:特殊化继承、规范化继承

1.4.3 构造继承

一个类可以从其基类中继承几乎所有需要的功能,只是改变一些用作类接口的方法名,或是修改方法中的参数列表

即使新类和基类之间并不存在抽象概念上的相关性,这种实现也是可行的

例如:

树-独木舟 保留了材质等属性,修改了形状的属性

堆栈-队列 修改了pop() push()等方法,栈顶和栈底变成了队首和队尾

写二进制文件-写学生信息文件

当继承的目的只是用于代码复用时,新创建的子类通常都不是子类型(违反里氏替换原则)。这称为构造子类化。一般为了继承而继承,如利用一些工具类已有的方法

1.4.4  泛化继承

派生类扩展基类的行为,形成一种更泛化的抽象。不完全等同于特殊化继承的反面。是从基类扩展一部分行为,形成更加泛化的抽象。在程序中表现为对原来存在的功能进行修改或者扩展

子类对基类功能进行修改、拓展。其功能的范围比基类更大,并不一定包含。因此不符合可替换原则

可替换原则: 

  1. 子类可以实现父类的抽象方法,但不能覆盖(改写)父类的非抽象方法。
  2. 子类中可以增加自己特有的方法。
  3. 当子类的方法重写父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

前两者保证逻辑上可替换,后两者保证语法上可替换

从Window派生出了Colored_Window。
public Class Window{
...
    double size;
    setSize(){...}
    getSize(){...}
}
public Class Colored_Window extends Window{
...
    String color;
    setColor(String str){...};
    getColor(String str){...};
//扩展了查看颜色和设置颜色两个行为
}

1.4.5  扩展继承

如果派生类只是往基类中添加新行为,并不修改从基类继承来的任何属性,即是扩展继承。(泛化子类化对基类已存在的功能进行修改或扩展,扩展子类化则是增加新功能

由于基类的功能仍然可以使用,而且并没有被修改,因此扩展继承并不违反可替换性原则,用这种方式构建的派生类还是派生类型 

举例:学生(父类)、会弹琴的学生(子类)

1.4.6  限制继承

如果派生类的行为比基类的少或是更严格时(违反里氏替换原则),就是限制继承。

常常出现于基类不应该、也不能被修改时。

限制继承可描述成这么一种技术:它先接收那些继承来的方法,然后使它们无效

举例:双向队列-〉堆栈

1.4.7  变体继承

子类和父类之间都是对方的变体,可以任意选择两个之间的父子关系

举例:用为控制鼠标的代码与用来控制图形输入板的代码几乎完全相同。在概念上没有理由让一个类作为另外一个类的父类,因此可以选择任何一个类作为另外一个类的父类

1.4.8  合并继承

可以通过合并两个或者更多的抽象特性来形成新的抽象。

一个类可以继承自多个基类的能力被称为多重继承 

多重继承在java中是不允许的,因为一旦父类中有同名同签名方法则子类不知道该调用哪个方法

举例:助教

1.5 构造函数在继承中

1.5.1 构造函数的调用
  • 在构造实例对象时,由程序自动调用(隐式调用)
  • 由程序员主动调用:在子类构造方法中调用父类的方法
1.5.2 构造函数的继承
  • 构造函数不能被继承(子类和父类有自己的构造函数)
  • 构造函数只能通过两种方法获得:1、自己编写;2、系统默认生成
1.5.3 子类与父类的构造函数
  • 子类构造函数可以用super()主动调用父类的构造函数(调用必须在第一行完成)
  • 子类构造函数如果没有主动调用父类构造函数,则系统会主动调用父类构造函数
  • 如果系统调用super(),而父类中的super()并不存在,则报错
1.5.4 子类、父类构造例题:

题目一:

运行下面程序:

class SuperClass{
    private int n;
    SuperClass(){
        System.out.println("SuperClass()");
    }
    SuperClass(int n){
        System.out.println("SuperClass(int n)");
        this.n = n;
    }
}
class SubClass extends SuperClass{
    private int n;
    
    SubClass(){
        super(300);
        System.out.println("SuperClass");
        
    }    
    SubClass(int n){
        System.out.println("SubClass(int n):"+n);
        this.n = n;
    }
}
public class TestSuperSub{
    public static void main (String args[]){
        SubClass sc = new SubClass();
        SubClass sc2 = new SubClass(200); 
    }
}

结果如下:

SuperClass(int n)
SuperClass
SuperClass()
SubClass(int n):200

题目二:

class A { 
	A() { 
System.out.println("A"); 
}
 } 
class B extends A {
	 B() {
       System.out.println("B"); 
   } 
}
 class C extends B { 
	C() { 
      System.out.println("C");
    }
 } 
public class hrt { 
	public static void main(String args[]) {
	 new C();
   } 
}

运行结果:

输出
A
B
C

基类构造器总是在导出类的构造过程中被调用,而且按照继承层级逐渐向上链接(调用顺序则是从基类开始向下)。可以理解为,这么做的逻辑关系是在一个类构建时可能会用到其父类的成员、方法。在清理时顺序相反。

总结

本系列内容均来自:山东大学-潘丽老师-面向对象开发技术-课程ppt、《设计模式》、《大话设计模式》

如果觉得写的还不错,可以点个赞收藏一下呀~~

祝大家学业、事业、爱情顺利!

天天开心,没有Bug每一天

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部