菜单
菜单
文章目录
  1. 六 继承和替换
    1. 1.扩展和简化
    2. 2.改写
    3. 3.接口和抽象类
    4. 4.继承形式
      1. 特殊化继承
      2. 规范化继承
      3. 泛化子类化
      4. 扩展继承
      5. 限制
      6. 变体
      7. 合并(多重继承)
    5. 5.可替换性
  2. 七、静态行为和动态行为
    1. 1.静态类和动态类
    2. 2.方法绑定
  3. 八、替换的本质
    1. 1.内存布局
    2. 2.复制和克隆
    3. 3.相同和同一
  4. 九、多重继承
    1. 1.分类化继承
      1. 1.名称歧义:
      2. 2.对替换的影响:
    2. 2.接口的多重继承
    3. 3.另一种方法
    4. 4.继承公共祖先
    5. 5.构造函数与析构函数
    6. 6.虚基类
  5. 十、多态及软件复用
    1. 1.多态
      1. (1).重载(专用多态)
      2. (2).改写(包含多态)
      3. (3).多态变量(赋值多态)
      4. (4).泛型(模板)
    2. 2.复用
  6. 十一、重载
    1. 1.重载
    2. 2.类型签名
    3. 3.范畴
    4. 4.基于类型签名的重载
    5. 5.基于范畴的重载
    6. 6.强制、转换、造型
    7. 7.重定义
  7. 十二、改写
    1. 1.改写与重载
    2. 2.标识改写
    3. 3.代替和改进
    4. 4.延迟方法
    5. 5.改写和遮蔽

面向对象复习(二)

六 继承和替换

1.扩展和简化

​ 这种把继承作为一种扩展同时也作为一种收缩的思想,正是面向对象技术强大的原因,同时也会在正常的部署中引起混淆 。
​ 继承总是向下传递的。
​ 继承的作用是代码复用和概念复用,以及共享方法的定义。
​ 静态类型的语言中,子类必须要有父类的所有数据成员。子类的实例必须至少通过继承实现父类所定义的所有功能。
​ 替换原则:如果B是A的子类,那么在任何情况下都可以用B来替换A,且无影响。

​ 子类型:符合替换原则的子类关系,区别于一般的可能不符合替换原则的子类关系

2.改写

子类为了避免与父类方法的冲突,需要对父类进行改写。

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

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

​ 改写机制:java等只需要改写统一签名的父类方法。C++则需要父类使用Virtual来表明这一含义。

3.接口和抽象类

接口也可以相互继承

抽象方法:定义在类和接口之间的方法,并不直接实现。创建实例时,子类必须实现父类的抽象方法。

4.继承形式

特殊化继承

  • 很多情况下,都是为了特殊化才使用继承。
  • 在这种形式下,新类是基类的一种特定类型,它能满足基类的所有规范。
  • 用这种方式创建的总是子类型,并明显符合可替换性原则。
  • 与规范化继承一起,这两种方式构成了继承最理想的方式,也是一个好的设计所应追求的目标。

规范化继承

  • 规范化继承用于保证派生类和基类具有某个共同的接又,即所有的派生类实现了具有相同方法界面的方法。
  • 基类中既有已实现的方法,也有只定义了方法接又、留待派生类去实现的方法。
  • 派生类只是实现了那些定义在基类却又没有实现的方法。派生类并没有重新定义已有的类型,而是去实现一个未完成的抽象规范。也就是说,基类定义了某些操作,但并没有去实现它。只有派生类才能实现这些操作。在这种情况下,基类有时也被称为抽象规范类。
  • 在Java中,关键字abstract确保了必须要构建派生 类。声明为abstract的类必须被派生类化,不可能 用new运算符创建这种类的实例。除此之外,方法 也能被声明为abstract,同样在创建实例之前,必 须覆盖类中所有的抽象方法。
  • 规范化继承可以通过以下方式辨认:基类中只是提供了方法界面,并没有实现具体的行为,具体的行为必须在派生类中实现。
  • GraphicalObject没有实现关于描绘对象的方法,因 此它是一个抽象类。其子类Ball,Wall和Hole通过 规范子类化实现这些方法。

####构造继承

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

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

构造子类化经常违反替换原则(形成的子类并不是子类型)

泛化子类化

派生类扩展基类的行为,形成一种更泛化的抽象。

泛化子类化通常用于基于数据值的整体设计,其次才是基于行为的设计。

扩展继承

如果派生类只是往基类中添加新行为,并不修改从基类继承来的任何属性,即是扩展继承。

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

限制

如果派生类的行为比基类的少或是更严格时,就是限制继承。常常出现于基类不应该、也不能被修改时。限制继承可描述成这么一种技术:它先接收那些继承来的方法,然后使它们无效。双向队列-〉堆栈

由于限制继承违反了可替换性原则,用它创建的派生类已不是派生类型,因此应该尽可能不用。

变体

两个或多个类需要实现类似的功能,但他们的抽象概念之间似乎并不存在层次关系。控制机械鼠标=控制轨迹球 但是,在概念上,任何一个类作为另一个类的子类都不合适 因此,可以选择其中任何一个类作为父 类,并改写与设备相关的代码

合并(多重继承)

可以通过合并两个或者更多的抽象特性来形成新的抽象。一个类可以继承自多个基类的能力被 称为多重继承 。

Java语言中的匿名类创建匿名类的条件 : 只能创建一个匿名类的实例 匿名类必须继承于父类或接又,并且不 需要构造函数进行初始化。

p.add(new ButtonAdapter(“Quit”){ 
public void pressed(){
System.exit(0);
}
});

继承使得构造函数这个过程变得复杂 由于父类和子类都有待执行的初始化代 码,在创建新对象时都要执行Java等语言 只要父类构造函数不需要参数,父类的构造 函数和子类的构造函数都会自动地执行。 当父类需要参数时,子类必须显示地提供参数。在java中通过super这个关键字来实现

5.可替换性

可替换性是面向对象编程中一种强大的软件开发技术。

可替换性的意思是:变量声明时指定的类型不必与它所容纳的值类型相一致。

这在传统的编程语言中是不允许的,但在面向对象的编程语言中却常常出现。

七、静态行为和动态行为

//术语静态总是用来表示在编译时绑定于对象并且不允许以后对其进行修改的属性或特征。
//术语动态用来表示直到运行时绑定于 对象的属性或特征。

1.静态类和动态类

变量的静态类是指用于声明变量的类。

静态类在编译时就确定下来,并且再也不会改变。

变量的动态类指与变量所表示的当前数值相关的类。

动态类在程序的执行过程中,当对变量赋新值时可以改变。

//对于静态类型面向对象编程语言,在编译时消息传递表达式的合法性不是基于接收器的当前动态数值,而是基 //于接收器的静态类来决定的。

2.方法绑定

静态方法绑定/动态方法绑定

响应消息时对哪个方法进行绑定是由接收器当前所包含的动态数值来决定的。

八、替换的本质

1.内存布局

分配方案:只分配基类的内存空间

最小静态空间分配c++

  • 对于指针(引用)变量:当消息调用可能被改写的成员函数时,选择哪个成员函数取决于接收器的动态数值。
  • 对于其他变量:关于调用虚拟成员函数的绑定方式取决于静态类(变量声明时的类),而不取决于动态类(变量所包含的实际数值的类)

解释

​ 变量赋值过程中,数值从子类表示的类型转换为弗雷所表示的类型。

​ 类似于将整形变量赋值给浮点型变量。

​ 对于基于堆栈的变量,可以确保动态类总是等同于静态类

​ 所以不能对对象中的不存在字段进行存取。不必考虑赋值过程中的内存数据丢失。

最大静态空间分配:分配变量值可能使用的最大存储空间

​ 堆栈中不保存对象值。堆栈通过指针大小空间来保存标识变量,数据值保存在堆中。

​ 指针变量都具有恒定不变的大小,变量赋值时,不会有任何问题。

内存分配方法影响赋值的含义:

​ 1.复制语义:变量值独立

​ 2.指针语义:同一(Java)

2.复制和克隆

浅复制:共享实例变量

深复制:建立实例变量的新副本(C++拷贝构造函数;java改写clone方法)

clone要说明的有两点:

​ 一是拷贝对象返回的是一个新对象,而不是一个引用。

​ 二是拷贝对象与用new操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。

影子clone的话:cloneB b2 =b1.clone(); b2不会改变b1 但会改变b1通过clone的对象

3.相同和同一

相同是指不同对象相同值

同一是指同一内存对象

九、多重继承

1.分类化继承

​ 多重继承是指同一个类可以有多个不同的父类,并且可以继承每个父类的行为和数据

​ 问题:

1.名称歧义:

​ (1)使用全限定名

​ (2)使用重定义和重命名

2.对替换的影响:

​ 引入两个辅助类(c++)

2.接口的多重继承

​ java C#都不支持类的多重继承,但他们都支持接口的多重继承

3.另一种方法

内部类 可以避免很多语义问题

ClassGraphicalCardDeck extends CardDeck {

  public void draw ( ) {
//外部类改写CardDeck方法
  }

  private drawingClass drawer= new drawingClass();

  public GraphicalObject myDrawingObject () {return drawer;}

  private class drawingClass extends GraphicalObject {

  public void draw ( ) {
//内部类改写GraphicalObject方法
  }

  }

}

4.继承公共祖先

eg1:

class Link{

public:

  Link *nextLink;

}

class CardDeck:publicLink{..};

class GraphicalObject:publicLink{..}

当创建同时继承自CardDeck类和GraphicalObject类的子类时,这个子类应该包含多少个nextLink字段?

假如纸牌列表和图形对象列表是不同的,那么每种类型的列表都应该有各自的链接。因此,子类用于两个独立的链接字段看起来更为恰当。

eg2:

  • 输入输出流既是输入流的派生,也是输出流的派生。
  • 但是,只存在一个对象,两个文件指针引用相同的数值。即指需要公共祖先的一份拷贝。
  • 在c++语言中,这个问题通过在父类列表中使用virtual修饰符来解决。
  • 关键字virtual意味着在当前派生类中,超类可以出现多次,但只包含超类的一份拷贝。
class Stream{
File *fid;
}
class InStream : public virtual Stream{
int open(File *);
..};
class OutStream : public virtual Stream{
int open(File *);
..};
class InOutStream : public InStream, public OutStream {
int open(File *);
..};

5.构造函数与析构函数

​ 构造函数也遵从先祖先(基类),再客人(成员对象),后自己(派生类)的原则,有多个基类之间则严格按照派生定义时从左到右的顺序来排列先后。

​ 析构函数调用正好相反。

6.虚基类

十、多态及软件复用

1.多态

(1).重载(专用多态)

​ 类型签名区分

class overloader{
//three overloaded meanings for the same name
public void example (int x){……}
public void example (int x,double y){……}
public void example (String x){……}
}

(2).改写(包含多态)

​ 层次关系中相同类型签名

class parent{
public void example(int x){……}
}
class child extends parent{
//same name,different method body
public void example(int x){……}
}

(3).多态变量(赋值多态)

​ 声明与包含不同

Parent p=new child();//declared as parent,holdingchild value

(4).泛型(模板)

​ 创建通用工具

Template<class T> T max(T left,Tright)

{

  //return largest argument

  if (left<right) return right;

  return left;

}

2.复用

继承和组合

  • 如果A类有一个B类,那么自然地,B类的数据字段也应该是一个A类实例的一部分。另一方面,如果A类是一个B类,那么使用继承是正确的编码方式。

  • 通过继承,新的类可以声明为已存在类的子类。通过这种方式,与初始类相关的所有数据字段和函数都自动地与新的数据抽象建立联系。

    class set:public List{

      public:

      //constructor

      Set();

      //operations

      void add(int);

      int size();

    }

二者比较

  • 组合是较为简单的一种技术。优点是在特定的数据结构中需要执行哪些操作。无需考虑列表类所定义的所有操作。
  • 继承无法知道一个方法是否可以合法地应用于集合。
  • 使用继承构建数据抽象的代码的简洁性是继承的一个优点
  • 继承无法防止用户使用父类的方法来操纵新的数据结构:FirstElement

十一、重载

1.重载

  • 重载是在编译时执行的,而改写是在运行时选择的。
  • 重载是多态的一种很强大的形式。
  • 非面向对象语言也支持。

2.类型签名

  • 函数类型签名是关于函数参数类型、参数顺序和返回值类型的描述。
  • 类型签名通常不包括接收器类型
  • 因此,父类中方法的类型签名可以与子类中方法的类型签名相同。

3.范畴

  • 范畴定义了能够使名称有效使用的一段程序,或者能够使名称有效使用的方式。(局部变量/public成员)
  • 通过继承创建的新类将同时创建新的名称范畴,该范畴是对父类的名称范畴的扩展。
  • 对于一个程序代码中的任何位置,都存在着多个活动的范畴。(类成员方法同时具有类范畴和本地范畴)
  • 通过类型签名和范畴可以对重载进行两种分类:基于具有不同范畴的方法;基于具有不同类型签名的方法。

4.基于类型签名的重载

多个过程(或函数、方法)允许共享同一名称,且通过该过程所需的参数数目、顺序和类型来对它们进行区分。即使函数处于同一上下文,这也是合法的。

class Example{

  //same name,threedifferent methods

  int sum(int a){return a;}

  int sum(int a,int b){return a+b;}

  int sum(int a,int b,int c){return a+b+c;}

}

5.基于范畴的重载

  • 相同的名称可以在不引起歧义且不造成精度损失的情况下出现于多个不同的范畴。
  • 并不一定语义要相关:如继承

6.强制、转换、造型

  • 强制是一种隐式的类型转换,它发生在无需显式引用的程序中。
double x=2.8;

int i=3;

x=i+x;//integer i will be  converted to real
  • 转换表示程序员所进行的显式类型转换。在许多语言里这种转换操作称为“造型”。
x=((double)i)+x;
  • 造型和转换既可以实现基本含义的改变;也可以实现类型的转换,而保持含义不变(子类指针转换为父类指针)。

    • 上溯造型
    X a=new X();    
    Y b=new Y();
    a=b;  //将子类对象造型成父类对象,相当做了个隐式造型:a = (X)b;

    • 下溯造型
    X a=new X(); 

    Y b=new Y();

    X a1=b;

    Y b1=(Y)a1;

7.重定义

  • 当子类定义了一个与父类具有相同名称但类型签名不同的方法时,发生重定义。

  • 类型签名的变化是重定义区别于改写的主要依据。

  • 两种不同的技术解析重定义:融和模型和分级模型。

    • JAVA使用融和模型

      class Parent {

        public void example (int a)

        {
      System.out.println(“inparent method”);
      }

      }

      class Child extends Parent {

        public void example (int a,int b)

        {
      System.out.println(“inchild method”);
      }

      }

      //使用时

      Child aChild= new Child();

      aChild.example(3);

      //对于C++语言来说,则会产生编译错误

    • C++每个范畴都维护自己独立的列表。

      为了使名称与方法匹配,需要依次对各个范畴进行检查,因此将其标识为分级的。

      通过在子类中重定义两个方法来实现与Java模型相同的效果

      class Parent {

        public void example (inta)

        {
        System.out.println(“inparent method”);
        }

      }

      class Child extends Parent {

        public void example (int a)

        {
        Parent::example(a);
        }

        public void example (nt b)

        {
        System.out.println(“inchild method”);
        }
      int a,i;

      }

十二、改写

  • 如果子类的方法具有与父类的方法相同的名称和类型签名,称子类的方法改写了父类的方法。

  • 与替换原则结合使用。

  • 改写可看成是重载的一种特殊情况。由于重载也涉及一个方法名称和两个或更多个方法体

1.改写与重载

  1. 对于改写来说,方法所在的类之间必须符合父类/子类继承关系,而对于简单的重载来说,并无此要求。
  2. 如果发生改写,两个方法的类型签名必须匹配。
  3. 重载方法总是独立的,而对于改写的两个方法,有时会结合起来一起实现某种行为。
  4. 重载通常是在编译时解析的,而改写则是一种运行时机制。对于任何给定的消息,都无法预言将会执行何种行为,而只有到程序实际运行的时候才能对其进行确定。

2.标识改写

各种语言在如何通过代码实现标识改写这方面存在着差异。(Java不需要,C++需要virtual)

改写并不能改变方法的可存取性。

如果一个方法在父类中为public,那么不允许在子类中将该方法声明为private。反之亦然。

3.代替和改进

两种不同的关于改写的解释方式:

1.代替(replacement):在程序执行时,实现代替的方法完全覆盖父类的方法。即,当操作子类实例时,父类的代码完全不会执行。

2.改进(refinement):实现改进的方法将继承自父类的方法的执行作为其行为的一部分。这样父类的行为得以保留且扩充。

这两种形式的改写都很有用,并且经常在一种编程语言内同时出现

如:几乎所有的语言在构造函数中都使用改进语义。

4.延迟方法

  • 如果方法在父类中定义,但并没有对其进行实现,那么我们称这个方法为延迟方法。

  • 延迟方法有时也称为抽象方法,并且在C++语言中通常称之为纯虚方法。

  • 延迟方法的一个优点就是可以使程序员在比实际对象的抽象层次更高的级别上考虑与之相关的活动。

abstract class Shape{

  abstract public void draw();

}

Circle、Square、Triangle
  • 延迟方法更具实际意义的原因:在静态类型面向对象语言中,对于给定对象,只有当编译器可以确认与给定消息选择器相匹配的响应方法时,才允许程序员发送消息给这个对象。

5.改写和遮蔽

  • 改写与遮蔽存在着外在的语法相似性

  • 类似于重载,改写区别于遮蔽的最重要的特征就是,遮蔽是在编译时基于静态类型解析的,并且不需要运行时机制。

  • 几种语言需要对改写显式声明,如果不使用关键字,将产生遮蔽。

Parent p = new Parent();
Child c =new Parent();
p.x=12;
c.x=42;
p=c;
p.x=12;