概述
第一章 面向对象思想
代理和团体
-
一个面向对象程序可以组织一个团体,这个团体由一组互相作用的叫做“对象”的代理组成。
-
每一个对象都扮演一个角色。
-
并且为团体中的其他成员提供特定的服务或者执行特定的行为。
消息和方法
行为的启动是通过将“消息”传递给对此行为负责的代理(对象)来完成的。
消息对行为对要求进行编码,并且伴随着执行要求所需的附加信息(参数)来一起传递。
接收器:消息发送的对象。如果接收器接受了信息,那么同时它也接受了消息所包含的行为责任。然后接收器响应消息,执行相应的“方法”以实现要求。
信息隐藏原则
提出要求的客户不需要了解要求实现的具体方式。
消息传递与过程调用区别
1. 消息传递有指定的接收器。过程调用没有指定的接收器。
2. 消息的解释(即用于响应消息的方法)由接收器来决定,并且随着接收器的不同而不同。
3. 消息(函数或过程名称)和响应消息的代码段(方法)之间是**后期绑定关系**。传统过程调用中名称与代码段是之间是**早期(编译时或链接时)绑定关系**。
责任
面形对象的一个基本概念就是用责任来描述行为:提高了抽象水平,对象更加独立。
协议:描述与一个对象相关的整个责任的集合。
结构化与面向对象比较:
传统程序的执行通常是对数据结构进行操作(例如改变数组或记录中的域),与之相反,面向对象程序却要求数据结构(即对象)提供服务。
类和实例
对象
对象是独立存在的客观事物,它由一组属性和一组操作构成。
属性和操作是对象的两大要素。属性是对象静态特征的描述,操作是对象动态特征的描述。
属性一般只能通过执行对象的操作来改变。
操作又称为方法或服务,它描述了对象执行的功能。通过消息传递,还可以为其它对象使用。
对象的性质
- 封装性:信息隐藏
- 自治性:主动数据
- 通信性:并发
- 暂存性:作用域/期
- 永久性:文档串行化
不干预原则
允许对象以任何它认为合适的,不干预其他对象的方式来完成任务,而不要干预它。
复合对象
- 划分
- 聚合
部分/整体
部分/整体关系中有两种方式:组合和聚合。
-
组合关系中部分和整体的关系很紧密。
-
聚合关系中则比较松散,一个部分对象可以属于几个整体对象。
类
-
根据抽象的原则对客观事物进行归纳和划分,只关注与当前目标相关的特征,把具有相同特征的事物归为一个类。
-
它是一个抽象的概念。
-
类是具有相同属性和相同操作(服务)的对象的集合。它包括属性和操作。
-
类是对象相关行为的储存库(repository)。即同一个类的所有对象都能执行同样的动作。
类和实例的关系
-
所有对象都是类的实例。
-
在响应消息时调用何种方法由类的接收器决定。
-
一个特定类的所有对象使用相同的方法来响应相似的消息。
继承
-
类被组织成有单个根节点的树状结构,称为继承层次结构。
-
与类实例相关的内存和行为都会被树结构中的后代自动继承。
-
继承表达了对象的一般与特殊的关系。
-
特殊类的对象具有一般类的全部属性和服务。
一般/特殊
对象之间存在着一般和特殊的结构关系,也就是说它们存在继承关系。很多时候也称作泛化和特化关系。
方法绑定与改写
接收器搜索并执行相应的方法以响应给定的消息。
如果没有找到匹配的方法,搜索就会传导到此类的父类。搜索会在父类链上一直进行下去,直到找到匹配的方法,或者父类链结束。
如果能在更高类层次找到相同名称的方法,所执行的方法就称为改写了继承的行为。
多态性
多态性是指一般类中定义的属性和服务,在特殊类中不改变其名字,但通过各自不同的实现后,可以具有不同的数据类型或具有不同的行为。
第二章 抽象
抽象的概念
抽象是指对于一个过程或者一件制品的某些细节有目的的隐藏,以便把其他方面、细节或者结构表达得更加清楚。
抽象,是控制复杂性时最重要的工具。
信息隐藏
在抽象表现开发过程中有目的性地忽略细节。
抽象层次
抽象层次一 团体
在最高级别上,程序被视为一个对象的“团体”,这些对象间相互作用,以完成共同的目标。
团体的概念
在面向对象程序开发过程中,关于“团体”有两个层次的含义:
- 首先是指程序员的团体,他们在现实世界中相互作用,以便开发出应用程序来。
- 第二个团体是这些程序员创建的对象的团体,它们在虚拟世界中相互作用,以完成它们的共同目标。
抽象层次二 单元
许多语言允许协同工作的对象组合到一个“单元”(unit)中。
例如,Java的“包” (packages),C++的“名字空间”(name spaces),Delphi中的“单元”(units)。
这些单元允许某些特定的名称暴露在单元以外,而其他特征则隐藏在单元中。
抽象层次三 CS(客户端与服务器)
处理两个独立对象之间的交互。
两个对象间交互时涉及两层抽象:一个对象(服务者, server)向另一个对象(客户, client)提供服务,二者之间以通信来交互。
对象间消息传递。
该级别抽象通常用接口来表示。定义行为,但不描述如何来实现。
抽象层次四 服务实现方式
考虑抽象行为的具体实现方式。例:多种实现堆栈方式。
抽象层次五 具体实现
关注执行一个方法的具体操作实现。
抽象形式
抽象的思想可以进一步划分为不同的形式。
-
特化:汽车是由发动机、传动装置、车体、车轮组成;发动机是由 ……(有什么(has a)思想)
-
分而治之
优点:在某一个层次只关心该层次实现需要的细节即可(前提:设计好了各个配件之间的交互关系)(是什么(is a)思想)
服务:商品制造者关心实现,高层设计者和使用者不关心实现,所以对接口和实现的划分,不仅从高层的角度对设计易于理解,而且使软件组件的替换成为可能。
组合/复合法:由少量简单的形式,根据组合规则构建出新的形式。关键在于既可对初始形式进行组合,也可以对新形式进行组合。
分类:当系统中组件数量变大时,常用分类(Catalogs)来进行组织。
-
多视角:对一个制品进行不同视角的观察,每一种视角强调特定的细节,而忽略其他部分
继承方式
- Is-a继承
- Has-a继承
抽象过程发展简史
-
汇编语言:最早的抽象机制
-
过程/函数
-
模块:解决全局名称空间拥挤问题。
用来改善建立和管理名称集合及其相关数值的一种技术。
模块提供了将名称空间划分成两个部分的能力。
公有部分可以在模块外存取,私有部分只能从模块内存取。
模块不允许实现实例化。实例化是一种能够建立数据区域多份拷贝的能力。
-
抽象数据类型。
目标:1).定义抽象,创建多个实例;
2).使用实例,知其所提供操作,不必知道如何实现。
抽象数据类型思想的重要进展是最终把接口的概念和实现的概念分离开来。
-
以服务为中心。
汇编语言和过程:功能为中心;
模块和ADT:数据为中心;(结构、表示、操纵)
面向对象:服务为中心。
-
消息、继承和多态。
ADT基础上增加的新思想。
第三章 类和方法
封装
- 避免重复的代码
- 保护类受到不必要的修改
相关概念
实例:表示类的一个具体代表或者范例。
实例变量/数据字段/数据成员(三者同一含义):实例所维护的内部变量。
类中声明次序建议-可读性
- 先列出主要特征,次要的列在后面。
- 私有数据字段列在后面。
- 构造函数列在前面。
- 对方法的声明应该通过分组来表示,这样可以迅速便捷找到对应于给定消息选择器的方法。分组的原则可以按照字母顺序排列。或者按照方法的职能进行分组。
定义和实现分离
Java,C#:方法主体直接放在类定义中。
C++:分离
类主题的变化
接口
- 不提供实现
- 接口定义新类型,可以声明变量
- 类的实例可以赋值给接口类型变量
向前定义
- 多个互相引用的类(相互递归)
- Java全文扫描。C++向前定义
内部类(嵌套类)
- 在一个类中定义另外一个类。(Java内部类。C++嵌套类)
- Java内部类被连接到外部类的具体实例上,并且允许存取其实例和方法。C++仅是命名手段,限制和内部类相关的特征可视性。
java内部类与cpp嵌套类区别:
java的非静态内部类有个外部类的引用outer,使用这个变量可以引用外部的所有变量,包括private
静态的java内部类也叫做嵌套类,静态的内部类就没有外部的引用了,但是也只能调用外部的静态方法和变量
c++的内部类几乎等同于语法上的嵌套,而C++的内部类不能访问外部类的private变量,想访问的话必须在内部类中声明外部类为friend class (或者在需要访问外部变量的方法的参数中传递外部类引用,或者每个嵌套类对象都包含一个外部类的引用,不过比较浪费资源)
更多知识见
http://blog.csdn.net/a450828540/article/details/8993160
http://blog.csdn.net/a450828540/article/details/9045067
类的数据字段(类属性)
被一个类的所有实例共享的公共数据字段。
Java和C++使用修饰符static创建共享数据字段。
如何对该字段初始化?
对象本身不对共享字段初始化。内存管理器自动将共享数据初始化为某特定值,每个实例去测试该特定值。第一个进行测试的做初始化。
Java:静态数据字段的初始化是在加载类时,执行静态块来完成。
c++:两种方式
- 由基本数据类型表示的静态数据字段可以在类的主体中进行初始化。
- 在类外对静态数据字段进行初始化。(类型 类名:静态成员=值)
作为对象的类
详细见“反射”部分
第四章 消息、实例和初始化
消息
**消息:**要求某个对象执行在它所在的那个类中定义的某个操作的规格说明。是对象间相互请求或相互协作的途径。
**消息传递(messgae passing,有时称为method lookup,方法查询):**请求对象执行一项特定行为的动态过程。
一个消息传递表达式由三部分组成
- 接受消息的对象:接收器(receiver)
- 接收对象要采取的方法:消息选择器(message selector)
- 方法需要的:参数(argument)
A.b(c);//A是接收器,b是消息选择器,c是参数
消息传递语法
//C++, C#, Java,Python, Ruby
aCard.flip ();
aCard.setFaceUp(true);
aGame.displayCard(aCard, 45, 56);
//Pascal, Delphi,Eiffel, Oberon
aCard.flip;
aCard.setFaceUp(true);
aGame.displayCard(aCard, 45, 56);
#Smalltalk
aCard flip.
aCard setFaceUp: true.
aGame display: aCard atLocation: 45 and: 56.
消息和对象
对象之间的相互作用是通过消息产生。消息由某个对象发出,请求其他某个对象执行某一处理或回答某些信息。
静态类型语言和动态类型语言
静态类型语言:类型和变量联系在一起。(高效性)
- 编译时作出内存分配决定。不必运行时刻重新分配。
- 控制类型错误。
动态类型语言:变量看作名称标识,类型和数值联系在一起。(灵活性)
差异:变量或数值是否具备类型这种特性。
分类
- 面向对象静态语言:C++,C#,Delphi,pascal,Eiffel,Java
- 面向对象动态语言:Objective-c,Smalltalk,Dylan,Python,JavaScript
- 非面向对象静态:Ada,Algol,C,Fortran,Haskell,ML,Modula
- 非面向对象动态:APL,Forth,Lis,Prolog,Snobol
消息传递方面的差异
- 静态类型语言在编译时使用接收器的类型来检查选择器,以理解它所接收的消息。
- 动态类型语言在编译时无法核实这一消息。如果接收器不理解消息选择器,那么就有可能产生运行时错误(静态类型语言不会产生这种错误)
从方法内部存取接收器(伪变量)
在大多数面向对象语言中,接收器并不出现在方法列表中,而是隐藏于方法的定义中。˙只有当必须从方法体内部去存取接收器的数值时,才会使用伪变量(pseudo-variable)。
- Java,C++:this
- Eiffel:Current
- Smalltalk,Object-C,Object Pascal:self
伪变量
-
不需要声明,不能被更改
-
在使用时就好像作为类的一个实例
-
默认隐藏:很多编程语言中,对接收器伪变量的使用都可以被忽略。如果在没有引用接收器的条件下,访问一个数据字段或者调用一个方法,那么意味着接收器伪变量将作为消息的主体。
//两者等价
public void flip () { setFaceUp( ! faceUp ); }
public void flip(){this.setFaceUp(!this.faceUp); }
-
对象自身引用:this隐含指向调用成员函数的对象
-
参数传递:当某一方法想要把自身作为一个参数传递给另一个参数时,就必须使用伪变量来解决问题。
addActionListener(this);
-
Java中,对于构造函数,使用this和构造函数的参数进行初始化数据成员。通过显示地使用this,可以区分两个分别用作函数参数和数据成员的同名变量。
-
在Python,CLOS,Oberon语言中,接收器必须在方法体中显示声明。(对于这些语言,尽管原则上第一个参数可以以任何名称来命名,但是一般都命名为self或者this,以此来表示此方法与接收器伪变量之间的关系)例如在Python中:
aCard.moveTo(27,3)
而相应的方法声明需要三个参数
class PlayingCard:
def moveTo(self,x,y):
...
对象的创建
创建:为一个新对象分配存储空间并且将这段空间与对象名称进行绑定。
初始化:不但包含为对象的数据区域设置初始值,还包括建立操作对象所需的初始条件这个更一般的过程。
一些编程语言允许用户把变量声明和初始化结合起来(Java)
大多数面向对象语言都把变量的命名过程和对象的创建过程分离开来。
创建对象语法
//C++
PlayingCard * aCard = new PlayingCard(Diamond, 3);
//Java, C#
PlayingCard aCard = new PlayingCard(Diamond, 3);
#Smalltalk
aCard <- PlayingCard new.
#Python(没有显式使用new操作符)
aCard = PlayingCard(2,3)
对象数组的创建
两个层次:
- 数组自身的分配和创建
- 数组所包含的对象的分配和创建
C++中
两个层次结合在一起。数组由对象组成,而每个对象使用缺省(即无参数)构造函数来初始化。
PlayingCard cardArray[52];
Java中
new仅创建数组。数组包含的对象必须独立创建。(典型通过循环来实现)
PlayingCard cardArray[ ] = new PlayingCard[13];
for (int i = 0; i < 13; i++)
cardArray[i] = new PlayingCard(Spade,i+1);
指针和内存分配
所有面向对象语言在它们的底层表示中都使用指针,但不是所有语言都把这种指针暴露给程序员。
对象引用实际是存在于内部表示中的指针。
指针
- 指针引用堆分配内存。对于堆分配的变量值,只要存在对它的引用,就会一直存在。因此,变量值的生存期一般长于创建该变量过程的生存期。
- 堆分配的内存必须通过某种方式回收。
- 某些语言中,指针和传统变量有区别。(比如C++中,以通常方式声明的变量,即所谓的自动(sutomatic)变量,其生存期总是绑定在创建该变量的函数上,退出过程时,变量的内存值就会被回收。而赋值给指针(或者说是引用,指针的另外一种形式)的数值没有绑定到过程入口和出口处。)
内存回收
- 使用new创建——堆内存
- 堆内存没有绑定在过程的入口和出口处。
- 内存有限
释放内存关键字
- C++:delete/delect []
- Object Pascal:free
垃圾回收机制(Java,C#,Smalltalk,CLOS)
- 时刻监控对象的操作,对象不再使用时,自动回收其所占内存。
- 通常在内存将要耗尽时工作。
- 在回收无用内存时,需要将正在执行的应用程序挂起,回收完成后程序恢复执行。垃圾回收需要一定的执行时间。
垃圾回收与自己释放内存比较
- 付出额外代价
- 避免释放多次;避免使用已被释放内存;避免耗尽内存。
垃圾回收机制的实现
- 确保动态分配的内存对象都有一个指定的属主(owner),内存的属主负责保证内存位置的合理使用,以及当内存不再使用时得以释放。
- 引用计数(reference counts):引用共享对象的指针的计数值。只要加入一个新指针计数值就会加一,只要取消一个指针,计数值就会减一。当计数值达到0时,就意味着没有指针引用该对象,它的内存可以被回收。
内存分配
按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式(动态)的,和堆式的。
静态内存分配
静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间。
这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求。
栈式存储分配
栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的。
和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存。
栈式存储分配按照先进后出的原则进行分配。
堆式存储分配
静态存储分配要求在编译时能知道所有变量的存储要求
栈式存储分配要求在过程的入口处必须知道所有的存储要求
而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配。(比如可变长度串和对象实例。)
堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。
构造函数
**构造函数:**用来初始化一个新创建对象的方法。
**优点:**它能确保对象在正确地初始化之前不会被使用。防止多次调用初始化。
构造函数的使用
-
Java和C++中,可以通过检查与类显示的名称是否相同来识别构造函数和普通方法。并且构造函数不声明返回值的数据类型。
-
Java和C#中,数据字段可以初始化为特定的数值,这种赋值独立于构造函数中的参数赋值,数据字段可以在初始化时进行赋值,也可以在后来的构造函数中再次赋值。在C++中,也使用相似的语法来声明静态数据变量或常量。
class Compex{
public double realPart = 0.0;//initialize data areas
public double imagPart = 0.0;
public Complex (double rv){realPart = rv;}
}
-
构造函数的重载:在C++,C#,Java中,只要每个函数参数的数目、类型或次序不同,就允许多个函数使用相同的名称定义。构造函数经常使用这种定义方式。例如一个构造函数为无参构造,另一个为有参构造。
-
C++中调用缺省构造函数必须去掉括号,在这里使用括号虽符合语法,但是语义截然不同。
PlayingCard cardFive; // creates a new card
PlayingCard cardSix(); // forward definition for function named cardSix that returns a PlayingCard
- 使用new语法和无参构造函数建立对象:C++不需要括号,Java、C#需要。
PlayingCard cardSeven = new PlayingCard(); // Java
PlayingCard *cardEight = new PlayingCard; // C++
-
初始化器(C++中):用于对象成员初始化和派生类对基类初始化。
Class PlayingCard { public: PlayingCard (Suits is, int ir) : suit(is), rank(ir), faceUp(true) { } ... };
C++类规范
C++中几乎所有的类都应该定义4个重要的函数。这被称为正统规范的类(orthodox canonical class)
- 缺省构造函数
- 拷贝构造函数
- 赋值操作符
- 析构函数
对于这四个函数,如果用户没有提供相应函数的实现,系统就会自动创建相应函数的缺省版本。
拷贝构造函数
就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。
#include <iostream>
using namespace std;
class Test {
public:
Test(int temp){
p1=temp;
}
protected:
int p1;
};
void main() {
Test a(99);
Test b=a;
}
在上面的代码中,我们并没有看到拷贝构造函数,同样完成了复制工作,这又是为什么呢?因为当一个类没有自定义的拷贝构造函数的时候系统会自动提供一个默认的拷贝构造函数,来完成复制工作。
常数值
final
Java中使用final。
- 可以直接初始化。
- 也可以在构造函数中赋值。如果有多个构造函数,那么每个构造函数都必须初始化这一数据字段。
const
C++中使用。
在构造函数中使用一个初始化子句来赋值。
区别
- const常量,禁止以任何方式修改,即使是处于对象的内部状态。(个别分数据字段可以命名为mutable,这时即使数据字段处于一个常量对象内,也可以进行改变,此种情况较少)
- final仅断言相关变量不会赋予新值,并不能阻止在对象内部对变量值进行改变。如对消息的响应。(只是意味着它不会被重新赋值,并不意味着它不会再改变)
final aBox = new Box(); // can be assigned only once
aBox.setValue(8); // but can change
aBox.setValue(12);
析构函数和终止器
C++中。内存空间开始释放对象,就会自动调用析构函数。
析构函数粉名称为波浪字符“~”加上类的名称。它不需要任何参数,也不会被用户直接调用。
自动变量:当声明变量的函数返回时,变量的空间就会被释放。
动态分配的变量:空间的释放通过操作符delete进行。
Java中,在垃圾回收系统即将回收变量的内存前,才调用finalize方法,由于这项操作可能发生于任何时刻,也可能从不发生,因此,在Java语言中使用finalize方法的情况远不如在C++语言中使用析构函数的情况多。
Smalltalk语言中的元类
作为对象的类
Java,Smalltalk中类本身是对象。那么什么类代表了对象所属的类别,即这个类是什么类。有一个特殊的类,一般称为Class,这就是类的类。
这个类所具有的行为
- 创建实例
- 返回类名称
- 返回类实例大小
- 返回类实例可识别消息列表
全对象系统
类是对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ppu4QQ6N-1600225751543)(/Users/yuxiangning/Library/Application Support/typora-user-images/image-20200425132345427.png)]
反射和内省
反射(reflection)和内省(introspection)是指程序在运行过程中“了解”自身的能力。
用于反射和内省的技术分为两大类
- 获取理解当前计算状态的特征
- 用于修改的特征:增加新的行为
类对象
- 反射工具都开始于一个对象,该对象是关于一个类的动态(运行时)体现。
- 类对象是更一般的类(称为Class类)的实例。
- 类对象通常都包括类名称、类实例所占用内存的大小以及创建新实例的能力。
Java获取类对象:
Class aClass = aVariable.getClass();
C++获取类对象:
typeinfo aClass = typeid(AVariable);
类对象操作
获取父类
Class parentClass = aClass.getSuperclass(); // Java
字符串类名称
char * name = typeinfo(aVariable).name(); // C++
String internalName = aClass.getName();//Java
String descriptiveName = aClass.toString();
检测对象类
- 对这种类型检测的不恰当使用是设计类结构不好的一个标志。
- 大多数情况下,都可以通过调用改写方法来代替显式检测
Child *c = dynamic_cast<Child *>(aParentPtr);
if (c!=0){ … } //C++
if (aVariable instanceof Child) …
if (aCalss.isInstance(aVariable)) … //Java
通过类建立实例
Object newValue = aClass.newInstance(); // Java
Java中的类行为
Class forName(string)
Class getSuperClass()
Constructor[] getConstructors()
Field getField(string)
Field[] getFields()
Method[] getDeclaredMethods()
boolean isArray()
boolean isAssignableFrom(Class cls)
boolean isInstance(Object obj)
boolean isInterface()
Object newInstance()
作为对象的方法
Java和Smalltalk中,将方法看作是可以存取和操纵的对象。
Java中,一个方法是Method类的一个实例。定义了如下操作:
String getName()
Class getDeclaringClass()
Int getModifiers()
Class getReturntype()
Class[] getParameterTypes()
Object invoke(Object receiver,Object[]args)
举例:
Method [ ] methods = aClass.getDeclaredMethods();
System.out.println(methods[0].getName());
Class c = methods[0].getReturnType();
Class sc = String.class;
Class [ ] paramTypes = new Class[1];
paramTypes[0] = sc;
try {
Method mt = sc.getMethod( "concat“ , paramTypes);
Object mtArgs [ ] = { "xyz" };
Object result = mt.invoke("abc", mtArgs);
System.out.println("result is " + result);
} catch (Exception e) {
System.out.println("Exception " + e);
}
修改机制
Java语言的标准类库定义了一个名称为ClassLoader的类,这个类可以根据存储于文件中的信息来加载一个类。
Java中类对象是自身的一个实例
面向对象语言一般支持以下原则
- 所有对象都是某个类的实例
- 类也是对象
那么就可以回答类对象属于什么类?
在Java语言中,回答相对比较简单,一个类为Class的实例,也就是自身的一个实例。
Java中反射的使用
//得到某个对象的属性
public Object getProperty(Object owner, String fieldName) throws Exception {
Class ownerClass = owner.getClass();
Field field = ownerClass.getField(fieldName);
Object property = field.get(owner);
return property;
}
//得到某个类的静态属性
public Object getStaticProperty(String className, String fieldName) throws Exception {
Class ownerClass = Class.forName(className);
Field field = ownerClass.getField(fieldName);
Object property = field.get(ownerClass);
return property;
}
//执行某对象的方法
public Object invokeMethod(Object owner, String methodName, Object[] args) throws Exception {
Class ownerClass = owner.getClass();
Class[] argsClass = new Class[args.length];
for (int i = 0, j = args.length; i < j; i++) {
argsClass[i] = args[i].getClass();
}
Method method = ownerClass.getMethod(methodName, argsClass);
return method.invoke(owner, args);
}
//执行某个类的静态方法
public Object invokeStaticMethod(String className, String methodName,Object[] args) throws Exception {
Class ownerClass = Class.forName(className);
Class[] argsClass = new Class[args.length];
for (int i = 0, j = args.length; i < j; i++) {
argsClass[i] = args[i].getClass();
}
Method method = ownerClass.getMethod(methodName, argsClass);
return method.invoke(null, args);
}
//新建实例
public Object newInstance(String className, Object[] args) throws Exception {
Class newoneClass = Class.forName(className);
Class[] argsClass = new Class[args.length];
for (int i = 0, j = args.length; i < j; i++) {
argsClass[i] = args[i].getClass();
}
Constructor cons = newoneClass.getConstructor(argsClass);
return cons.newInstance(args);
}
//判断是否为某个类的实例
public boolean isInstance(Object obj, Class cls) {
return cls.isInstance(obj);
}
//得到数组中的某个元素
public Object getByArray(Object array, int index) {
return Array.get(array,index);
}
Smalltalk中的元类
元类
创建一个新的隐藏类:元类(metaclass)。
元类是描述类的类。
类也是对象,每个类一定是某个元类的实例。
元类为语言提供了一个方法:
- 可以将类的特定行为集成起来。
- 一个类首先并不是作为Class的实例,而是首先作为它的元类的实例,而元类又是继承自Class类。
- 通过继承,元类得到了所有Class的行为,但是它也提供了定义类的特定行为的空间。
元类的继承层次
类的继承关系与相应的元类的继承关系是平行的。
如果B是A的子类,则B的元类也是A的元类的子类。
object是根类(无超类),而其相应的元类objectclass还有一个抽象超类class。
类层次结构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wmEz6vOb-1600225751545)(/Users/yuxiangning/Library/Application Support/typora-user-images/image-20200425141700493.png)]
对应的元类层次结构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8DMTdLVM-1600225751546)(/Users/yuxiangning/Library/Application Support/typora-user-images/image-20200425141826271.png)]
引入元类的优点
- 概念上一致:只用一个概念——对象就可表述系统中所有成分
- 使类成为运行时刻一部分,有助于改善程序设计环境
- 继承的规范化:类与元类的继承采用双轨制
Smalltalk的类库和元体系结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QxPE6HtD-1600225751548)(/Users/yuxiangning/Library/Application Support/typora-user-images/image-20200425132202953.png)]
类MetaPlayingCard中的行为,只能被对象PlayingCard所理解,而不能被其他对象所理解。对象PlayingCard是类MetaPlayingCard的唯一实例。
- 根类Object。所有类是Object的派生类。
- 每个类是其元类的实例。
- 每个元类是类Metaclass的实例。
- 所有元类是类Class的派生类。
类中描述对象的个体性质
- 实例属性(变量)
- 实例方法
元类中描述对象的公共性质
- 类属性(变量)
- 类方法
对象的产生方式
对象的产生有两种基本方式。
-
一种是以原型(prototype)对象为基础产生新的对象。
-
一种是以**类(class)**为基础产生新对象。
原型的概念已经在认知心理学中被用来解释概念学习的递增特性,原型模型本身就是企图通过提供一个有代表性的对象为基础来产生各种新的对象,并由此继续产生更符合实际应用的对象。而原型-委托也是OOP中的对象抽象,代码共享机制中的一种。
一个类提供了一个或者多个对象的通用性描叙。从形式化的观点看,类与类型有关,因此一个类相当于是从该类中产生的实例的集合。而这样的观点也会带来一些矛盾,比较典型的就是在继承体系下,子集(子类)对象和父集(父类)对象之间的行为相融性可能很难达到,这也就是OOP中常被引用的——子类型(subtype)不等于子类(subclass)[Budd 2002]。
而在一种所有皆对象的世界观背景下,在类模型基础上还诞生出了一种拥有元类(metaclass)的新对象模型。即类本身也是一种其他类的对象。
以上三种根本不同的观点各自定义了三种基于类(class-based),基于原型(prototype-based)和基于元类 (metaclass-based)的对象模型。
而这三种对象模型也就导致了许多不同的程序设计语言(如果我们暂时把静态与动态的差别放在一边)。是的, 我们经常接触的C++ ,Java都是使用基于类的对象模型,但除此之外还有很多我们所没有接触的OOPL采用了完全不一样的对象模型,他们是在用另外一种观点诠释OOP的内涵。
第五章 继承和替换
继承
继承作为一种扩展同时也作为一种收缩的思想,正是面向对象技术强大的原因,同时也会在正常的部署中引起混淆。
继承总是向下传递的,因此一个类可以从它上面的多个超类中继承各种属性 。派生类可以覆盖从基类继承来的行为。
is-a检验(“是一个”检验):检验两个概念是否为继承关系。
继承的作用:
- 代码复用
- 概念复用。共享方法的定义。
访问控制
Java与C++中权限
-
private:只能用于父类内部。
-
public:可用于类定义外部。
-
protected(注意区别):
Java中:默认对本包和所有子类可见。
C++中:只能被本类或者子类访问。
Java中的访问控制
可见/访问性 | 在同一类中 | 同一包中 | 不同包中 | 同一包子类中 | 不同包子类中 |
---|---|---|---|---|---|
public | yes | yes | yes | yes | yes |
protected | yes | yes | no | yes | yes |
package | yes | yes | no | yes | no |
private | yes | no | no | no | no |
- 在java中有public、protected、private三种显示的修饰符用于控制可见性,package不是显示的修饰符,它是隐含的,即如果在类、变量等前没加显示的可见性修饰符,那它就是package级别的。如果在类的定义中没有指定package,那么java会把它放在缺省包中,一般来说这个缺省的包就是当前目录。
- 在子类中的方法如果重载了父类的方法,那么该方法的可见级别应更低或者相同,如父类中的方法是public,那么子类中方法必须是public。
- 在java中,一般来说,变量成员最好是private,对它们的访问可以通过public的方法,在这些方法中可以做些控制以保证数据的一致性。这些方法名一般以get和set做为前缀。
不同语言中的继承
面向对象编程语言可以分为两类:
- 第一类语言要求每个类都必须继承于已经存在的父类。(Java,Smalltalk,Objective-C,Delphi Pascal)——优点:存在一个关于所有对象都有的根类。缺点:所有类结合成一个紧密耦合的单元。
- 另一类语言没有这个要求。(C++,Apple Pascal)——优点:可以拥有几个独立的继承层次,不包含整个巨大的类库。缺点:无法定义每个对象都必须包含的功能。
子类(subclass)、子类型(subtype)和替换
静态类型语言中,父类的数据类型和子类(或派生类)的数据类型之间的关系:
- 子类实例必须拥有父类的所有数据成员。
- 子类的实例必须至少通过继承实现父类所定义的所有功能。
- 这样,在某种条件下,如果用子类实例来替换父类实例,那么将会发现子类实例可以完全模拟父类的行为,二者毫无差异。
替换(substitution,也称为归类,subsumption)原则
替换原则:指如果类B是类A的子类,那么在任何情况下都可以用类B来替换类A,而外界毫无察觉。
子类:一个类是通过继承创建的。
子类型:符合替换原则的子类关系。区别于一般的可能不符合替换原则的子类关系。
所有面向对象编程语言都支持替换原则,尽管有些语言在改写方法时需要附加的语法。(C++语言例外,对于C++语言,只有指针和引用真正地支持替换原则,声明为值(不是指针)的变量不支持替换原则)
静态类型语言比动态类型语言更加强调替换原则。
可替换性
可替换性是面向对象编程中一种强大的软件开发技术。
可替换性:变量声明时指定的类型不必与它所容纳的值类型相一致。
这在传统的编程语言中是不允许的,但在面向对象的编程语言中却常常出现。
子类与子类型间的差异
如果说新类是已存在类的子类型,那么这个新类不仅要提供已存在类的所有操作,而且还要满足于这个已存在类相关的所有属性。
因此,即使符合堆栈的接口定义,但是不满足堆栈的属性特征,也不是子类型。
子类型关系是通过行为这个术语描述的,与新类的定义或构造无关。(可以不是继承)
例如:Dictionary类支持与Array类相同的接口,因此即使Dictionary类与Array类之间并不存在继承关系,但是也可以说Dictionary是Array的子类型。
改写和虚拟方法
子类有时为了避免继承父类的行为,需要对其进行改写。
语法上:子类定义一个与父类有着相同名称且类型签名相同的方法。
运行时:变量声明为一个类,它所包含的值来自于子类,与给定消息相对应的方法同时出现于父类和子类。
改写与替换结合时,想要执行的一般都是子类的方法。
改写机制
- Java、Smalltalk等面向对象语言,只要子类通过同一类型签名改写父类的方法,自然便会发生所期望的行为。
- C++中,需要父类中使用关键字virtual来说明是否允许这种替换(子类可能发生改写行为,但是不一定发生改写行为)。
接口和抽象类
语法
Java,C#:abstract
C++:virtual(纯虚方法,并赋值为0)
Java中抽象类与抽象方法的关系
- 抽象方法必须在抽象类中。
- 抽象类中的方法不一定要必须是抽象方法,可以有抽象方法,和非抽象方法。其中非抽象方法,往往都是抽象类的所有子类所具有的,而抽象方法则由具体不同的子类实现不同的方法。
Java中抽象类和接口的区别
-
abstract class 在 Java 语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface.
-
在abstract class 中可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface中,只能够有静态的不能被修改的数据成员(也就是必须是 static final的,不过在 interface中一般不定义数据成员),所有的成员方法都是abstract的。
-
abstract class和interface所反映出的设计理念不同。其实abstract class表示的是"is-a"关系,interface表示的是"like-a"关系。
-
实现抽象类和接口的类必须实现其中的所有方法。抽象类中可以有非抽象方法。接口中则不能有实现方法。
-
接口中定义的变量默认是public static final 型,且必须给其初值,所以实现类中不能重新定义,也不能改变其值。
-
抽象类中的变量默认是 friendly 型,其值可以在子类中重新定义,也可以重新赋值。
-
接口中的方法默认都是 public,abstract 类型的。
继承的形式
- 特殊化(specialization)继承
- 规范化(specification)继承
- 构造(Construction)继承
- 泛化继承
- 扩展继承
- 限制继承
- 变体继承
- 合并继承 (多重继承)
特殊化(specialization)继承
- 在这种形式下,新类是基类的一种特定类型,它能满足基类的所有规范。 用这种方式创建的总是子类型,并明显符合可替换性原则。
- 与规范化继承一起,这两种方式构成了继承最理想的方式,也是一个好的设计所应追求的目标。
很多情况下,都是为了特殊化才使用继承。
规范化继承
- 规范化继承用于保证派生类和基类具有某个共同的接口,即所有的派生类实现了具有相同方法界面的方法。
- 基类中既有已实现的方法,也有只定义了方法接口、留待派生类去实现的方法。派生类只实现定义在基类中却又没有实现的方法。
- 派生类不重新定义已有的类型,而是去实现一个未完成的抽象规范。也就是说,基类定义了某些操作,但并没有去实现它。只有派生类才能实现这些操作。
在这种情况下,基类有时也被称为抽象规范类。
规范化继承可以通过以下方式辨认:基类中只是提供了方法界面,并没有实现具体的行为,具体的行为必须在派生类中实现。
构造继承
- 一个类可以从其基类中继承几乎所有需要的功能,只是改变一些用作类接口的方法名,或是修改方法中的参数列表。
- 即使新类和基类之间并不存在抽象概念上的相关性,这种实现也是可行的。
树-独木舟
堆栈-队列
写二进制文件-写学生信息文件
- 当继承的目的只是用于代码复用时,新创建的子类通常都不是子类型。这称为构造子类化。
- 一般为了继承而继承,如利用一些工具类已有的方法。
构造子类化经常违反替换原则(形成的子类并不是子类型)
泛化子类化
- 与特化子类化相反,对基类已存在的功能进行修改或扩展。
- 派生类扩展基类的行为,形成一种更泛化的抽象。
泛化子类化通常用于基于数据值的整体设计,其次才是基于行为的设计。
扩展继承
- 如果派生类只在基类的基础上添加新行为,但并不修改从基类继承来的任何属性,即是扩展继承。
- 由于基类的功能仍然可以使用,而且并没有被修改,因此扩展继承同样符合可替换性原则,用这种方式构建的派生类还是子类型 。
限制继承
- 如果派生类的行为比基类的少或是更严格时,就是限制继承。
- 常常出现于基类不应该、也不能被修改时。
- 限制继承可描述成这么一种技术:它先接收那些继承来的方法,然后使它们无效。
- 由于限制继承违反了可替换性原则,用它创建的派生类已不是派生类型,因此应该尽可能不用。
双向队列-〉堆栈
合并继承
- 可以通过合并两个或者更多的抽象特性来形成新的抽象。
- 一个类可以继承自多个基类的能力被称为多重继承 。
变体子类化
- 两个或多个类需要实现类似的功能,但他们的抽象概念之间似乎并不存在层次关系。
- 但是,在概念上,任何一个类作为另一个类的子类都不合适。
- 因此,可以选择其中任何一个类作为父类,并改写与设备相关的代码。
- 更好的方法是将两个类的公共代码提炼成一个抽象类,并且让这两个类都继承于这个抽象类。
- 与泛化子类化一样,但基于已经存在的类创建新类时,就不能使用这种方法了。
控制机械鼠标=控制轨迹球
Java中匿名类
创建匿名类(也称为类定义表达式)的条件:
- 只能创建一个匿名类的实例
- 匿名类必须继承于父类或接口,并且不需要构造函数进行初始化。
p.add(new ButtonAdapter(“Quit”){
public void pressed(){System.exit(0);}
}
);
实际上匿名类是一个新类的定义。如上,这个匿名类是基于ButtonAdapter的子类,重写了pressed方法,并且创建了一个关于这个子类的实例。这个新类所需的所有方法都由匿名函数给出。
继承和构造函数
继承使得构造函数这个过程变得复杂
由于父类和子类都有待执行的初始化代码,在创建新对象时都要执行
Java等语言
- 只要父类构造函数不需要参数,父类的构造函数和子类的构造函数都会自动地执行。
- 当父类需要参数时,子类必须显示地提供参数。在java中通过super这个关键字来实现。
C++
是通过在初始化时使用父类的名称来实现这一功能的。
Python
不会自动调用父类的初始化方法。
虚拟析构函数
C++中,如果不将虚构函数声明为virtual,将无法正确调用子类的析构函数。
如果指向父类的指针变量指向子类的一个实例并对此变量进行释放(通过delete语句),那么将只调用父类的析构函数。如果将父类的析构函数声明为virtual,那么父类的析构函数和子类的析构函数都将执行。
class Parent{
public:
~Parent(){ cout << "in parentn"; }
};
class Child : public Parent{
public:
~Child(){ cout << "in childn"; }
}
Parent *p = new Child();
delete p;
//输出
in parent
C++中将析构函数声明为虚拟是一个良好的习惯。
继承的优点和代价
优点
- 软件的可复用性
- 代码共享
- 接口的一致性
- 软件组件
- 快速原型法(探索编程)
- 多态和框架
- 信息隐藏
代价
- 程序执行速度
- 程序大小
- 消息传递的开销
- 程序复杂性
第六章 静态行为和动态行为
静态行为和动态行为
**静态:**用来表示在编译时绑定于对象并且不允许以后对其进行修改的属性或特征。
**动态:**用来表示直到运行时绑定于对象的属性或特征。
静态类和动态类
- 变量的静态类是指用于声明变量的类。静态类在编译时就确定下来,并且再也不会改变。
- 变量的动态类指与变量所表示的当前数值相关的类。动态类在程序的执行过程中,当对变量赋新值时可以改变。
Object obj = new Dog();
在上述代码中,Object
是静态类,Dog
则是动态类
静态类型和动态类型的区别
对于静态类型面向对象编程语言,在编译时消息传递表达式的合法性不是基于接收器的当前动态数值,而是基于接收器的静态类来决定的。
向上/下造型
向上造型
如果有
class Animal{
public void speak(){ System.out.print("Animal speak");}
}
class Dog extends Animal{
public void speak(){ bark();}
public void bark(){ System.out.print("Woof!");}
}
则
Dog fido;
fido = new Dog();
fido.speak();//输出 Woof!
fido.bark();//输出 Woof!
//but
Animal pet;
pet = fido;
pet.speak();//输出 Woof!
pet.bark();//编译时错误
Java中,如果父类是被子类实例化的(向上转型),且子类重写了父类中的某个方法,此时父类调用这个方法,是被子类重写之后的方法。要想调用父类中被重写的方法,则必须使用关键字super。
向上造型语法:
//java
Animal aPet = ...;
if (aPet instanceof Dog){
...
}
//cpp
Animal * aPet = ...;
Dog * d = dynamic_cast<Dog *>(aPet);
if (d != 0){
...
}
向下造型(down casting,或反多态,reverse polymorphism)
做出数值(变量)是否属于指定类(使用instanceof等)的决定之后,通常下一步就是将这一数值的类型由父类转换为子类。这一过程称为向下造型,或者反多态,因为这一操作所产生的效果恰好与多态赋值的效果相反。
几种语言的函数在造型转换成功时返回有效结果,在造型转换非法时返回空值。
//java
Animal aPet;
Dog d;
d = (Dog)aPet;
//cpp
Animal * aPet = ...;
Dog * d = dynamic_cast<Dog *>(aPet);
if (d != 0){
...
}
方法绑定
响应消息时对哪个方法进行绑定是由接收器当前所包含的动态数值来决定的。
静态(前期)绑定
在程序执行前方法已经被绑定(也就是说在编译过程中就已经知道这个方法到底是哪个类中的方法),此时由编译器或其它连接程序实现。例如:C。
针对Java简单的可以理解为程序编译期的绑定;这里特别说明一点,java当中的方法只有final,static,private和构造方法是前期绑定。
动态(后期)绑定
在运行时根据具体对象的类型进行绑定。
若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。
不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。
动态绑定的过程:
- 虚拟机提取对象的实际类型的方法表;
- 虚拟机搜索方法签名;
- 调用方法。
更详细的关于静态绑定和动态绑定的介绍,可以看
多态
如果方法所执行的消息绑定是由最近赋值给变量的数值的类型来决定的,那么就称这个变量是多态的。
-
Java,Smalltalk等变量都是多态的。
-
C++声明为简单类型的变量,非多态。
-
C++中同时满足
1.使用指针或引用;
2.相关方法声明为virtual;
才可以实现多态消息传递。
Java中
class Animal{
public void speak(){ System.out.print("Animal speak");}
}
class Dog extends Animal{
public void speak(){ bark();}
public void bark(){ System.out.print("Woof!");}
}
class Bird extends Animal{
public void speak(){ System.out.print("Tweet!");}
}
Animal pet;
pet = new Dog();
pet.speak();//输出 Woof!
pet = new bird();
pet.speak();//输出 Tweet!
C++中
class Animal{
public:
virtual void speak(){
cout << "Animal Speak !n";
}
void reply(){
cout << "Animal Reply !n";
}
}
class Dog: public Animal{
public:
virtual void speak(){
cout << "Woof! n";
}
void reply(){
cout << "Woof again!n";
}
}
class Bird : public Animal{
public:
virtual void speak(){
cout << "Tweet !n";
}
}
简单类型不多态:
Animal a;
Dog b;
b.speak();//输出 Woof!
a = b;
a.speak();//输出 Animal speak!
Bird c;
c.speak();//输出 Tweet!
a = c;
a.speak();//输出 Animal speak!
指针和引用的对象是多态的:
Animal * d;
Dog b;
d = &b;
(*d).speak();//输出 Woof!
Bird c;
d = & c;
d->speak();//输出 Tweet!
Animal & e;
Dog b;
&e = b;
e.speak();//输出 Woof!
Bird c;
&e = c;
d->speak();//输出 Tweet!
如果省去virtual,指针所指向的对象引用就将不再是多态的了。例如reply方法:
Animal * g;
Dog b;
g = &b;
(*g).reply();//输出 Animal reply!
Bird c;
g = & c;
g->reply();//输出 Animal reply!
//如果接收器的静态类是子类,而不是父类,那么在子类中修改的非虚拟方法也将得以执行。
b.reply();//输出 Woof again!
第七章 替换与继承
内存布局
分配方案
最小静态空间分配
- C++使用最小静态空间分配策略,运行高效。
- 只分配基类所需的存储空间。
为了防止采用这种策略时因为多态而引发的程序错误(具体参照P179),C++改变了虚拟方法的调用规则:
- 对于指针/引用变量:当信息调用可能被改写的成员函数时,选择哪个函数取决于接收器的动态数值。
- 对于其他变量:调用虚拟成员函数的方式取决于静态类(变量声明分类),而不取决于动态类(变量所包含的实际数值的类)。
最大静态空间分配
- 分配变量值可能使用的最大存储空间。
- 这一方案不合适,因为需要找到最大的对象,就需要对继承树上的所有对象都进行扫描,然后找到需要分配最大内存的对象才能。
动态内存分配
- 栈中不保存对象值。
- 栈通过指针大小空间来保存标识变量,数据值保存在堆中。
- 指针变量都具有恒定不变的大小,变量赋值时,不会有任何问题。
- Smalltalk、Java都采用该方法。
区分:“分配策略”见第四章中“指针与内存分配”的“内存分配”
复制
-
**浅复制(shallow copy):**共享实例变量。
-
深复制(deep copy):
建立实例变量的新的副本。
- 实现方法: C++:拷贝构造函数,Java:改写clone方法
克隆(clone)
一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。这种过程便是克隆。
在Java语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的手段。
影子clone
下面的例子包含三个类UnCloneA,CloneB,CloneMain。CloneB类包含了一个UnCloneA的实例和一个int类型变量,并且重载clone()方法。CloneMain类初始化CloneB类的一个实例b1,然后调用clone()方法生成了一个b1的拷贝b2。最后考察一下b1和b2的输出:
package clone;
class UnCloneA {
private int i;
public UnCloneA(int ii) { i = ii; }
public void doubleValue() { i *= 2; }
public String toString() {
return Integer.toString(i);
}
}
class CloneB implements Cloneable{
public int aInt;
public UnCloneA unCA = new UnCloneA(111);
public Object clone(){
CloneB o = null;
try{
o = (CloneB)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return o;
}
}
public class CloneMain {
public static void main(String[] a){
CloneB b1 = new CloneB();
b1.aInt = 11;
System.out.println("before clone,b1.aInt = "+ b1.aInt);
System.out.println("before clone,b1.unCA = "+ b1.unCA);
CloneB b2 = (CloneB)b1.clone();
b2.aInt = 22;
b2.unCA.doubleValue();
System.out.println("=================================");
System.out.println("after clone,b1.aInt = "+ b1.aInt);
System.out.println("after clone,b1.unCA = "+ b1.unCA);
System.out.println("=================================");
System.out.println("after clone,b2.aInt = "+ b2.aInt);
System.out.println("after clone,b2.unCA = "+ b2.unCA);
}
}
/** RUN RESULT:
before clone,b1.aInt = 11
before clone,b1.unCA = 111
=================================
after clone,b1.aInt = 11
after clone,b1.unCA = 222
=================================
after clone,b2.aInt = 22
after clone,b2.unCA = 222
*/
输出的结果说明int类型的变量aInt和UnCloneA的实例对象unCA的clone结果不一致,int类型是真正的被clone了,因为改变了b2中的aInt变量,对b1的aInt没有产生影响,也就是说,b2.aInt与b1.aInt已经占据了不同的内存空间,b2.aInt是b1.aInt的一个真正拷贝。相反,对b2.unCA的改变同时改变了b1.unCA,很明显,b2.unCA和b1.unCA是仅仅指向同一个对象的不同引用!从中可以看出,调用Object类中clone()方法产生的效果是:先在内存中开辟一块和原始对象一样的空间,然后原样拷贝原始对象中的内容。对基本数据类型,这样的操作是没有问题的,但对非基本类型变量,我们知道它们保存的仅仅是对象的引用,这也导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。
大多时候,这种clone的结果往往不是我们所希望的结果,这种clone也被称为“影子clone”。要想让b2.unCA指向与b2.unCA不同的对象,而且b2.unCA中还要包含b1.unCA中的信息作为初始信息,就要实现深度clone。
默认的克隆方法为浅克隆,只克隆对象的非引用类型成员。
怎么进行深度clone?
把上面的例子改成深度clone很简单,需要两个改变:
一是让UnCloneA类也实现和CloneB类一样的clone功能(实现Cloneable接口,重载clone()方法)。
二是在CloneB的clone()方法中加入一句o.unCA = (UnCloneA)unCA.clone();
Java中的深拷贝和浅拷贝
Java中常用的拷贝操作有三个:
- operator =
- 拷贝构造函数
- clone()方法
预定义非集合类型的拷贝过程
int x=1;
int y=x;
y=2;
x is 1
y is 2
Integer a=1;
Integer b=a;
b=2;
a is 1
b is 2
String m="ok";
String n=m;
n="no";
m is "ok";
n is "no";
Integer a=1;
Integer b=new Integer(a);
b=2;
a is 1
b is 2
String m="ok";
String n=new String(m);
n="no";
m is "ok“;
n is "no";
自定义类型的拷贝过程
public class Person implements Cloneable {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
Person p = new Person();
p.setAge(32);
p.setName("陈抒");
Person p2 = p;
p2.setAge(33);
p2.setName("老陈");
System.out.println(p.getAge());
System.out.println(p.getName());
System.out.println(p2.getAge());
System.out.println(p2.getName());
}
}
//输出:
33
老陈
33
老陈
可查找关于“Java中的深拷贝和浅拷贝”了解更多
第八章 多重继承
指一个对象可以有两个或更多不同的父类,并可以继承每个父类的数据和行为。
多重继承的问题
名称歧义
解决方法一:
使用全限定名
GraphicalCardDeck gcd;
Card *aCard = gcd->CardDeck::draw();
Card *aCard = gcd->GraphicalObject::draw();
不够理想:
- 语法上与其他的函数调用语法不同
- 程序员必须记住哪个方法来自于哪个类
解决方案二:
使用重定义和重命名的结合
class GraphicalCardDeck : public CardDeck, public GraphicalObject {
public:
virtual Card*draw () { return CardDeck::draw(); }
virtual void draw(Graphics *g) { GraphicalObject::draw(g); }
}
GraphicalCardDeck gcd;
Graphis g;
gcd->draw(); // selects CardDeck draw
gcd->draw(g); // selects GraphicalObject draw
class GraphicalCardDeck : public CardDeck, public GraphicalObject {
public:
virtual void draw () { return CardDeck::draw(); }
virtual void paint () { GraphicalObject::draw(); }
}
GraphicalCardDeck gcd;
gcd->draw(); // selects CardDeck draw
gcd->paint(); // selects GraphicalObject draw
对替换的影响
名称重定义仅解决了单独使用GraphicalCardDeck类时的部分问题。
考虑使用替换原则带来的问题?
GraphicalObject处于图形对象组成的列表中
GraphicalObject * g = new GraphicalCardDeck();
g->draw(); // opps, doing wrong method!
希望执行显示图像,结果执行了CardDeck中对应的draw方法,而不是图形操作。
在C++语言中,解决这一问题的典型方法就是引入两个新的辅助类。并且使用不同的方法名称来重定义draw操作。
Class CardDeckParent : public CardDeck {
Public :
virtual void draw () { cardDeckDraw ();}
virtual void cardDeckDraw () { CardDeck :: draw ();}
};
Class GraphicalObjectParent : public GraphicalObject {
Public :
virtual void draw () { goDraw ();}
virtual void goDraw () {GraphicalObject :: draw ();}
};
Class GraphicalCardDeck : public CardDeckParent, GraphicalObjectParent {
Public :
virtual void cardDeckDraw () { }
virtual void goDraw () { }
};
- 子类继承这些新的父类,改写相应的新方法。
- 当独立使用子类时,新的子类对两个行为都可以访问。
- 当以替换的方式对该对象赋值给任何一个父类的实例时,都会产生所希望的行为。
GraphicalCardDeck * gcd = new GraphicalCardDeck();
CardDeck * cd = gcd;
GraphicalObject * go = gcd;
cd->draw();//execute cardDeckDraw
go->draw();//execute goDraw
gcd->draw();//error
接口的多重继承
Java,C#语言都不支持类的多重继承,但它们都支持接口的多重继承。
对于子类来说,接口不为其提供任何代码,所以不会产生两部分继承代码之间的冲突。
- 两个父类接口中的方法具有相同的类型签名,此时两个方法将得以合并。子类只需实现一个方法。
interface CardDeck {
public void draw ()
}
interface GraphicalObject {
public void draw ()
}
class GraphicalCardDeck implements CardDeck, GraphicalObject {
public void draw (){ … } //Only one method
}
- 两个父类中的方法具有不同类型签名,此时子类必须同时实现两个方法。
interface CardDeck {
public void draw ();
}
interface GraphicalObject {
public void draw (Graphics g);
}
class GraphicalCardDeck implements CardDeck, GraphicalObject {
public void draw (){ … }
public void draw (Graphics g){ … }
}
- 不能通过返回值的类型或抛出异常的类型来区分两个方法。否则会报错。
interface CardDeck {
public void draw () throws EmptyDeckException;
}
interface GraphicalObject {
public void draw ();
}
class GraphicalCardDeck implements CardDeck, GraphicalObject {
public void draw (){ … } //error
}
- Java中允许接口定义final常量。如果两个父接口使用相同的名称和不同数值来定义常量,此时会报错。
继承于公共祖先
子类应该拥有公共基类数据成员的一份还是两份拷贝? 实际中两种现象都存在。
情况一:
class Link{
public:
Link *nextLink;
}
class CardDeck:public Link{..};
class GraphicalObject:public Link{..}
当创建同时继承自CardDeck类和GraphicalObject类的子类时,这个子类应该包含多少个nextLink字段?
假如纸牌列表和图形对象列表是不同的,那么每种类型的列表都应该有各自的链接。因此,子类用于两个独立的链接字段看起来更为恰当。
情况二:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yGo0obYb-1600225751549)(/Users/yuxiangning/Desktop/图片1.png)]
输入输出流既是输入流的派生,也是输出流的派生。但是,只存在一个对象,两个文件指针引用相同的数值。即指需要公共祖先的一份拷贝。
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 *)
..};
在C++语言中,这个问题通过在父类列表中使用virtual修饰符来解决。
关键字virtual意味着在当前派生类中,超类可以出现多次,但只包含超类的一份拷贝。
它需要将处于中间状态的父类而不是最终的结合类,指定为虚拟类型。
构造函数与析构函数调用顺序
构造函数也遵从先祖先(基类),再客人(成员对象),后自己(派生类)的原则,有多个基类之间则严格按照派生定义时从左到右的顺序来排列先后。析构函数调用正好相反。
C++中虚基类的初始化顺序
虚基类:是用关键字virtual声明继承的父类,即便该基类在多条链路上被一个子类继承,但是该子类中只包含一个该虚基类的备份,虚基类主要用来解决继承中的二义性问题,这就是是虚基类的作用所在。
一般情况下,派生类的构造函数只需负责对其直接基类初始化,再由直接基类负责对间接基类初始化。
而对虚基类的派生类:
- 在最后的派生类中不仅要负责对其直接基类进行初始化,还要对虚基类初始化(默认是无参,除非特别指定参数)。并且虚基类的构造函数的调用早于其他非虚基类的构造函数的调用。
- 只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类对虚基类的构造函数的调用。
- 若同一层次中包含多个虚基类,这些虚基类的构造函数按其说明次序调用。
- 若虚基类由非虚基类派生而来,则仍先调用基类构造函数,再调用派生类构造函数。
代码一:
#include <iostream>
using namespace std;
class A
{
public:
A(){cout<< 'a';}
int a;
};
class B: public A
{
public:
B(){cout<< 'b';}
int b;
};
class C: public A
{
public:
C(){cout<< 'c';}
int c;
};
class D: public B, public C
{
public:
D(){cout<< 'd';};
int d;
};
int main()
{
D d;
return 0;
}
//abacd
程序输出abacd,调用类D的构造函数会先调用类D的直接基类的构造函数,因为按基类出现的顺序调用构成函数,所以先调用类B的构造函数,同理:调用类B的构造函数,先调用类A的构造函数,所以输出abacd。
代码二:
<pre name="code" class="cpp">#include <iostream>
using namespace std;
int t= 0;
class A
{
public:
A(){cout<< 'a'; t++;}
A(int i){cout<< 'a'+ i; t++;}
int a;
};
class B: virtual public A
{
public:
B():A(7){cout<< 'b';}
int b;
};
class C: virtual public A
{
public:
C():A(5){cout<< 'c';}
int c;
};
class D: public B, public C
{
public:
D():B(),C(){cout<< 'd';};
int d;
};
int main()
{
D d;
cout<< t;
return 0;
}
//abcd
类D的构造函数通过初始化调用了虚基类的构造函数A,然后再调用类B和类C的构造函数。
D对象中只含一个A的对象数据。
- 问:那么这里类B和类C的构造函数会不会调用虚基类的构造函数A呢?
不会, 因为C++编译器只执行最后的派生类对虚基类的构造函数的调用,而忽略基类的其他派生类对虚基类的构造函数的调用。
- 问:如何不使用虚基类的无参构造函数,而是强制提供参数呢?
将D类改成如下即可
class D: public B, public C
{
public:
D():A(2),B(),C(){cout<< 'd';};
int d;
};
公共祖先中的虚拟方法
见《面向对象编程导论》原书第3版 中文版 P204。
内部类
Java中的嵌套类可以访问外部类的方法。C++语言禁止这样使用,但可以通过构建内部类时,传递外部类的引用参数来模拟这一行为。
为了创建一个继承于两个父类的对象,可以使外部类继承自第一个父类,而内部类继承于第二个父类。外部类改写来自第一个父类的方法。内部类改写来自第二个父类的方法。
Class GraphicalCardDeck extends CardDeck {
public void draw ( ) { //外部类改写CardDeck的draw()方法
}
private drawingClass drawer= new drawingClass();
public GraphicalObject myDrawingObject () {return drawer;}
private class drawingClass extends GraphicalObject {
public void draw ( ) {//内部类改写GraphicalObject的draw()方法
}
}
}
存在的问题
替换问题:GraphicalCardDeck类的实例可以赋值给类型为CardDeck的变量,却不能赋值给类型为GraphicalObject的变量。
解决方案:外部类可以返回一个关于内部类的实例,并赋值给父类变量。
第九章 多态及软件复用
多态的形式
- 重载(专用多态):类型签名区分。
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){……}
}
- 改写(包含多态):层次关系中,相同类型签名。是重载的一种特殊情况,但是只发生在有父类和子类关系的上下文中。
Class parent{
public void example(int x){……}
}
Class child extends parent{
//same name,different method body
public void example(int x){……}
}
- 多态变量(赋值多态):声明与包含不同
Parent p=new child();//declared as parent,holding child value
- 泛型(模板):创建通用工具
Template <class T> T max(T left,T right)
{
//return largest argument
if (left<right)
return right;
return left;
}
软件复用机制
两种常用的软件复用机制:
- 组合(分层):提供了一种利用已存在的软件组件来创建新的应用程序的方法。
- 继承
举例:书P209:List和set例子
组合和继承的比较
- 组合是较为简单的一种技术。优点是在特定的数据结构中需要执行哪些操作。无需考虑列表类所定义的所有操作。
- 继承无法知道一个方法是否可以合法地应用于集合。
- 使用继承构建数据抽象的代码的简洁性是继承的一个优点。
- 继承无法防止用户使用父类的方法来操纵新的数据结构:FirstElement。
多态
- 重载(overloading)
- 改写(overriding)
- 多态变量(polymorphic variable)
- 泛型(generics)
第十章 重载(专用多态)
- 重载是在编译时执行的(早绑定),而改写是在运行时选择的(后期绑定)。
- 重载是多态的一种很强大的形式。
- 非面向对象语言也支持。
函数签名
函数类型签名是关于函数参数类型、参数顺序和返回值类型的描述。
类型签名通常不包括接收器类型。因此,父类中方法的类型签名可以与子类中方法的类型签名相同。
范畴
对于一个程序代码中的任何位置,都存在着多个活动的范畴。
类成员方法同时具有类范畴和本地范畴
通过类型签名和范畴可以对重载进行两种分类:
- 基于具有不同范畴的方法。
- 基于具有不同类型签名的方法。
基于类型签名的重载
多个过程(或函数、方法)允许共享同一名称,且通过该过程所需的参数数目、顺序和类型来对它们进行区分。即使函数处于同一上下文,这也是合法的。
class Example{
//same name,three different 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;}
}
关于重载的解析,是在编译时基于参数值的静态类型完成的。涉及运行时机制。【重要】
Class Parent { };
Class Child : public Parent { };
void Test(Parent *p) { }
void Test(Child *c) { }
Parent * value = new Child( );
//Test(value);会执行第一个方法!
C++语言中的流输出
通过重载可以将一个库函数扩展成支持用户定义的数据类型的库函数。
Ostream & operator << (ostream & destination, Fraction & source )
{
destination << source.numerator()<<“/”<<source.denominator();
return destination;
}
强制、转换和造型
强制是一种隐式的类型转换,它发生在无需显式引用的程序中。
double x=2.8;
int i=3;
x=i+x;//integer i will be converted to real
转换表示程序员所进行的显式类型转换。在许多语言里这种转换操作称为“造型”。
x=((double)i)+x;
造型和转换既可以实现基本含义的改变(例如将实数变为整数);也可以实现类型的转换,而保持含义不变(子类指针转换为父类指针)。
//x是y的父类
//上溯造型
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
当一个语句涉及隐式转换、显示转换和面向对象数值的替换时,用来解决重载函数名称的算法将变得非常复杂。通常,这个算法至少包含一下几个步骤:
- 如果存在一个与实参精确匹配的方法,就调用这个方法。
- 否则,检查是否存在使用标准类型提升(例如,从短整数转换成标准整数)的匹配。
- 否则,检查是否存在使用标准类型转换(例如,将子类型解释父类型的实例)的匹配。
- 否则,检查是否存在使用用户提供的转换的匹配。
- 否则,如果不存在匹配或者超过一个方法符合匹配原则,那么就发生编译时错误。
基于范畴的重载
相同的名称可以在不引起歧义且不造成精度损失的情况下出现于多个不同的范畴。
并不一定语义要相关。
重定义
- 当子类定义了一个与父类具有相同名称但类型签名不同的方法时,发生重定义。
- 类型签名的变化是重定义区别于改写的主要依据。
- 两种不同的技术解析重定义:融和模型和分级模型。
融合模型
Java使用融合模型,对所有可能的方法进行检测,选择最匹配的方案。
class Parent {
public void example (int a)
{System.out.println(“in parent method”);}
}
class Child extends Parent {
public void example (int a,int b)
{System.out.println(“in child method”);}
}
//main方法中:
Child aChild = new Child();
aChild.example(3);
对于Java和C#来说会调用父类方法。
分级模型
C++使用分级模型,即在名称定义所在的范畴内进行匹配。
上述逻辑的代码在C++中会编译出错,解决的方法是在Child
类中增加一个一个参数的example()
方法
class Parent {
public void example (int a)
{System.out.println(“in parent method”);}
}
class Child extends Parent {
public void example (int a)
{Parent::example(a);}
public void example (int a,int b)
{System.out.println(“in child method”);}
}
第十一章 改写(包含多态)
如果子类的方法具有与父类的方法相同的名称和类型签名,称子类的方法改写了父类的方法。
重载和改写的区别
- 对于改写来说,方法所在的类之间必须符合父类/子类继承关系,而对于简单的重载来说,并无此要求。
- 如果发生改写,两个方法的类型签名必须匹配。
- 重载方法总是独立的,而对于改写的两个方法,有时会结合起来一起实现某种行为。
- 重载通常是在编译时解析的,而改写则是一种运行时机制。对于任何给定的消息,都无法预言将会执行何种行为,而只有到程序实际运行的时候才能对其进行确定。
标识改写
各种语言在如何通过代码实现标识改写这方面存在着差异。(Java不需要,C++需要virtual)
改写并不能改变方法的可存取性。如果一个方法在父类中为public,那么不允许在子类中将该方法声明为private。反之亦然。
代替与改进
两种不同的关于改写的解释方式:
- 代替(replacement):在程序执行时,实现代替的方法完全覆盖父类的方法。即,当操作子类实例时,父类的代码完全不会执行。
- 改进(refinement):实现改进的方法将继承自父类的方法的执行作为其行为的一部分。这样父类的行为得以保留且扩充。
这两种形式的改写都很有用,并且经常在一种编程语言内同时出现。如:几乎所有的语言在构造函数中都使用改进语义。
延迟(deferred)方法
如果方法在父类中定义,但并没有对其进行实现,那么我们称这个方法为延迟方法。
延迟方法有时也称为抽象方法,并且在C++语言中通常称之为纯虚方法。
延迟方法的一个优点就是可以使程序员在比实际对象的抽象层次更高的级别上考虑与之相关的活动。
延迟方法更具实际意义的原因:在静态类型面向对象语言中,对于给定对象,只有当编译器可以确认与给定消息选择器相匹配的响应方法时,才允许程序员发送消息给这个对象。
Java,C#:abstract
C++:virtual = 0
最后
以上就是危机月饼为你收集整理的山东大学面向对象学习笔记第一章 面向对象思想第二章 抽象第三章 类和方法第四章 消息、实例和初始化第五章 继承和替换第六章 静态行为和动态行为第七章 替换与继承第八章 多重继承第九章 多态及软件复用第十章 重载(专用多态)第十一章 改写(包含多态)的全部内容,希望文章能够帮你解决山东大学面向对象学习笔记第一章 面向对象思想第二章 抽象第三章 类和方法第四章 消息、实例和初始化第五章 继承和替换第六章 静态行为和动态行为第七章 替换与继承第八章 多重继承第九章 多态及软件复用第十章 重载(专用多态)第十一章 改写(包含多态)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复