山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结

前言

这篇文章里的总结大部分来自老师的PPT,然后还有一些自己对概念的补充(也就是网上扒的很多知识讲解和自己对于一些概念的理解),希望能对后面的学弟学妹们在复习的时候有点帮助φ(゜▽゜*)♪
这里是总结的pdf版本(和下面的总结长得一样一样的):
链接:
https://pan.baidu.com/s/1Tc13gxYOnCLgPZaJ2vySHQ?pwd=1234
提取码:1234
点击跳转
这里是总结的md版本(想对总结进行修改的小伙伴可以来这里):
链接:https://pan.baidu.com/s/1gk2GPVMTMdERAwDmVfg65g?pwd=1234
提取码:1234
点击跳转

1 基本概念

  • 属性:在类中表示对象或实体拥有的特性时称为属性
  • 方法:对象执行的操作称为方法。
  • 静态属性:多个对象都可以对静态属性进行操作, 实现同类多个对象间的数据共享。
  • 静态方法:静态方法为类所有,可以通过对象来使用,也可以通过类来使用。 但一般提倡通过类名来使用,因为静态方法只要定义了类,不必建立类的实例就可使用。 静态方法只能调用静态变量;没有伪变量this。构造和析构函数不能为静态成员。
1.1 变量
1.1.1 静态变量
  • 被一个类的所有实例共享的公共数据字段。

  • Java和C++使用修饰符static创建共享数据字段。

  • Java:静态数据字段的初始化是在加载类时,执行静态块来完成。

c++

由基本数据类型表示的静态数据字段可以在类的主体中进行初始化。

也可在类外对静态数据字段进行初始化。

对象本身不对共享字段初始化。内存管理器自动将共享数据初始化为某特定值,每个实例去测试该特定值。第一个进行测试的做初始化。

class CountingClass {
public:
CountingClass () { count++; ... }
private:
static int count;
};
// global initialization is separate from class
int CountingClass::count = 0;

类型 类名:静态成员=值

java:

在静态变量的声明时初始化,例如

static int i = 5;
static int j = 6;
在静态代码块中初始化,例如
static int i;
static int j;
static{
    i = 5;
    j = 6;
}

这两种初始化方式本质上是一样的。对于前一种初始化方式,会首先声明所有静态变量并赋默认值,然后再按顺序对被初始化的变量重新赋值。所以,上述两段程序的执行顺序相同。

静态变量的初始化时机
在类的生命周期内,静态变量只会被初始化一次。
静态变量的初始化时机分为以下几种情况

静态变量类型 初始化时机
非final类型 类的初始时
final类型—编译时可计算出取值 编译时
final类型—编译时不可计算出取值 类初始化时

静态变量的初始化时机与类的初始化时机紧密相关(final类型的静态变量除外,它编译时初始化)。在类的初始化阶段,java虚拟机执行类的初始化语句,为静态变量赋予初始值、执行静态代码块,所以静态变量的初始化时机即为类的初始化时机。

创建类的实例时,类的初始化顺序为:父类静态变量 --> 子类静态变量 --> 父类实例变量 --> 子类实例变量 --> 父类构造函数 --> 子类构造函数

1.1.2 成员变量

或叫实例域、实例字段(instance field),或叫成员变量(member variable)。实例的变量,每个实例的变量可能不同。

1.1.3 静态变量和普通变量的区别

static全局变量与普通的全局变量
static全局变量只初使化一次,作用域被限制在该变量的源文件内有效,防止在其他文件单元中被引用;

普通全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。

static局部变量和普通局部变量
static局部变量只被初始化一次,下一次依据上一次结果值

1.2 方法
1.2.1 静态成员方法
  • 成员函数。不能访问非静态成员。
  • 类方法可以直接调用类变量和类方法。
  • 无this,因为没有实例存在,“this”不知道引用哪个实例。
  • 构造和析构函数不能为静态成员。

注意这样一个特殊的情形:如果已有一个类变量,再定义一个方法去操作这个类变量。那么此方法可以不加static修饰符,本质上仍然是一个类方法。但是,现在就只能通过该类的一个实例,才能调用这个方法(常见于面试中)。

public static String classVariable = "Class variable.";
// 修改上述方法
public void instanceMethod() {
        System.out.println("Instance method.");
        // 修改
        classVariable += "append";
        System.out.println("classVariable:"+classVariable);
}
1.2.2 普通成员方法

实例方法(成员方法):供实例用的方法,必须要先有实例,才能通过此实例调用实例方法

1.2.3 静态方法和普通方法的区别

static方法在内存中只有一份,而普通方法在每个被调用中都会维持一份拷贝

1.调用对象、引用变量不同

对于静态方法:是使用static关键字修饰的方法,又叫类方法。属于类的,不属于对象,在实例化对象之前就可以通过类名.方法名调用静态方法。(静态属性,静态方法都是属于类的,可以直接通过类名调用)。
A.在静态方法中,可以调用静态方法。
B.在静态方法中,不能调用非静态方法。
C.在静态方法中,可以引用类变量(即,static修饰的变量)。
D.在静态方法中,不能引用成员变量(即,没有static修饰的变量)。
E.在静态方法中,不能使用super和this关键字

对于非静态方法:是不含有static关键字修饰的普通方法,又称为实例方法,成员方法。属于对象的,不属于类的。(成员属性,成员方法是属于对象的,必须通过new关键字创建对象后,再通过对象调用)。
A.在普通方法中,可以调用普通方法。
B.在普通方法中,可以调用静态方法
C.在普通方法中,可以引用类变量和成员变量
D.在普通方法中,可以使用super和this关键字

2.调用方法不同

静态方法可以直接调用,类名调用和对象调用。(类名.方法名 / 对象名.方法名)
但是非静态方法只能通过对象调用。(对象名.方法名)

3.生命周期不同

静态方法的生命周期跟相应的类一样长,静态方法和静态变量会随着类的定义而被分配和装载入内存中。一直到线程结束,静态方法和静态属性才会被销毁。(也就是静态方法属于类)
非静态方法的生命周期和类的实例化对象一样长,只有当类实例化了一个对象,非静态方法才会被创建,而当这个对象被销毁时,非静态方法也马上被销毁。(也就是非静态方法属于对象)

 class ABC{
 public static void testStatic(){
	 System.out.println("This is static method");
 }
 public void testMethod(){
 	 System.out.println("This is instance method");
 }

 public static void main(String[] str){
      ABC.testStatic();         //直接通过类调用

      ABC a = new ABC();        //实例化,然后构造方法会初始化
      a.testMethod();           //对象调用方法
}
}
1.3 常量

常量变量都属于变量,只不过常量是赋过值后不能再改变的变量,而普通的变量可以再进行赋值操作。

字面量是指由字母,数字等构成的字符串或者数值,它只能作为右值出现,所谓右值是指等号右边的值,如:int a = 123这里的a为左值,123为右值。

java用final定义常量

c++用const定义常量(与此同时,还有define和enum枚举)

// a变量
int a;
// b为常量,10为字面量
final int b = 10;
// str为变量,hello world为字面量
String str = "hello world";

类和对象的区别:

类是概念模型,由对象抽象而来,定义对象的所有特性和所需的操作;对象是类的实例化

在响应消息时调用何种方法由类的接收器来决定。一个特定类的所有对象使用相同的方法来响应类似的消息。

对象是实际的实体,真实的模型,所有属于同一个类的对象都具有相同的特性和操作。
对象 = 状态(实例变量)+行为(方法)

接收器:
消息发送的对象。如果接收器接受了消息,那么同时它也接受了消息所包含的行为责任。然后,接受器响应消息,执行相应的“方法”以实现要求。

消息的解释由接收器决定,并且随着接收器的不同而不同。

责任:
用责任来描述行为。A对行为的要求仅表明所期望的结果,C可随意选择使用的方法来实现所期待的目标,并在此过程中不受A的干扰。

提高了抽象水平,对象更加独立。

不干预原则:允许对象以任何它认为合适的不干涉其他对象的方式来完成任务,而不要干预它。

1.4 修饰符

1.4.1 Java访问修饰符

就是确定类中属性或方法的访问权限,换句话说,就是这些属性和方法所起的作用范围。

private,私有的访问权限,也是最严格的访问权限,仅只能在设置了该权限的类中访问,利用这个访问权限,表现出封装思想。

default,默认的访问权限,也是可以省略的访问权限,它不仅能在设置了该权限的类中访问,也可以在同一包中的类或子类中访问。(如果非同一个包,就算是子类也不可以访问

protected,受保护的访问权限,它除了具有default的访问权限外,还可以在不同包中所继承的子类访问。

public,公有的访问权限,也是最宽松的访问权限,不仅可以是同一个类或子类,还是同一个包中的类或子类,又还是不同包中的类或子类,都可以访问。

1.5 声明次序建议

  • 先列出主要特征,次要的列在后面。

  • 私有数据字段列在后面。

  • 构造函数列在前面。

2 对象

2.1 概念

可以简单看成状态行为的结合

  • 状态: 用实例变量来描述

  • 行为:用方法来表示

从对象外部来看,只能够看到对象的行为

而从对象的内部来看,方法通过修改对象的状态,以及和其他对象的相互作用,提供适当的行为

2.2 对象性质

  • 封装性:信息隐藏
  • 自治性:主动数据
  • 通信性:并发
  • 暂存性:作用域/期
  • 永久性:文档串行化

2.3 对象创建

2.3.1 语法

C++

PlayingCard * aCard = new PlayingCard(Diamond, 3);

Java, C#

PlayingCard aCard = new PlayingCard(Diamond, 3);

Smalltalk

aCard <- PlayingCard new.
2.3.2 对象数组的创建

所谓对象数组,指每一个数组元素都是对象的数组,即若一个类有若干个对象,我们把这一系列的对象用一个数组来存放。对象数组的元素是对象,不仅具有数据成员,而且还有函数成员。

涉及问题:

  • 数组自身的分配和创建
  • 数组所包含的对象的分配和创建

在C++中,这两个问题是结合起来的,数组由对象组成,而每个对象则使用缺省(即无参)构造函数来进行初始化。

Linklist* link = new Linklist[2];
link[0].Head->data = 2;

在java中,用来创建数组的new操作符只能用来创建数组,数组包含的每个数值必须独立创建,典型的方法是通过循环来实现。

Linklist link[] = new Linklist[2];
for(int i=0;i<2;i++){
link[i] = new Linklist();
}
2.3.3 变量声明和初始化结合
	Student lihua = new Student();
      lihua.name="李华";
      lihua.age=19;
2.3.4 变量声明和创建分离
	Student lihua;
     lihua= new Student();

消息和对象

对象之间的相互作用是通过消息产生。消息由某个对象发出请求其他某个对象执行某一处理或回答某些信息。

类和实例和对象的区别

  • 对象是类的实例,类是对象的模板。

  • 对象是对客观事物的抽象,类是对对象的抽象。

  • 在面向对象程序设计中,“类"在实例化之后叫做一个"实例”(Person person = new Person())。

    "类"是静态的,不占进程内存,而"实例"拥有动态内存。

  • 实例和对象的区别

    • 对象就是类的实例,所有的对象都是类的实例,但并不是所有的实例都是对象。

      抽象类被定义为永远不会也不能被实例化为具体的对象。

      有一种对象只叫对象,有一种对象叫实例化对象(实例)。

      我们知道抽象类是不可以被实例化的,那它的对象就不能叫实例化对象,只能叫对象,如下:

      Type type = typeof(int);//Type是抽象类,不允许实例化,这里的type是Type类的对象,而非实例化对象(实例)
      

      而普通类的对象,既可以叫对象,也可以叫实例化对象(实例),如下:

      class Person{}
      
      class Program
      {
          static void Main(string[] args)
          {
              Person person = new Person();//这里person既可以叫做Person类的对象,也可以叫实例化对象(实例)    
          }
      }    
      

3实例

表示类的一个具体代表或者范例

3.1 实例变量

表示实例所维护的内部变量,还有数据字段数据成员代表一个意思

4 消息

消息传递(方法查询)表示请求对象执行一项特定行为的动态过程

4.1概念

对象间相互请求或相互协作的途径

  • 对象接收多个消息,响应不同(文理科)
  • 同一消息给多个对象,响应不同(考学结果)
  • 广播,可响应可不响应(当前课堂)
4.1.1 消息表达式(3个部分)

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第1张图片

  1. 接收器:消息传递的目的函数

  2. 消息选择器:表示待传递的特定的消息文本

  3. 参数:用于响应消息的参数

    更加普遍的说法:

    • 接受消息的对象**:接收器**
    • 接收对象要采取的方法**:消息选择器**
    • 方法需要的**:参数**

信息隐藏:

作为某对象提供服务的一个用户,只需要知道对象将接受的消息的名字,不需要知道要完成要求需要执行哪些动作。 对象在接收到消息后,会负责任务完成。

封装:

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

4.2 消息传递与过程调用

消息传递(message passing):表示请求对象执行一项特定行为的动态过程

  • 每一条消息都有一个指定的接收器相对应,’接收器就是消息发送的对象。

  • 过程调用没有指定的接收器。

  • 消息总是传递给某个称为接收器的对象(消息->接收器)

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

4.3 消息传递语法

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.

4.4 从方法内部存取接收器

伪变量

接收器并不存在于消息选择器(方法)的参数列表中,而是隐藏在方法的定义中。若必须从方法体内部去存取接收器的数值时,就会使用到伪变量(java和c++中的this)(不需要声明,不能被更改)

Java,C++:this
Eiffel:Current
Smalltalk,object-c:self

this隐含指向调用成员函数的对象(一般默认隐藏)

class PlayingCard {...	public void flip () { setFaceUp( ! faceUp ); }...}
class PlayingCard {...public void flip(){this.setFaceUp(!this.faceUp); }...}

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

5. 继承

5.1. 父类和子类数据类型的关系

子类实例必须拥有父类的所有数据成员。(私有成员变量也会拥有,但是无法访问)

子类的实例必须至少通过继承实现父类所定义的所有功能。

这样,在某种条件下,如果用子类实例来替换父类实例,那么将会发现子类实例可以完全模拟父类的行为,二者毫无差异。

5.2. 替换原则

指如果类B是类A的子类,那么在任何情况下都可以用类B来替换类A,而外界毫无察觉。

5.2.1 子类型

指符合替换原则的子类关系。

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

5.3. 改写

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

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

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

改写与替换结合时,想要执行的一般都是子类的方法。

5.3.1 改写机制

Java、Smalltalk等面向对象语言,只要子类通过同一类型签名改写父类的方法,自然便会发生所期望的行为。

C++中,需要父类中使用关键字Virtual来表明这一含义。

遮蔽

是指父类变量接收子类类型,并调用方法或者使用变量时候,使用的父类的方法和变量,而不发生多态的现象。

5.4. 继承形式

5.4.1 特化子类化(子类型化)

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

5.4.2 规范子类化

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

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

5.4.3 构造子类化

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

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

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

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

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

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

5.4.4 泛化子类化

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

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

例子:

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

5.4.5 扩展子类化

如果派生类只是往基类中添加新行为,并不修改从基类继承来的任何属性,即是扩展继承。(泛化子类化对基类已存在的功能进行修改或扩展,扩展子类化则是增加新功能)由于基类的功能仍然可以使用,而且并没有被修改,因此扩展继承并不违反可替换性原则,用这种方式构建的派生类还是派生类型 。

举例:SET- STRINGSET 按前缀查找

5.4.6 限制子类化

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

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

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

举例:双向队列-〉堆栈

5.4.7 变体子类化

两个或多个类需要实现类似的功能,但他们的抽象概念之间似乎并不存在层次关系。

举例:控制机械鼠标=控制轨迹球

在概念上,任何一个类作为另一个类的子类都不合适,因此,可以选择其中任何一个类作为父类,并改写与设备相关的代码

然而,通常使用的更好的方法是将两个类的公共代码提炼成一个抽象类,比如:PointingDevice类,然后让上面的控制机械鼠标和控制轨迹球两个类都继承于这个抽象类。

与泛化子类化一样,但基于已经存在的类创建新类时,就不能使用这种方法了。

5.4.8 结合子类化

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

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

举例:助教

5.5. 复制和克隆

复制
  • 浅复制(shallow copy):共享实例变量。
  • 深复制(deep copy):建立实例变量的新的副本。
    • 实现方法: C++:拷贝构造函数,Java:改写clone方法
克隆

一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。这种过程便是克隆。

影子克隆:

下面的例子包含三个类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很简单,

需要两个改变:

  • 一是让UnCloneA类也实现和CloneB类一样的clone功能(实现Cloneable接口,重载clone()方法)。

  • 二是在CloneB的clone()方法中加入一句o.unCA = (UnCloneA)unCA.clone();

提问:如果两个或更多的方法具有相同的名称和相同的参数数目,java编译器如何匹配?

回答:

  • 按照调用此方法的对象进行匹配。

  • 按照参数类型匹配。

6. 元类

元类是描述类的类,在面向对象思想中,类也是对象,类一定是某个元类的实例

元类的实例化的结果为我们用class定义的类,正如类的实例为对象。

元类对象中存储的是关于类的信息(类的版本,名字,类方法等)。

元类所具有的行为:创建实例,返回类名称,返回类实例大小,返回类实例可识别消息列表。

提问:引入元类的优点有哪些?
回答:

  • 概念上一致:只用一个概念——对象就可表述系统中所有成分
  • 使类成为运行时刻一部分,有助于改善程序设计环境
  • 继承的规范化:类与元类的继承采用双轨制

反射和内省

指程序在运行过程中“了解”自身的能力

用于反射和内省的技术分为两大类:

  • 获取理解当前计算状态的特征
  • 用于修改特征,增加新的行为

反射工具都开始于一个对象,该对象是关于一个类的动态(运行时)体现。类对象是更一般的类(称为Class类)的实例。类对象通常都包括类名称、类实例所占用内存的大小以及创建新实例的能力。

获取类对象

C++:
typeinfo aClass = typeid(AVariable); 
Java:
Class aClass = aVariable.getClass(); 

获取父类对象:

Class parentClass = aClass.getSuperclass(); // Java

类对象操作-检测对象类

如何决定多态变量是否真正包含一个指定子类的实例
Child *c-dynamic_cast(aParentPtr);
	if (c!=0){ … }   //C++

if (aVariable instanceof Father)  …//Java  1
if (aCalss.isInstance(aVariable)) …  //java  2
//A instanceof B ,返回值为boolean类型,用来判断A是否是B的实例对象或者B子类的实例对象。

反射的作用:可以用于程序中的错误方法。可以灵活的调用对象,可以在运行时检测或修改程序行为。
反射的缺点:

  • 效率低,因此,能不用反射实现的功能就尽量不使用反射。
  • 暴露内部私有变量和方法
检测对象类

对这种类型检测的不恰当使用是设计类结构不好的一个标志。

大多数情况下,都可以通过调用改写方法来代替显式检测(instanceof)

多态

1. 重载、重写、重定义

1.1 重载 overload

定义:函数名相同,但是参数列表不同,注意main函数不能重载,每个程序的main函数只有一个

特点:(1) 函数名相同 (2) 作用域相同 (3) 参数列表不同 (4) virtual关键字可有可无 (5) 返回值可以不同 (6) 访问修饰符可以不同

例子:某类:void restrictionChanged(int); void restrictionChanged(double);

1.2 重写 override(覆盖)

定义:子类重写基类的虚函数

特点:(1) 函数名相同 (2) 作用域不同 (3) 参数列表相同 (4) 基类函数必须有virtual关键字且不能有static (5) 返回值相同 (6) 重写函数的访问修饰符可以不同

例子:基类:virtual void restrictionChanged(); 子类:void restrictionChanged()

覆盖指的是子类覆盖父类函数(被覆盖)

1.分别位于子类和父类中

2.函数名字与参数都相同

3.父类的函数是虚函数(virtual)

1.3 重定义 overwrite(隐藏)

定义:子类重定义基类的函数

特点:

  • 函数名相同

  • 作用域不同

  • 返回值可以不同

  • 访问修饰符可以不同

  • 参数不同

参数列表不同:指的是个数或类型,但是不能靠返回类型来判断

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

融合模型

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);

分级模型

C++使用分级模型,即在名称定义所在的范畴内进行匹配。

上述逻辑main方法中的代码在C++中会编译出错,解决的方法是在Child类中增加一个一个参数的example()方法,但是aChild.example(1,2)是可以的

一个参数的时候报错,说明基类方法已经被隐藏了(重新定义继承函数,原来的函数被隐藏)

2. 概念

多态是面向对象程序设计(OOP)的一个重要特征,指同一个实体同时具有多种形式,即同一个对象,在不同时刻,代表的对象不一样,指的是对象的多种形态

可以把不同的子类对象都当作父类来看,进而屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,统一调用标准。

多态对象只能调用父类中定义子类中重写的功能,并不能调用子类的特有功能,这样就实现了代码的统一

对于动态类型语言,所有的变量都可能是多态的。

对于静态类型语言,多态变量则是替换原则的具体表现。

3.特点

  1. 多态的前提1是继承

  2. 多态的前提2要有方法的重写

  3. 父类引用指向子类对象,如:Animal a = new Cat();

  4. 多态中,编译看左边,运行看右边
    山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第2张图片

4. 多态的形式(4种)

4.1 重载(专用多态):类型签名区分

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

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

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;}
}

关于重载的解析,是在编译时基于参数值的静态类型完成的。不涉及运行时机制。

签名

函数类型签名是关于函数参数类型参数顺序返回值类型的描述。

函数签名经常被用在函数重载,因为调用重载的方法从名字上是无法确定你调用的是哪一个方法,而要从你传入的参数该函数的签名来进行匹配,这样才可以确定你调用的是哪一个函数。

4.1.2 基于范畴的重载

相同的名称可以在不引起歧义不造成精度损失的情况下出现于多个不同的范畴。

并不一定语义要相关

范畴

范畴定义了能够使名称有效使用的一段程序,或者能够使名称有效使用的方式。例如局部变量(其作用于在{}内),public成员。

通过继承创建的新类将同时创建新的名称范畴,该范畴是对父类的名称范畴的扩展

4.2 改写/重写(包含多态):层次关系中,相同类型签名,是重载的一种特殊情况,但是只发生在有父类和子类关系的上下文中。

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

  • 语法上:子类定义一个与父类有着相同名称类型签名相同的方法。
  • 运行时:变量声明为一个类,它所包含的值来自于子类,与给定消息相对应的方法同时出现于父类和子类。

改写替换原则结合时,想要执行的一般都是子类的方法。

改写可看成是重载的一种特殊情况

  • 接收器搜索并执行相应的方法以响应给定的消息。
  • 如果没有找到匹配的方法,搜索就会传导到此类的父类。搜索会在父类链上一直进行下去,直到找到匹配的方法,或者父类链结束。
  • 如果能在更高类层次找到相同名称的方法,所执行的方法就称为改写了继承的行为
4.2.1 改写和重载的对比
  • 继承角度:对于改写来说,方法所在的类之间必须符合父类/子类继承关系,而对于简单的重载来说,并无此要求

  • 类型签名角度:如果发生改写,两个方法的类型签名必须匹配

  • 方法作用角度: 重载方法总是独立的,而对于改写的两个方法,有时会结合起来一起实现某种行为

  • 编译器角度: 重载通常是在编译时解析的,而改写则是一种运行时机制。对于任何给定的消息,都无法预言将会执行何种行为,而只有到程序实际运行的时候才能对其进行确定。

4.2.2 改写与遮蔽的对比
遮蔽

是指父类变量接收子类类型,并调用方法或者使用变量时候,使用的父类的方法和变量,而不发生多态的现象。

class Father
{
public:
	Father();
	void fason();
};
Father::Father() {
	cout << "I am a father" << endl;
}
void Father:: fason() {
	cout << "I am the method in father" << endl;
}
class Son :
    public Father
{
public:
    Son();
    void fason();
};
Son::Son() {
	cout << "I am son" << endl;
}
void Son::fason() {
	cout << "I am method in son" << endl;
}
int main() {
	Father *fs = new Son();
	fs->fason();
	Son* son = new Son();
	son->fason();
	return 0;
}

output

I am a father
I am son
I am the method in father
I am a father
I am son
I am method in son

  • 语法角度:改写与遮蔽存在着外在的语法相似性
  • 编译角度:类似于重载,改写区别于遮蔽的最重要的特征就是,遮蔽是在编译时基于静态类型解析的,并且不需要运行时机制
  • 关键字:几种语言需要对改写显式声明,如果不使用关键字,将产生遮蔽
4.2.3 改写机制在不同语言中的区别
  • Java、Smalltalk等面向对象语言,只要子类通过同一类型签名改写父类的方法,自然便会发生所期望的行为。
  • C++中,需要父类中使用关键字Virtual来表明这一含义(否则会发生遮蔽

上面实例中,Father.h修改成:

class Father
{
public:
	Father();
	virtual void  fason();
};

output

I am a father
I am son
I am method in son
I am a father
I am son
I am method in son

4.2.4 两种不同的关于改写的解释方式
  • 代替(replacement):在程序执行时,实现代替的方法完全覆盖父类的方法。即,当操作子类实例时,父类的代码完全不会执行。
  • 改进(refinement):实现改进的方法将继承自父类的方法的执行作为其行为的一部分。这样父类的行为得以保留且扩充.
    • java中使用super.方法名()
    • c++中可以使用 _super::方法名() 或者 使用 父类名::方法名()

这两种形式的改写都很有用,并且经常在一种编程语言内同时出现
如:几乎所有的语言在构造函数中都使用改进语义

4.2.5 改写、遮蔽和重定义的差异
  • 改写:父类与子类的类型签名相同,并且在父类中将方法声明为虚拟的。
  • 遮蔽:父类与子类的类型签名相同,但是在父类中并不将方法声明为虚拟的。
  • 重定义:父类与子类的类型签名不同

4.3 多态变量(复制多态):声明与包含不同

如果方法所执行的消息绑定是由最近赋值给变量的数值的类型来决定的,那么就称这个变量是多态的。

在C++中,必须使用指针或者引用,并且对相关方法声明为virtual,才可以使用多态消息传递。

4.3.1 延迟方法

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

优点:
可以使程序员在比实际对象的抽象层次更高的级别上考虑与之相关的活动。

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

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

4.3.2. 多态变量形式(4种)
4.3.2.1 简单多态变量(有点继承的感觉)

调用方法时调用变量的动态类型的方法而不是静态类型的方法
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第3张图片

4.3.2.2 接收器变量(内部接口或者父类指向对象声明但未初始化)

多态变量作为一个数值,表示正在执行的方法内部的接收器。包含接收器的变量没有被正常的声明,通常被称为伪变量。

隐藏

伪变量 smalltalk:self,C++,Java,C#:this山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第4张图片山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第5张图片

由于基础方法被子类所继承,因此它们可以用于各个子类实例。

多态变量在框架中的作用
  • 多态接收器功能的强大之处表现在消息传递与改写相结合时。这种结合是软件框架开发的关键。

一般框架系统中的方法分为两大类:

  • 在父类中定义基础方法,被多个子类所继承,但不被改写;
  • 父类定义了关于多种活动的方法,但要延迟到子类才实现。

接收器变量多态性的展现

  • 当执行基础方法时,接收器实际上保存的是一个子类实例的数值。

  • 当执行一个改写方法时,执行的是子类的方法,而不是父类的方法。

4.3.2.3 反多态(向下造型)
4.3.2.3.1 造型
int i=3;//隐式
int i = (int)2.8;//显式

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

转换表示程序员所进行的显式类型转换。在许多语言里这种转换操作称为造型

向上造型:子类的对象可以向上造型为父类的类型。即父类引用子类对象,这种方式被称为向上造型。

使用格式:父类类型 变量名 = new 子类类型()

向下造型:向下造型是处理多态变量的过程,并且在某种意义上这个过程的取消操作就是替换。(把指向子类对象的父类引用赋给子类引用,需要强制转化)

使用格式:子类类型 变量名 = (子类类型)父类类型

instanceof:测试左边的对象是否是右边类的示例

Father f1 = new Son();

Son s1 = (Son)f1;

但有运行出错的情况:

Father f2 = new Father();

Son s2 = (Son)f2;//编译无错但运行会出现错误

在不确定父类引用是否指向子类对象时,可以用instanceof来判断:

if(f3 instanceof Son){

     Son s3 = (Son)f3;

}
4.3.2.4 纯多态(多态方法)
  • 多态方法支持可变参数的函数。

  • 支持代码只编写一次、高级别的抽象

  • 以及针对各种情况所需的代码裁剪。

  • 通常是通过给方法的接收器发送延迟消息来实现这种代码裁剪的。

关于纯多态的一个简单实例就是用JAVA语言编写的StringBuffer类中的append方法。这个方法的参数声明为Object类型,因此可以表示任何对象类型。

Class Stringbuffer{
	String append(Object value){
		return append(value,toString());}}

方法toString在子类中得以重定义。

toString方法的各种不同版本产生不同的结果。

所以append方法也类似产生了各种不同的结果。

Append:一个定义,多种结果。

4.4 泛型(模板):创建通用工具

4.4.1 概念

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

4.4.2 泛型的使用
4.4.2.1 泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

泛型类的最基本写法(这么看可能会有点晕,会在下面的例子中详解):

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
  private 泛型标识 /*(成员变量类型)*/ var; 
  .....

  }
}

一个最普通的泛型类:

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;

    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);

//传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");
Log.d("泛型测试","key is " + genericInteger.getKey());
Log.d("泛型测试","key is " + genericString.getKey());

定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型

Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);

Log.d("泛型测试","key is " + generic.getKey());
Log.d("泛型测试","key is " + generic1.getKey());
Log.d("泛型测试","key is " + generic2.getKey());
Log.d("泛型测试","key is " + generic3.getKey());
D/泛型测试: key is 111111
D/泛型测试: key is 4444
D/泛型测试: key is 55.55
D/泛型测试: key is false
  • 1.泛型的类型参数只能是类类型,不能是简单类型。

  • 2.不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。

    if(ex_num instanceof Generic<Number>){   
    } 
    
4.4.2.2 泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator implements Generator{
 * 如果不声明泛型,如:class FruitGenerator implements Generator,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator,public T next();中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}
泛型通配符

我们知道Ingeter是Number的一个子类,同时在特性章节中我们也验证过GenericGeneric 实际上是相同的一种基本类型。那么问题来了,在使用 Generic 作为形参的方法中,能否使用 Generic 的实例传入呢?在逻辑上类似于 GenericGeneric 是否可以看成具有父子关系的泛型类型呢?

为了弄清楚这个问题,我们使用Generic这个泛型类继续看下面的例子:

public void showKeyValue1(Generic<Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gNumber);

// showKeyValue这个方法编译器会为我们报错:Generic 
// cannot be applied to Generic
// showKeyValue(gInteger);

通过提示信息我们可以看到Generic不能被看作为Generic的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic类型的类,这显然与java中的多态理念相违背。因此我们需要一个在逻辑上可以表示同时是Generic和Generic父类的引用类型。由此类型通配符应运而生。

public void showKeyValue1(Generic<?> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。再直白点的意思就是,此处的‘?’和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。

可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。

4.4.2.3 泛型方法

在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。

尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中非常容易将泛型方法理解错了。

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型

/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}
Object obj = genericMethod(Class.forName("com.test.test"));

泛型方法的基本用法

public class GenericTest {
   //这个类是个泛型类,在上面已经介绍过
   public class Generic<T>{     
        private T key;

        public Generic(T key) {
            this.key = key;
        }

        //我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
        //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
        //所以在这个方法中才可以继续使用 T 这个泛型。
        public T getKey(){
            return key;
        }

        /**
         * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
        public E setKey(E key){
             this.key = keu
        }
        */
    }

    /** 
     * 这才是一个真正的泛型方法。
     * 首先在public与返回值之间的必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 这个T可以出现在这个泛型方法的任意位置.
     * 泛型的数量也可以为任意多个 
     *    如:public  K showKeyName(Generic container){
     *        ...
     *        }
     */
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());
        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();
        return test;
    }

    //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic这个泛型类做形参而已。
    public void showKeyValue1(Generic<Number> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

    //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
    //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
    public void showKeyValue2(Generic<?> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

     /**
     * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
     * 虽然我们声明了,也表明了这是一个可以处理泛型的类型的泛型方法。
     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
    public  T showKeyName(Generic container){
        ...
    }  
    */

    /**
     * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
     * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
     * 所以这也不是一个正确的泛型方法声明。
    public void showkey(T genericObj){

    }
    */

    public static void main(String[] args) {


    }
}

类中的泛型方法

当然这并不是泛型方法的全部,泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下

public class GenericFruit {
    class Fruit{
        @Override
        public String toString() {
            return "fruit";
        }
    }

    class Apple extends Fruit{
        @Override
        public String toString() {
            return "apple";
        }
    }

    class Person{
        @Override
        public String toString() {
            return "Person";
        }
    }

    class GenerateTest<T>{
        public void show_1(T t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
        //由于泛型方法在声明的时候会声明泛型,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
        //apple是Fruit的子类,所以这里可以
        generateTest.show_1(apple);
        //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
        //generateTest.show_1(person);

        //使用这两个方法都可以成功
        generateTest.show_2(apple);
        generateTest.show_2(person);

        //使用这两个方法也都可以成功
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}

泛型方法与可变参数

再看一个泛型方法和可变参数的例子:

public <T> void printMsg( T... args){
    for(T t : args){
        Log.d("泛型测试","t is " + t);
    }
}
printMsg("111",222,"aaaa","2323.4",55.55);

静态方法和泛型

静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法

public class StaticGenerator<T> {
    ....
    ....
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){

    }
}
4.4.3 方法总结

泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:

无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。

4.4.4 泛型上下边界

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

为泛型添加上边界,即传入的类型实参必须是指定类型的子类型

public void showKeyValue1(Generic<? extends Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);

//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
//showKeyValue1(generic1);

showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);

如果把泛型类的定义也改一下

public class Generic<T extends Number>{
    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}
//这一行代码也会报错,因为String不是Number的子类
Generic<String> generic1 = new Generic<String>("11111");

泛型方法:

//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的上添加上下边界,即在泛型声明的时候添加
//public  T showKeyName(Generic container),编译器会报错:"Unexpected bound"
public <T extends Number> T showKeyName(Generic<T> container){
    System.out.println("container key :" + container.getKey());
    T test = container.getKey();
    return test;
}
4.4.5 泛型数组

在java中是 ”不能创建一个确切的泛型类型的数组” 的。

List<String>[] ls = new ArrayList<String>[10];  // x
List<?>[] ls = new ArrayList<?>[10];  //√
List<String>[] ls = new ArrayList[10];//√
4.4.6 模板
4.4.6.1 函数模板
template 
返回值类型  模板名(形参表)
{
    函数体
}

函数模板看上去就像一个函数。前面提到的 Swap 模板的写法如下:

template <class T>
void Swap(T & x, T & y)
{
    T tmp = x;
    x = y;
    y = tmp;
}
4.4.6.2 类模板
4.4.6.2.1 单个类模板语法
  //类的类型参数化 抽象的类
  //单个类模板
 template<typename T>
  class A 
  {
  public:
      A(T t)
      {
          this->t = t;
     }
 
     T &getT()
     {
         return t;
     }
 protected:
 public:
     T t;
 };
 void main()
 {
    //模板了中如果使用了构造函数,则遵守以前的类的构造函数的调用规则
     A<int>  a(100); 
     a.getT();
     printAA(a);
     return ;
 }
 1 #include
 2 using namespace std;
 3 //A编程模板类--类型参数化
 4 /*
 5 类模板的定义 类模板的使用 类模板做函数参数
 6 */
 7 template 
 8 class A
 9 {
10 public:
11     A(T a = 0)
12     {
13         this->a = a;
14     }
15 public:
16     void printA()
17     {
18         cout << "a:" << a << endl;
19     }
20 protected:
21     T a;
22 private:
23     
24 };
25 //从模板类派生时,需要具体化模板类,C++编译器需要知道父类的数据类型是什么样子的
26 //要知道父类所占的内存多少
27 class B :public A
28 {
29 public:
30     B(int a =10, int b =20):A(a)
31     {
32         this->b = b;
33     }
34     void printB()
35     {
36         cout << "a:" << a << "b:" << b << endl;
37     }
38 protected:
39 private:
40     int b;
41     
42 };
43 //从模板类派生模板类
44 template 
45 class C :public A
46 {
47 
48 public:
49     C(T c,T a) : A(a)
50     {
51         this->c = c;
52     }
53     void printC()
54     {
55         cout << "c:" << c << endl;
56     }
57 protected:
58     T c;
59 private:
60     
61 };
62 
63 void main()
64 {
65     //B b1(1, 2);
66     //b1.printB();
67     C c1(1,2);
68     c1.printC();
69 }

5. 多态的三种实现方式

5.1 普通类多态定义(普通类继承)

格式: 父类类型 变量名 = new 子类类型()。

class Father {
    int num = 4;
}
 
class Son extends Father {
    int num = 5;
}
 
//普通类多态形式
Father father = new Son();

5.2 抽象类多态定义(抽象类继承)

abstract class Father {
    abstract void method();
}
 
class Son extends Father {
    public void method() {
        System.out.println("abstract");
    }
}
 
//抽象类多态表现形式
Father father = new Son();

5.3 接口的多态定义(接口实现)

interface Father {
    public void method();
}
 
class Son implements Father{
    public void method() {
        System.out.println("implements")
    }
}
 
//接口多态的表现形式
Father father = new Son();

6.5 多态变量在框架中的作用

多态接收器功能的强大之处表现在消息传递与改写相结合时。这种结合是软件框架开发的关键。

一般框架系统中的方法分为两大类:

在父类中定义基础方法,被多个子类所继承,但不被改写;

父类定义了关于多种活动的方法,但要延迟到子类才实现。

6. 练习:多态入门案例

public class Animal {
public void speak( Animal   p) {System.out.println("Animal Speak!");}
}
public class Dog extends Animal {
public void speak(Animal   p) {System.out.println("汪!");}
public void speak(Dog  t) { 
     System.out.println("汪汪");}
}

请写出下面程序的输出结果 
Animal  p1 = new Animal () ;
Animal  p2 = new Dog () ;
Dog p3 = new Dog () ;
 
p1.speak(  p1  ) ;
p1.speak(  p2  ) ;
p1.speak(  p3  ) ;
 
p2.speak(  p1  ) ;
p2.speak(  p2  ) ;
p2.speak(  p3  ) ;
 
p3.speak(  p1  ) ;

运算结果:(注意这是在java中的

Animal Speak!

Animal Speak!

Animal Speak!

7.利弊

7.1 好处

(1)向上转型:隐藏了子类类型,提高代码的扩展性。

(2)向下转型:可以使用子类特有功能。

7.2 弊端

(1)向上转型:只能使用父类共性的内容,无法使用子类特有功能。

(2)向下转型:容易发生类型转换异常(ClassCastException)。

静态行为和动态行为

1.静态类和动态类

变量的静态类是指用于声明变量的类。静态类在编译时就确定下来,并且再也不会改变。

变量的动态类指与变量所表示的当前数值相关的类。动态类在程序的执行过程中,当对变量赋新值时可以改变。表示直到运行时绑定于对象的属性或特征。

2.静态类型和动态类型的区别

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

1.静态方法在程序初始化后会一直贮存在内存中,不会被垃圾回收器回收
2.非静态方法只在该类初始化后贮存在内存中,当该类调用完毕后会被垃圾回收器收集释放。
3.静态方法在初始化类时初始化,并分配内存;动态方法只有先创建类的实例对象后,才能调用动态方法
4.静态方法实在类装载的时候就加载的,可以直接用类名调用,不必实例化。动态方法,是在由具体的类的对象的时候由对象调用的
5.静态方法在访问本类的成员时,只容许访问静态成员(即静态成员变量和静态方法),而不容许访问实例成员变量和实例方法;实例方法则无此限制。

引申:
一般在什么时候定义静态方法?

主要用在工具类中,或者扩展方法

好处:对对象的共享数据进行单独空间的存储,节省空间。没有必要每一个对象都存一份。可以直接被类名调用

坏处:声明周期过长。访问出现局限性。(静态虽好,只能访问静态)

2.1方法绑定

含义:

一个对象接到一个消息以后要进行预响应,把消息名和响应这个消息的代码结合起来的过程,叫做方法绑定。但是因为重名的原因,导致不是一个萝卜一个坑的一一对应。

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

public class Animal{
     public void speak(){System.out.println("Animal speak!");}
}
class Dog extends Animal{
     public void bark(){System.out.println("aowang!");}
     public void speak(){System.out.println("wang!");}
}
test:
Animal pet;
pet = new Dog();
pet.speak();//正确,出来 wang
pet.bark();//错误,因为检查合法性是在动态绑定之前,所以Animal无法知道有bark这个方法。

C++中多态方法绑定

使用指针或引用;相关方法声明为virtual;才可以实现多态消息传递。

2.1.1 静态方法绑定

静态绑定(前期绑定)是指:在程序运行前就已经知道方法是属于那个类的,在编译的时候就可以连接到类的中,定位到这个方法。

在Java中,final、private、static修饰的方法以及构造函数都是静态绑定的,不需程序运行,不需具体的实例对象就可以知道这个方法的具体内容。

2.1.2 动态方法绑定

动态绑定(后期绑定)是指:在程序运行过程中,根据具体的实例对象才能具体确定是哪个方法
动态绑定是多态性得以实现的重要因素,它通过方法表来实现:每个类被加载到虚拟机时,在方法区保存元数据,其中,包括一个叫做 方法表(method table)的东西,表中记录了这个类定义的方法的指针,每个表项指向一个具体的方法代码。如果这个类重写了父类中的某个方法,则对应表项指向新的代码实现处。从父类继承来的方法位于子类定义的方法的前面
动态绑定语句的编译、运行原理:我们假设 Father ft=new Son(); ft.say(); Son继承自Father,重写了say()。
1:编译:我们知道,向上转型时,用父类引用执行子类对象,并可以用父类引用调用子类中重写了的同名方法。但是不能调用子类中新增的方法,为什么呢?
因为在代码的编译阶段,编译器通过 声明对象的类型(即引用本身的类型) 在方法区中该类型的方法表中查找匹配的方法(最佳匹配法:参数类型最接近的被调用),如果有则编译通过。(这里是根据声明的对象类型来查找的,所以此处是查找 Father类的方法表,而Father类方法表中是没有子类新增的方法的,所以不能调用。
编译阶段是确保方法的存在性,保证程序能顺利、安全运行。
2:运行:我们又知道,ft.say()调用的是Son中的say(),这不就与上面说的,查找Father类的方法表的匹配方法矛盾了吗?不,这里就是动态绑定机制的真正体现。
上面编译阶段在 声明对象类型 的方法表中查找方法,只是为了安全地通过编译(也为了检验方法是否是存在的)。而在实际运行这条语句时,在执行 Father ft=new Son(); 这一句时创建了一个Son实例对象,然后在 ft.say() 调用方法时,JVM会把刚才的son对象压入操作数栈,用它来进行调用。而用实例对象进行方法调用的过程就是动态绑定:根据实例对象所属的类型去查找它的方法表,找到匹配的方法进行调用。我们知道,子类中如果重写了父类的方法,则方法表中同名表项会指向子类的方法代码;若无重写,则按照父类中的方法表顺序保存在子类方法表中。故此:动态绑定根据对象的类型的方法表查找方法是一定会匹配(因为编译时在父类方法表中以及查找并匹配成功了,说明方法是存在的。这也解释了为何向上转型时父类引用不能调用子类新增的方法:在父类方法表中必须先对这个方法的存在性进行检验,如果在运行时才检验就容易出危险——可能子类中也没有这个方法)。

空间分配

1.内存分配方法

1.1 最小静态空间分配

  • C++使用最小静态空间分配策略,运行高效。

  • 只分配基类所需的存储空间。

    为了防止采用这种策略时因为多态而引发的程序错误(具体参照P179),C++改变了虚拟方法的调用规则:

  • 对于指针 / 引用变量:当信息 调用可能被改写的成员函数时,选择哪个函数取决于接收器的动态数值。

  • 对于其他变量:调用虚拟成员函数的方式取决于静态类,而不取决于动态类

1.2 最大静态空间分配

无论基类还是派生类,都分配可用于所有合法的数值的最大的存储空间。

这一方案不合适,因为需要找到最大的对象,就需要对继承树上的所有对象都进行扫描,然后找到需要分配最大内存的对象才能

1.3 动态内存分配

  • 堆栈中不保存对象值。
  • 堆栈通过指针大小空间来保存标识变量,数据值保存在堆中。
  • 指针变量都具有恒定不变的大小,变量赋值时,不会有任何问题。

只分配用于保存一个指针所需的存储空间。在运行时通过对来分配指针对应对象所需的存储空间,同时将指针设为相应的合适值。

2. 内存分配策略

2.1 静态存储分配

静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间

这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求

2.2 动态存储分配

也被称为栈式存储分配,它是由一个类似于堆栈的运行栈来实现的。

和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存。

栈式存储分配按照先进后出的原则进行分配。

2.3堆式存储分配

堆式存储分配专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例。

堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。

框架

  • 对于一类相似问题的骨架解决方案。
  • 通过类的集合形成,类之间紧密结合,共同实现对问题的可复用解决方案
  • 继承和改写的强大能力体现

框架开发的一个重要基础:使用继承的两种方式

  • 代码复用:基本方法,对问题的现存的解决方案。
  • 概念复用:特化方法,用于特定应用的解决方案。

例如:雇员排序

原来:

class Employee {
public:
	string name;
	int salary;
	int startingYear;	
}
//插入排序-根据工作年份
void sort (Employee * data[ ], int n) {
	for (int i = 1; i < n; i++) {
		int j = i-1;
		while (j >= 0 && 
		   v[j+1]->startingYear < v[j]->startingYear) {
			// swap elements
			Employee * temp = v[j];
			v[j] = v[j+1];
			v[j+1] = temp;
			j = j - 1;
		}
	}
}

现在想要改用薪水排序或者姓名排序,那么就会发生麻烦;修改方案,不再对雇员记录排序,而是对一个浮点数组排序:(复用的是排序的思想,不是真正的实现。)

class InsertionSorter {
public:
	void sort () {
		int n = size();
		for (int i = 1; i < n; i++) {
			int j = i - 1;
			while (j >= 0 && lessThan(j+1, j)) {
				swap(j, j+1);
				j = j - 1;
			}
		}
	}
private:
	virtual int size() = 0; // abstract methods
	virtual boolean lessThan(int i, int j) = 0;
	virtual void swap(int i, int j) = 0;
}
class EmployeeSorter : public InsertionSorter {
public:
	EmployeeSorter (Employee * d[], int n) 
		{ data = d; sze = n; }
private:
	Employee * data[];
	int sze = n;
	virtual int size () { return sze; }
	virtual bool lessThan (int i, int j) 
		{ return data[i]->startingYear < data[j]->startingYear; }
	virtual void swap (int i, int j) {
		Employee * temp = v[i];
		v[i] = v[j];
		v[j] = temp;
	}
}

基类不再需要改变。特化子类满足不同的需求。如:改变为对收入进行排序只需改变子类,无需改变父类;对浮点数进行排序也只需创建一个新的子类,而无需改变父类

UML设计

在UML类图中,常见的有以下几种关系: 泛化(Generalization), 实现(Realization), 关联(Association), 聚合(Aggregation), 组合(Composition), 依赖(Dependency)

⭐类图知识点

1.类图分为三部分,依次是类名、属性、方法

2.以<<开头和以>>结尾的为注释信息

3.修饰符+代表public,-代表private,#代表protected,什么都没有代表包可见。

4.带下划线的属性或方法代表是静态的。

1. 泛化(Generalization)

【泛化关系】:是一种继承关系, 表示一般与特殊的关系, 它指定了子类如何特化父类的所有特征和行为. 例如:老虎是动物的一种, 即有老虎的特性也有动物的共性.

【箭头指向】:带三角箭头的实线,箭头指向父类
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第6张图片

2. 实现(Realization)

【实现关系】:是一种类与接口的关系, 表示类是接口所有特征和行为的实现.

【箭头指向】:带三角箭头的虚线,箭头指向接口
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第7张图片

3. 关联(Association)

【关联关系】:是一种拥有的关系, 它使一个类知道另一个类的属性和方法;如:老师与学生,丈夫与妻子

关联可以是双向的,也可以是单向的。双向的关联可以有两个箭头或者没有箭头,单向的关联有一个箭头

【代码体现】:成员变量

【箭头及指向】:带普通箭头的实心线,指向被拥有者

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第8张图片

上图中,老师与学生是双向关联,老师有多名学生,学生也可能有多名老师。但学生与某课程间的关系为单向关联,一名学生可能要上多门课程,课程是个抽象的东西他不拥有学生。
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第9张图片

上图为自身关联

4. 聚合(Aggregation)

【聚合关系】:是整体与部分的关系, 且部分可以离开整体而单独存在. 如车和轮胎是整体和部分的关系, 轮胎离开车仍然可以存在.

聚合关系是关联关系的一种,是强的关联关系;关联和聚合在语法上无法区分,必须考察具体的逻辑关系。

【代码体现】:成员变量

【箭头及指向】:带空心菱形的实心线,菱形指向整体

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第10张图片

5. 组合(Composition)

【组合关系】:是整体与部分的关系, 但部分不能离开整体而单独存在. 如公司和部门是整体和部分的关系, 没有公司就不存在部门.

​ 组合关系是关联关系的一种,是比聚合关系还要强的关系,它要求普通的聚合关系中代表整体的对象负责代表部分的对象的生命周期

【代码体现】:成员变量

【箭头及指向】:带实心菱形的实线,菱形指向整体
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第11张图片

6. 依赖(Dependency)

【依赖关系】:是一种使用的关系, 即一个类的实现需要另一个类的协助, 所以要尽量不使用双向的互相依赖.

【代码表现】:局部变量、方法的参数或者对静态方法的调用

【箭头及指向】:带箭头的虚线,指向被使用者

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第12张图片
各种关系的强弱顺序:

泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖

下面这张UML图,比较形象地展示了各种类图关系:
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第13张图片

设计原则

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第14张图片

1.单一职责原则 SRP

不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。

此原则的核心就是解耦增强内聚性

1.1 原则出现原因

职责扩散:因为某种原因,职责P被分化为粒度更细的职责P1和P2。

问题:

类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。

解决:

遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

1.2 出现不符合单一职责原则的解决方案

比如:类T只负责一个职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1,P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。(这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。)

举例说明,用一个类描述动物呼吸这个场景:

class Animal{
	public void breathe(String animal){
		System.out.println(animal+"呼吸空气");
	}
}
public class Client{
	public static void main(String[] args){
		Animal animal = new Animal();
		animal.breathe("牛");
		animal.breathe("羊");
		animal.breathe("猪");
	}
}

运行结果:

牛呼吸空气
羊呼吸空气
猪呼吸空气

问题:

​ 程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。

  1. 修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码如下:
class Terrestrial{
	public void breathe(String animal){
		System.out.println(animal+"呼吸空气");
	}
}
class Aquatic{
	public void breathe(String animal){
		System.out.println(animal+"呼吸水");
	}
}

public class Client{
	public static void main(String[] args){
		Terrestrial terrestrial = new Terrestrial();
		terrestrial.breathe("牛");
		terrestrial.breathe("羊");
		terrestrial.breathe("猪");
          Aquatic aquatic = new Aquatic();
	     aquatic.breathe("鱼");
}}

运行结果:

牛呼吸空气
羊呼吸空气
猪呼吸空气
鱼呼吸水

我们会发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。

  1. 而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:
class Animal{
	public void breathe(String animal){
		if("鱼".equals(animal)){
			System.out.println(animal+"呼吸水");
		}else{
			System.out.println(animal+"呼吸空气");
		}
	}
}

public class Client{
	public static void main(String[] args){
		Animal animal = new Animal();
		animal.breathe("牛");
		animal.breathe("羊");
		animal.breathe("猪");
		animal.breathe("鱼");
	}
}

​ 可以看到,这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。

  1. 还有一种修改方式:
class Animal{
	public void breathe(String animal){
		System.out.println(animal+"呼吸空气");
	}

	public void breathe2(String animal){
		System.out.println(animal+"呼吸水");
	}

}

public class Client{
	public static void main(String[] args){
		Animal animal = new Animal();
		animal.breathe("牛");
		animal.breathe("羊");
		animal.breathe("猪");
		animal.breathe2("鱼");
	}
}

​ 可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,==但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。==这三种方式各有优缺点,那么在实际编程中,采用哪一种呢?其实这真的比较难说,需要根据实际情况来确定。我的原则是:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则;

​ 例如本文所举的这个例子,它太简单了,它只有一个方法,所以,无论是在代码级别上违反单一职责原则,还是在方法级别上违反,都不会造成太大的影响。实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是遵循单一职责原则的好。

1.3遵循单一职责原则的优点

  1. 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
  2. 提高类的可读性,提高系统的可维护性;
  3. 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。

需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。

2.里氏替换原则 LSP(关乎继承)

定义1: 如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。

定义2: 所有引用基类的地方必须能透明地使用其子类的对象。

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。

2.1 原则含义

  1. 子类必须完全实现父类的方法

  2. 子类可以有自己的个性(方法和属性)

  3. 覆盖或实现父类的方法时输入参数可以被放大

    子类中方法的前置条件必须与超类中被覆盖的方法的前置条件相同或者更宽松。

方法中的输入参数称为前置条件,返回参数称为后置条件

例子

public class Father {
    public Collection doSomething(HashMap map){
        System.out.println("父类被执行……");
        return map.values();
    }
}
public class Son extends Father{
    public Collection doSomething(Map map) {
        System.out.println("子类被执行……");
        return map.values();
    }
}
public class Client {
    public static void invoker(){
        Son ff = new Son();
        HashMap map = new HashMap();
        ff.doSomething(map);
    }

    public static void main(String[] args) {
        invoker();
    }
}

两个的运行结果都是:父类被执行……

原因

子类拥有父类的所有属性和方法,方法名相同,输入参数类型又不相同,这是重定义。

父类方法的输入参数是HashMap类型,子类的输入参数是Map类型,也就是说子类的输入参数类型的范围扩大了,子类代替父类传递到调用者中,子类的方法永远都不会被执行。这是正确的,如果你想让子类的方法运行,就必须覆写父类的方法。大家可以这样想,在一个Invoker类中关联了一个父类,调用了一个父类的方法,子类可以覆写这个方法,也可以重定义这个方法,前提是要扩大这个前置条件,就是输入参数的类型大于父类的类型覆盖范围。这样说可能比较理难理解,我们再反过来想一下,如果Father类的输入参数类型大于子类的输入参数类型,会出现什么问题呢? 会出现父类存在的地方,子类就未必可以存在,因为一旦把子类作为参数传入,调用者就很可能进入子类的方法范畴。 我们把上面的例子修改一下,扩大父类的前置条件。

public class Father {
    public Collection doSomething(Map map){
        System.out.println("父类被执行……");
        return map.values();
    }
}
public class Son extends Father{
    public Collection doSomething(HashMap map) {
        System.out.println("子类被执行……");
        return map.values();
    }
}
public class Client {
    public static void invoker(){
        Son ff = new Son();
        HashMap map = new HashMap();
        ff.doSomething(map);
    }

    public static void main(String[] args) {
        invoker();
    }
}

第一次Father运行时:父类被执行……

第二次Son运行时:子类被执行……

子类在没有覆写父类的方法的前提下,子类方法被执行了, 这会引起业务逻辑混乱,因为在实际应用中父类一般都是抽象类,子类是实现类,你传递一个这样的实现类就会“歪曲”了父类的意图,引起一堆意想不到的业务逻辑混乱,所以子类中方法的前置条件必须与超类中被覆盖的方法的前置条件相同或者更宽松。

  1. 覆盖或实现父类的方法时输出结果可以被缩小

父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T。

2.2 原则出现原因

问题:

有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

解决:

当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

​ 继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。

举例说明:

正方形类 Square正方形是矩形,因此Square类应该从Rectangle类派生而来。

继承的风险,矩形类

Public class Rectangle{
  private double width;
  private double heigth;
  public Rectangle(double w,double h) {
    width = w;  heigth = h;
  }
  public void setWidth(double w){ width = w; }
  public void setHeigth(double h){ height = h; }
  public double getWidth(){  return width; }
  public double getHeigth(){  return height; }
  public double area(){  return width * height; }
}

正方形类

Public class Square extends Rectangle{
  public Square(double s) {
    super(s,s);
  }
  public void setWidth(double w){ 
    super.setWidth ( w ); 
    super.setHeight( w ); 
  }
 public void setHeight(double h){ 
    super.setWidth ( h ); 
    super.setHeight( h ); 
  }
}

测试类

Public class TestRectangle{
  public static void testLSP(Rectangle r) {
    r.setWidth(4.0);
    r.setHeight(5.0); 
    System.out.println(Width is 4.0 and Height is 5.0 ,Area is ” + r.area());
  }
public static void main(String args[]){
  Rectangle r=new Rectangle(1.0,1.0);
  Square s = new Square(1.0);
 
  testLSP ( r );
  testLSP ( s );
  }
}

运行结果:Width is 4.0 and Height is 5.0 ,Area is 20

​ Width is 4.0 and Height is 5.0 ,Area is 25 x

不符合LSP替换原则

一个数学意义上的正方形可能是一个矩形,但是一个Square对象不是一个Rectangle对象,因为一个Square对象的行为与一个Rectangle对象的行为是不一致的!(矩形 仅包含get 因为set不同)

从行为上来说,一个Square不是一个Rectangle!一个Square对象与一个Rectangle对象之间不具有多态的特征。

2.3小结

Liskov替换法则(LSP)清楚地表明了IS A关系全部都是与行为有关的。

为了保持LSP,所有子类必须符合使用基类的client所期望的行为。

一个子类型不得具有比基类型更多的限制,可能这对于基类型来说是合法的,但是可能会因为违背子类型的其中一个额外限制,从而违背了LSP!

LSP保证一个子类总是能够被用在其基类可以出现的地方

3.依赖倒转原则(关乎接口)DIP

3.1原则含义

高层模块不应该依赖底层模块,二者都应该依赖其抽象;

抽象不应当依赖于细节,细节(具体的实现类)应当依赖于抽象(接口或者抽象类)。

3.2 原则出现原因

传统的设计是抽象层依赖具体层

传统的重用,侧重于具体层次的模块,比如算法、数据结构、函数库因此软件的高层模块依赖低层模块
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第15张图片

问题

类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

解决

将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。

依赖倒置原则基于这样一个事实:==相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。==使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

依赖倒置原则的核心思想是面向接口编程

面向接口编程

不将变量声明为某个特定的具体类的实例对象,而让其遵从抽象类定义的接口。实现类仅实现接口,不添加方法。

如:

(Draw(shape*p)  
不要Cricle*p  Rectangle *p  Triangle *p)

依赖于抽象:

  1. 任何变量都不应该持有一个指向具体类的指针或引用
  2. 任何类都不应该从具体类派生
  3. 任何方法都不应该覆写它的任何基类中已实现了的方法

约束:

如果一个类的实例必须使用另一个对象,而这个对象又属于一个特定的类,那么复用性会受到损害。

如果“使用”类只需使用“被使用”类的某些方法,而不是要求“被使用”类与“使用”类有“is-a”的关系,就可考虑,让“被使用”类实现一个接口,“使用”类通过这个接口来使用需要的方法,从而限制了类之间的依赖。

方案:为避免类之间因彼此使用而造成的耦合,让它们通过接口间接使用。

例子:

我们依旧用一个例子来说明面向接口编程比相对于面向实现编程好在什么地方。场景是这样的,母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。代码如下:

class Book{
	public String getContent(){
		return "很久很久以前有一个阿拉伯的故事……";
	}
}

class Mother{
	public void narrate(Book book){
		System.out.println("妈妈开始讲故事");
		System.out.println(book.getContent());
	}
}

public class Client{
	public static void main(String[] args){
		Mother mother = new Mother();
		mother.narrate(new Book());
	}
}

运行结果:

妈妈开始讲故事
很久很久以前有一个阿拉伯的故事……

​ 运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:

class Newspaper{
	public String getContent(){
		return "林书豪38+7领导尼克斯击败湖人……";
	}
}

​ 这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。

我们引入一个抽象的接口IReader。读物,只要是带字的都属于读物:

interface IReader{
	public String getContent();
}

Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改为:

class Newspaper implements IReader {
	public String getContent(){
		return "林书豪17+9助尼克斯击败老鹰……";
	}
}
class Book implements IReader{
	public String getContent(){
		return "很久很久以前有一个阿拉伯的故事……";
	}
}

class Mother{
	public void narrate(IReader reader){
		System.out.println("妈妈开始讲故事");
		System.out.println(reader.getContent());
	}
}

public class Client{
	public static void main(String[] args){
		Mother mother = new Mother();
		mother.narrate(new Book());
		mother.narrate(new Newspaper());
	}
}

运行结果:

妈妈开始讲故事
很久很久以前有一个阿拉伯的故事……
妈妈开始讲故事
林书豪17+9助尼克斯击败老鹰……

这样修改后,无论以后怎样扩展Client类,都不需要再修改Mother类了。这只是一个简单的例子,实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。

采用依赖倒置原则给多人并行开发带来了极大的便利,比如上例中,原本Mother类与Book类直接耦合时,Mother类必须等Book类编码完成后才可以进行编码,因为Mother类依赖于Book类。修改后的程序则可以同时开工,互不影响,因为Mother与Book类一点关系也没有。参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大。现在很流行的TDD开发模式就是依赖倒置原则最成功的应用。

​ 传递依赖关系有三种方式,以上的例子中使用的方法是接口传递,另外还有两种传递方式:构造方法传递setter方法传递,相信用过Spring框架的,对依赖的传递方式一定不会陌生。

在实际编程中,我们一般需要做到如下3点:

  1. 低层模块尽量都要有抽象类或接口,或者两者都有。

  2. 变量的声明类型尽量是抽象类或接口。

  3. 使用继承时遵循里氏替换原则。

    依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。

4.组合复用原则 CRP

合成复用原则 又称为 组合复用原则 , 合成/聚合复用原则 , 组合/聚合复用原则 ;

合成复用原则定义 : 想要达到 软件复用 的目的 , 尽量使用 对象 组合/聚合 , 而不是 继承关系 ;

聚合 是 has-A 关系 ; ( 关系较弱 ) 代表部分事物的对象 ( 次 ) 与 代表聚合事物的对象 ( 主 ) 生命周期无关 , 删除了聚合对象 , 不代表删除了代表部分事物的对象 ;

组合 是 contains-A 关系 ; ( 关系较强 ) 一旦删除 代表组合事物的对象 ( 主 ) , 那么 代表部分事物的对象 ( 次 ) 也一起被删除 ;

继承 是 is-A 关系 ;

电脑 与 U 盘 是聚合关系 , 电脑没了 , U 盘可以独立存在 , 还可以接在其它电脑上 ;

A 类中包含了 B 类的引用 , 当 A 类对象销毁时 , B 类引用所指向的对象也一同消失 , 没有任何一个引用指向他 , 该引用成为了垃圾对象 , 被回收 ; 这种情况就是 组合 ;

加入 A 类销毁后 , B 类对象还有在其它位置被引用 , B 类对象不会被销毁 , 此时这种关系就是聚合 ;

4.1COAD规则

仅当下列的所有标准被满足时,方可使用继承:

子类表达了“是一个…的特殊类型”,而非“是一个由…所扮演的角色”。

子类的一个实例永远不需要转化(transmute)为其它类的一个对象。

子类是对其父类的职责(responsibility)进行扩展,而非重写或废除(nullify)。

子类没有对那些仅作为一个工具类(utility class)的功能进行扩展。

5.接口隔离原则(ISP)

5.1 原则含义

客户端不应该依赖它不需要的接口;

一个类对另一个类的依赖应该建立在最小的接口上;

使用多个专门的接口比使用单一的总接口好。

5.2 原则出现原因

问题

类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类D和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。

解决

将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

例子
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第16张图片
这个图的意思是:类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现。类C依赖接口I中的方法1、方法4、方法5,类D是对类C依赖的实现。对于类B和类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也必须要实现这些用不到的方法。对类图不熟悉的可以参照程序代码来理解,代码如下:

interface I {
	public void method1();
	public void method2();
	public void method3();
	public void method4();
	public void method5();
}
 
class A{
	public void depend1(I i){
		i.method1();
	}
	public void depend2(I i){
		i.method2();
	}
	public void depend3(I i){
		i.method3();
	}
}
 
class B implements I{
	public void method1() {
		System.out.println("类B实现接口I的方法1");
	}
	public void method2() {
		System.out.println("类B实现接口I的方法2");
	}
	public void method3() {
		System.out.println("类B实现接口I的方法3");
	}
	//对于类B来说,method4和method5不是必需的,但是由于接口A中有这两个方法,
	//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
	public void method4() {}
	public void method5() {}
}
 
class C{
	public void depend1(I i){
		i.method1();
	}
	public void depend2(I i){
		i.method4();
	}
	public void depend3(I i){
		i.method5();
	}
}
 
class D implements I{
	public void method1() {
		System.out.println("类D实现接口I的方法1");
	}
	//对于类D来说,method2和method3不是必需的,但是由于接口A中有这两个方法,
	//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
	public void method2() {}
	public void method3() {}
 
	public void method4() {
		System.out.println("类D实现接口I的方法4");
	}
	public void method5() {
		System.out.println("类D实现接口I的方法5");
	}
}
 
public class Client{
	public static void main(String[] args){
		A a = new A();
		a.depend1(new B());
		a.depend2(new B());
		a.depend3(new B());
		
		C c = new C();
		c.depend1(new D());
		c.depend2(new D());
		c.depend3(new D());
	}
}

可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原有的接口I拆分为三个接口,拆分后的设计如图2所示:

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第17张图片

interface I1 {
	public void method1();
}
 
interface I2 {
	public void method2();
	public void method3();
}
 
interface I3 {
	public void method4();
	public void method5();
}
 
class A{
	public void depend1(I1 i){
		i.method1();
	}
	public void depend2(I2 i){
		i.method2();
	}
	public void depend3(I2 i){
		i.method3();
	}
}
 
class B implements I1, I2{
	public void method1() {
		System.out.println("类B实现接口I1的方法1");
	}
	public void method2() {
		System.out.println("类B实现接口I2的方法2");
	}
	public void method3() {
		System.out.println("类B实现接口I2的方法3");
	}
}
 
class C{
	public void depend1(I1 i){
		i.method1();
	}
	public void depend2(I3 i){
		i.method4();
	}
	public void depend3(I3 i){
		i.method5();
	}
}
 
class D implements I1, I3{
	public void method1() {
		System.out.println("类D实现接口I1的方法1");
	}
	public void method4() {
		System.out.println("类D实现接口I3的方法4");
	}
	public void method5() {
		System.out.println("类D实现接口I3的方法5");
	}
}

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。 也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。本文例子中,将一个庞大的接口变更为3个专用的接口所采用的就是接口隔离原则。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

说到这里,很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。

其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。

其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口,主要针对抽象,针对程序整体框架的构建。

​ 采用接口隔离原则对接口进行约束时,要注意以下几点:

接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
提高内聚,减少对外交互。如果类的接口不是内聚的,就表示该类具有“胖”的接口。ISP建议客户程序不应该看到它们作为单一的类存在。客户程序看到的应该是多个具有内聚接口的抽象基类。使接口用最少的方法去完成最多的事情。
运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。

6. 迪米特法则 LOD

6.1原则定义

狭义

一个对象应该对其他对象保持最少的了解。

如果两个类不必彼此通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可通过第三者转发这个调用。(朋友圈)

广义

控制信息过载,提高封装能力。

一个模块设计得好坏的一个重要的标志就是该模块在多大的程度上将自己的内部数据与实现有关的细节隐藏起来。信息的隐藏非常重要的原因在于,它可以使各个子系统之间脱耦,从而允许它们独立地被开发、优化、使用阅读以及修改。

6.2 原则出现原因

狭义

问题

类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

解决

尽量降低类与类之间的耦合。

自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。

​ 迪米特法则又叫最少知道原则,最早是在1987年由美国Northeastern University的Ian Holland提出。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。迪米特法则还有一个更简单的定义:只与直接的朋友通信。

首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现在成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

​ 举一个例子:有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。先来看一下违反迪米特法则的设计。

//总公司员工
class Employee{
	private String id;
	public void setId(String id){
		this.id = id;
	}
	public String getId(){
		return id;
	}
}

//分公司员工
class SubEmployee{
	private String id;
	public void setId(String id){
		this.id = id;
	}
	public String getId(){
		return id;
	}
}

class SubCompanyManager{
	public List<SubEmployee> getAllEmployee(){
		List<SubEmployee> list = new ArrayList<SubEmployee>();
		for(int i=0; i<100; i++){
			SubEmployee emp = new SubEmployee();
			//为分公司人员按顺序分配一个ID
			emp.setId("分公司"+i);
			list.add(emp);
		}
		return list;
	}
}

class CompanyManager{

	public List<Employee> getAllEmployee(){
		List<Employee> list = new ArrayList<Employee>();
		for(int i=0; i<30; i++){
			Employee emp = new Employee();
			//为总公司人员按顺序分配一个ID
			emp.setId("总公司"+i);
			list.add(emp);
		}
		return list;
	}
	
	public void printAllEmployee(SubCompanyManager sub){
		List<SubEmployee> list1 = sub.getAllEmployee();
		for(SubEmployee e:list1){
			System.out.println(e.getId());
		}
	 
		List<Employee> list2 = this.getAllEmployee();
		for(Employee e:list2){
			System.out.println(e.getId());
		}
	}

}

public class Client{
	public static void main(String[] args){
		CompanyManager e = new CompanyManager();
		e.printAllEmployee(new SubCompanyManager());
	}
}

​ 现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,而SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。修改后的代码如下:

class SubCompanyManager{
	public List<SubEmployee> getAllEmployee(){
		List<SubEmployee> list = new ArrayList<SubEmployee>();
		for(int i=0; i<100; i++){
			SubEmployee emp = new SubEmployee();
			//为分公司人员按顺序分配一个ID
			emp.setId("分公司"+i);
			list.add(emp);
		}
		return list;
	}
	public void printEmployee(){
		List<SubEmployee> list = this.getAllEmployee();
		for(SubEmployee e:list){
			System.out.println(e.getId());
		}
	}
}

class CompanyManager{
	public List<Employee> getAllEmployee(){
		List<Employee> list = new ArrayList<Employee>();
		for(int i=0; i<30; i++){
			Employee emp = new Employee();
			//为总公司人员按顺序分配一个ID
			emp.setId("总公司"+i);
			list.add(emp);
		}
		return list;
	}
	

	public void printAllEmployee(SubCompanyManager sub){
		sub.printEmployee();
		List<Employee> list2 = this.getAllEmployee();
		for(Employee e:list2){
			System.out.println(e.getId());
		}
	}

}

​ 修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。

缺点:

(1) 系统中出现大量小方法,这些方法仅仅是传递间接的调用,与系统的业务逻辑无关
(2) 遵循类之间的迪米特法则会是一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有之间的交互,但是这也造成系统的不同模块间的通讯效率降低,也会是系统的不同模块之间不容易协调。

广义

广义的迪米特法则在类的设计上的体现:
(1) 优先考虑将一个类设计成不变类
(2) 尽量降低一个类的访问权限:如将类的访问权限限定在一个包、命名空间内
(3) 尽量降低成员的访问权限:如合理使用public、protected、private

下面以一个实例展示如何遵循迪米特法则
有一个设计公司
一个部门的负责人是 项目经理,项目经理管理部门内的 程序人员、美术人员、产品设计人员
一个客户,客户应该只负责跟项目经理提需求,项目经理根据需求将工作任务安排给不同人员

类比到迪米特法则中
项目经理的朋友们: 程序人员、美术人员、产品设计人员
客户的朋友:项目经理
客户 跟 程序人员、美术人员、产品设计人员 是陌生人
正常情况下,客户类对象是不允许访问、调用 程序人员、美术人员、产品设计人员 对象的

代码实现如下

   // 添加命名空间,限制访问权限
namespace Company
{
    // 项目经理
    public class PM
    {
        // 引用程序人员,不允许通过 PM 对象访问该变量
        private Program _program;
        // 引用美术人员,不允许通过 PM 对象访问该变量
        private Art _art;
        // 引用产品人员,不允许通过 PM 对象访问该变量
        private Product _product;
   public PM()
    {
        // 直接在内部创建对象了
        Program program = new Program();
        SetProgram(program);

        Art art = new Art();
        SetArt(art);

        Product product = new Product();
        SetProduct(product);
    }

    // 添加引用对象
    public void SetProgram(Program program)
    {
        _program = program;
    }

    // 添加引用对象
    public void SetArt(Art art)
    {
        _art = art;
    }

    // 添加引用对象
    public void SetProduct(Product product)
    {
        _product = product;
    }

    // 需求调用
    public void DoSomething(string msg)
    {
        if (msg.CompareTo("programDemand") == 0)
        {
            _program.DoSomething(msg);
        }
        else if (msg.CompareTo("artDemand") == 0)
        {
            _art.DoSomething(msg);
        }
        else if (msg.CompareTo("productDemand") == 0)
        {
            _product.DoSomething(msg);
        }
    }
}

// 程序人员
public class Program
{
    // 需求调用
    public void DoSomething(string msg)
    {
        Console.WriteLine("程序人员:" + msg);
    }
}

// 美术人员
public class Art
{
    // 需求调用
    public void DoSomething(string msg)
    {
        Console.WriteLine("美术人员:" + msg);
    }
}

// 产品设计人员
public class Product
{
    // 需求调用
    public void DoSomething(string msg)
    {
        Console.WriteLine("产品设计人员:" + msg);
    }
}
}

客户类实现如下

// 添加命名空间,限制访问权限
namespace Client
{
    public class Client
    {
        public Client()
        {
            Company.PM pM = new Company.PM();

            pM.DoSomething("programDemand");
            pM.DoSomething("artDemand");
            pM.DoSomething("productDemand");
        }
    }

}

输出结果:

​ 程序人员:programDemand

​ 美工人员:artDemand

​ 产品设计人员:productDemand

迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,==虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,==例如本例中,总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

7.开闭原则 OCP

7.1原则定义

软件组成实体应该是对扩展开放的,但是对修改是关闭的。

通俗而言:软件系统中包含的各种组件,例如模块(Modules)、类(Classes)以及函数(Functions)等等,应该在不修改现有代码的基础上,引入新功能。 “开”,是允许对其进行功能扩展的; “闭”,是指对于原有代码的修改是封闭的,即不应该修改原有的代码。

在设计一个软件的时候,应当使这个软件可以在不被修改的前提下扩展

已有模块,尤其是最重要的抽象层模块不能动:保证稳定性和延续性

可以扩展新模块:增加新行为,保证灵活性

7.2原则出现原因

问题

在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

解决

当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

​ 开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式六项原则中定义最模糊的一个了,它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。以前,如果有人告诉我“你进行设计的时候一定要遵守开闭原则”,我会觉的他什么都没说,但貌似又什么都说了。因为开闭原则真的太虚了。

​ 在仔细思考以及仔细阅读很多设计模式的文章后,终于对开闭原则有了一点认识。其实,我们遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面五项原则遵守程度的“平均得分”,前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。

​ 其实开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。 因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。

​ 说到这里,再回想一下前面说的5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。

​ 最后说明一下如何去遵守这六个原则。对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。我们用一幅图来说明一下。
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第18张图片

​ 图中的每一条维度各代表一项原则,我们依据对这项原则的遵守程度在维度上画一个点,则如果对这项原则遵守的合理的话,这个点应该落在红色的同心圆内部;如果遵守的差,点将会在小圆内部;如果过度遵守,点将会落在大圆外部。一个良好的设计体现在图中,应该是六个顶点都在同心圆中的六边形。
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第19张图片
在上图中,设计1、设计2属于良好的设计,他们对六项原则的遵守程度都在合理的范围内;设计3、设计4设计虽然有些不足,但也基本可以接受;设计5则严重不足,对各项原则都没有很好的遵守;而设计6则遵守过渡了,设计5和设计6都是迫切需要重构的设计。

设计模式

1. 单例模式

1.1 模式定义

确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。 在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

类型:创建类模式
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第20张图片

要素:

  • 私有的构造方法
  • 指向自己实例的私有静态引用
  • 以自己实例为返回值的静态的公有的方法

单例模式根据实例化对象时机的不同分为两种:一种是饿汉式单例,一种是懒汉式单例。饿汉式单例在单例类被加载时候,就实例化一个对象交给自己的引用;而懒汉式在调用取得实例方法的时候才会实例化对象。
代码如下:

饿汉式单例

public class Singleton {
	private static Singleton singleton = new Singleton();
	private Singleton(){}
     //静态工厂方法:不通过 new,而是用一个静态方法来对外提供自身实例的方法。
	public static Singleton getInstance(){
		return singleton;
	}
     
     //写个main函数测试一下
    public static void main(String[] args) {
        for(int i = 0; i <2; i++){
            Singleton obj = Singleton.getInstance();
            System.out.println(obj);   //获得对象,打印

        }
    }
}

懒汉式单例

public class Singleton {
	private static Singleton singleton;
	private Singleton(){}
	public static synchronized Singleton getInstance(){
		if(singleton==null){
			singleton = new Singleton();
		}
		return singleton;
	}
}

运行结果:

Singleton@1b6d3586
Singleton@1b6d3586

单例模式的优点:

在内存中只有一个对象,节省内存空间。
避免频繁的创建销毁对象,可以提高性能。
避免对共享资源的多重占用。
可以全局访问。
适用场景:

  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 有状态的工具类对象。
  • 频繁访问数据库或文件的对象。
  • 以及其他我没用过的所有要求只有一个对象的场景。

比较

  1. 饿汉式单例和懒汉式单例由于构造方法是private的,所以他们都是不可继承的,但是其他很多单例模式是可以继承的,例如登记式单例。
  2. 饿汉式单例类在自己被加载时就将自己实例化。即便加载器是静态的,在饿汉式单例类被加载时仍会将自己实例化。单从资源利用效率角度来讲,这个比懒汉式单例类稍差些。

2.简单工厂模式

简单工厂模式并不是23种设计模式当中的一个模式,它属于一个创建模式

简单工厂模式是有一个工厂类根据传入的参量决定创建出哪一种产品类的实例

2.1角色与结构

工厂类(Creator)角色:该角色是工厂方法模式的核心,含有与应用紧密相关的商业逻辑。工厂类在客户端的直接调用下创建产品对象,它往往由一个具体类实现。

抽象产品(Product)角色:担任这个角色的类是工厂方法模式所创建的对象的父类,或它们共同拥有的接口。抽象产品角色可以用接口或者抽象类实现。

具体产品(Concrete Product)角色:工厂方法模式所创建的任何对象都是这个角色的实例,具体产品角色由一个具体类实现。
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第21张图片

2.2例子:(水果工厂)

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第22张图片

FruitIF:(水果的接口)

public interface FruitIF { 
void grow(); 
void harvest(); 
void plant(); 
} 

Apple

public class Apple implements FruitIF 
{ 
public void grow() 
{ 
log("Apple is growing..."); } 
public void harvest() 
{ 
log("Apple has been harvested."); } 
public void plant() 
{ 
log("Apple has been planted."); } 
public static void log(String msg) 
{ 
System.out.println(msg); } 
public int getTreeAge() { return treeAge; } 
public void setTreeAge(int treeAge){ 
this.treeAge = treeAge; } 
private int treeAge; 
} 

剩下的strawberry和grape差不多

FruitGardender:(水果代理商)

public class FruitGardener 
{ 
public FruitIF factory(String which) throws BadFruitException 
{ 
if (which.equalsIgnoreCase("apple")) 
{ return new Apple(); } 
else if (which.equalsIgnoreCase("strawberry")) 
{ return new Strawberry(); } 
else if (which.equalsIgnoreCase("grape")) 
{ return new Grape(); } 
else 
{ 
throw new BadFruitException("Bad fruit request"); 
} } }

test:(在使用时,只须调用FruitGardener的factory()方法即可 )

FruitGardener gardener = new FruitGardener(); 
gardener.factory("grape"); 
gardener.factory("apple"); 
gardener.factory("strawberry");

2.3模式优缺点

优点

模式的核心是工厂类。这个类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例。

而客户端则可以免除直接创建产品对象的责任,而仅仅负责“消费”产品。

简单工厂模式通过这种做法实现了对责任的分割。

缺点

工厂类集中了所有的产品创建逻辑,形成一个无所不知的全能类,有人把这种类叫做上帝类(God Class)

当产品类有不同的接口种类时,工厂类需要判断在什么时候创建某种产品。这种对时机的判断和对哪一种具体产品的判断逻辑混合在一起,使得系统在将来进行功能扩展时较为困难。

功能的扩展体现在引进新的产品上。“开–闭”原则要求系统允许当新的产品加入系统中,而无需对现有代码进行修改。这一点对于产品的消费角色是成立的,而对于工厂角色是不成立的:

对于产品消费角色来说,任何时候需要某种产品,只需向工厂角色请求即可。而工厂角色在接到请求后,会自行判断创建和提供哪一个产品。所以,产品消费角色无需知道它得到的是哪一个产品;换言之,产品消费角色无需修改就可以接纳新的产品。

对于工厂角色来说,增加新的产品是一个痛苦的过程。工厂角色必须知道每一种产品,如何创建它们,以及何时向客户端提供它们。

换言之,接纳新的产品意味着修改这个工厂角色的源代码。简单工厂角色只在有限的程度上支持“开–闭”原则。

3.工厂模式

工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。

在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类去做。这个核心类则摇身一变,成为了一个抽象工厂角色,仅负责给出具体工厂子类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。这种进一步抽象化的结果,使这种工厂方法模式可以用来允许系统在不修改具体工厂角色的情况下引进新的产品。

下面这张图片中会有两次方法延迟,要到VeggieGardener的实现类中才能被真正的实现factory方法
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第23张图片

3.1结构与角色

抽象工厂(Creator)角色:担任这个角色的是工厂方法模式的核心,它是与应用程序无关的。任何在模式中创建对象的工厂类必须实现这个接口。在上面的系统中这个角色由接口Creator 扮演;在实际的系统中,这个角色也常常使用抽象类实现。

具体工厂(Concrete Creator)角色:担任这个角色的是实现了抽象工厂接口的具体类。具体工厂角色含有与应用密切相关的逻辑,并且受到应用程序的调用以创建产品对象。在本系统中给出了两个这样的角色,也就是具体Java 类ConcreteCreator1 和ConcreteCreator2。

抽象产品(Product)角色:工厂方法模式所创建的对象的超类型,也就是产品对象的共同父类或共同拥有的接口。在本系统中,这个角色由接口Product 扮演;在实际的系统中,这个角色也常常使用抽象类实现。

具体产品(Concrete Product)角色:这个角色实现了抽象产品角色所声明的接口。工厂方法模式所创建的每一个对象都是某个具体产品角色的实例。在本系统中,这个角色由具体类CocnreteProduct1 和oncreteProduct2 扮演,它们都实现了接口Product。

3.2优缺点

优点:

如果系统需要加入一个新的产品,那么所需要的就是向系统中加入一个这个产品类以及它所对应的工厂类。没有必要修改客户端,也没有必要修改抽象工厂角色或者其他已有的具体工厂角色。对于增加新的产品类而言,这个系统完全支持“开-闭”原则。
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第24张图片

3.3 例子

仍然是上面那个例子,进行改装

FruitGardener:

public interface FruitGardener 
{ 
     public Fruit factory();
}

AppleGardener:

public class AppleGardener implements FruitGardener{
    public Fruit factory(){
         return new Apple();
    }
} 

其他水果实现工厂如上

test:

FruitGardener apple = new AppleGardener();
apple.factory();

4抽象工厂模式

抽象工厂模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态。
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第25张图片

例子:

左边的等级结构代表工厂等级结构,右边的两个等级结构分别代表两个不同的产品的等级结构。

产品族,是指位于不同产品等级结构中,功能相关联的产品组成的家族。例如上面例子中的苹果和苹果包装

抽象工厂模式与工厂方法模式的最大区别就在于,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式则需要面对多个产品等级结构

4.1结构与角色

抽象工厂(AbstractFactory)角色:担任这个角色的是工厂方法模式的核心,它是与应用系统的商业逻辑无关的。通常使用Java 接口或者抽象Java 类实现,而所有的具体工厂类必须实现这个Java 接口或继承这个抽象Java 类。

具体工厂类(Conrete Factory)角色:这个角色直接在客户端的调用下创建产品的实例。这个角色含有选择合适的产品对象的逻辑,而这个逻辑是与应用系统的商业逻辑紧密相关的。通常使用具体Java 类实现这个角色。

抽象产品(Abstract Product)角色:担任这个角色的类是工厂方法模式所创建的对象的父类,或它们共同拥有的接口。通常使用Java 接口或者抽象Java 类实现这一角色。

具体产品(Concrete Product)角色:抽象工厂模式所创建的任何产品对象都是某一个具体产品类的实例。这是客户端最终需要的东西,其内部一定充满了应用系统的商业逻辑。通常使用具体Java 类实现这个角色。

4.2 例子

public interface AbstractFactory{
     public Fruit getFruit();
     public Bag getBag();
}
public class AppleFactory implements AbstractFactory{
     public Fruit getFruit(){
          return new Apple();
     }
     public Bag getBag(){
          return new ABag();
     }
}

4.3使用场景

首先,作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过new就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。

其次,工厂模式是一种典型的解耦模式,迪米特法则在工厂模式中表现的尤为明显。假如调用者自己组装产品(组装汽车)需要增加依赖关系时,可以考虑使用工厂模式。将会大大降低对象之间的耦合度。

再次,由于工厂模式是依靠抽象架构的,它把实例化产品的任务交由实现类完成,扩展性比较好。也就是说,当需要系统有比较好的扩展性时,可以考虑工厂模式,不同的产品用不同的实现工厂来组装。

5.适配器模式

将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起的那些类可以一起工作。Adapter模式也叫做包装器Wrapper。

自我理解

需要内容在A类里面,但是现在的设定是我只能够使用B类,那么就需要提供一个适配器,使A转换为B类,能够被我使用

分类:

根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。

5.1 示例1

类适配器

通过多重继承目标接口和被适配者类方式来实现适配

举例(将USB接口转为VGA接口),类图如下:
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第26张图片

USBImpl的代码:

public class USBImpl implements USB{
       @Override
       public void showPPT() {
              // TODO Auto-generated method stub
              System.out.println("PPT内容演示");
       }
}

AdatperUSB2VGA 首先继承USBImpl获取USB的功能,其次,实现VGA接口,表示该类的类型为VGA。

public class AdapterUSB2VGA extends USBImpl implements VGA {
       @Override
       public void projection() {
              super.showPPT();
       }
}

Projector将USB映射为VGA,只有VGA接口才可以连接上投影仪进行投影

public class Projector<T> {
       public void projection(T t) {
              if (t instanceof VGA) {
                     System.out.println("开始投影");
                     VGA v = new VGAImpl();
                     v = (VGA) t;
                     v.projection();
              } else {
                     System.out.println("接口不匹配,无法投影");
              }
       }
}

test代码

   @Test
   public void test2(){
          //通过适配器创建一个VGA对象,这个适配器实际是使用的是USB的showPPT()方法
          VGA a=new AdapterUSB2VGA();
          //进行投影
          Projector p1=new Projector();
          p1.projection(a);
   } 

对象适配器
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第27张图片

public class AdapterUSB2VGA implements VGA {
       USB u = new USBImpl();
       @Override
       public void projection() {
              u.showPPT();
       }
}

实现VGA接口,表示适配器类是VGA类型的,适配器方法中直接使用USB对象。

接口适配器:

当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。

举例(将USB接口转为VGA接口,VGA中的b()和c()不会被实现),类图如下:
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第28张图片

AdapterUSB2VGA抽象类

public abstract class AdapterUSB2VGA implements VGA {
       USB u = new USBImpl();
       @Override
       public void projection() {
              u.showPPT();
       }
       @Override
       public void b() {
       };
       @Override
       public void c() {
       };
}

AdapterUSB2VGA实现,不用去实现b()和c()方法。

public class AdapterUSB2VGAImpl extends AdapterUSB2VGA {
       public void projection() {
              super.projection();
       }
}

5.2 示例2

Target定义Client使用的特定领域相关的接口。

Adaptee定义对Adaptee接口与Target接口进行适配。

Adapter对Adaptee接口与Target接口进行适配。

class Adaptee {
  public void SpecificRequest() {}
  }
interface Target {
  public void Request();
}
class Adapter implements Target {
  Adaptee adaptee;
  public Adapter(Adaptee ee) {
    adaptee = ee;
  }
  public void Request() {
    // Implement behavior using 
    // methods in Adaptee:
    adaptee.SpecificRequest();
  }
} 

5.3 使用场景

类适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。

对象适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Wrapper类,持有原类的一个实例,在Wrapper类的方法中,调用实例的方法就行。

接口适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。

6.代理模式

6.1 虚拟代理

虚拟代理模式(Virtual Proxy)是一种节省内存的技术,它建议创建那些占用大量内存或处理复杂的对象时,把创建这类对象推迟到使用它的时候

6.1.1 简单示例

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第29张图片

首先写抽象类Prize:

package Agency;

public abstract class Prize {
    protected  String name;
    public Prize(String name)
    {
        super();
        this.name=name;
    }
    public String getName()
    {
        return this.name;
    }
    abstract  public void checkPrize();//具体兑奖细节
}

然后写子类RealPrize

package Agency;

public class RealPrize extends Prize //兑奖
 {

     public RealPrize(String name)
     {
         super(name);

     }
     public void checkPrize()
     {
         System.out.println("Please check the prize--"+this.getName());
     }

}

这个LotteryTicket类就是我们的虚拟代理类。
具体代码如下

package Agency;

public class LotteryTicket extends Prize//虚拟代理
 {
     public LotteryTicket(String name)
     {
         super(name);
     }
     private Prize realPrize;
     public void checkPrize()
     {
         if(realPrize==null)
         {
             realPrize=new RealPrize(this.name);
         }
         realPrize.checkPrize();
     }
}

最后写个客户端类,进行模拟抽奖:

package Agency;

import java.util.ArrayList;

public class Client//模拟摇奖
{public static void main(String arg[])
{
    ArrayList<Prize> prizePool=new ArrayList<Prize>();
   prizePool.add(new LotteryTicket("Car"));
    prizePool.add(new LotteryTicket("Computer"));
    prizePool.add(new LotteryTicket("Doll"));
    prizePool.add(new LotteryTicket("5 doller"));
    for(int i=0;i<16;i++)
    {
        prizePool.add(new LotteryTicket("nothing"));
    }
    java.util.Random random=new java.util.Random();
    int mynumber=random.nextInt(20);
    Prize a=prizePool.get(mynumber);
    Prize b=prizePool.get(2);
    System.out.println("Your number is "+mynumber);
    a.checkPrize();
    System.out.println("Your number is "+mynumber);
    b.checkPrize();
}
}
6.1.2 优缺点

优点

这种方法的优点是,在应用程序启动时,由于不需要创建和装载所有的对象,因此加速了应用程序的启动。

缺点

因为不能保证特定的应用程序对象被创建,在访问这个对象的任何地方,都需要检测确认它不是空(null)。也就是,这种检测的时间消耗是最大的缺点。

应用虚拟代理模式,需要设计一个与真实对象具有相同接口的单独对象(指虚拟代理)。不同的客户对象可以在创建和使用真实对象地方用相应的虚拟对象来代替。虚拟对象把真实对象的引用作为它的实例变量维护。代理对象不要自动创建真实对象,当客户需要真实对象的服务时,调用虚拟代理对象上的方法,并且检测真实对象是否被创建。

如果真实对象已经创建,代理把调用转发给真实对象,如果真实对象没有被创建:

  1. 代理对象创建真实对象
  2. 代理对象把这个对象分配给引用变量。
  3. 代理把调用转发给真实对象

按照这种安排,验证对象存在和转发方法调用这些细节对于客户是不可见的。客户对象就像和真实对象一样与代理对象进行交互。因此客户从检测真实对象是否为null中解脱出来,另外,由于创建代理对象在时间和处理复杂度上要少于创建真实对象。因此,在应用程序启动的时候,用代理对象代替真实对象初始化。

6.2 远程代理

为一个对象在不同的地址空间提供局部代表。使用N X P r o x y类实现了这一目的。称这种代理为“大使”(A m b a s s a d o r)。

Remote Proxy可以隐藏一个对象存在于不同地址空间的事实。

它使得客户端程序可以访问在远程主机上的对象,远程主机可能具有更好的计算性能与处理速度,可以快速响应并处理客户端的请求。远程代理可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户端完全可以认为被代理的远程业务对象是在本地而不是在远程,而远程代理对象承担了大部分的网络通信工作,并负责对远程业务方法的调用。

6.3 保护代理

保护代理(Protection Proxy)控制对原始对象的访问。保护代理用于对象应该有不同的访问权限的时候。例如,在C h o i c e s操作系统中K e m e l P r o x i e s为操作系统对象提供了访问保护。

6.4 智能指引

取代了简单的指针,它在访问对象时执行一些附加操作。它的典型用途包括:

• 对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它(也称为S m a r tP o i n t e r s)。

• 当第一次引用一个持久对象时,将它装入内存。

• 在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。

简单示例(计算一个页面的访问量)

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第30张图片

package Guide;

public interface IPage {
    public void display();//显示页面
}
class Page implements IPage{
    private String url;
    public Page(String url)
    {
        this.url=url;
    }
    public void display(){
        System.out.println("Content from"+url);
    }
}

package Guide;

public class PageProxy implements  IPage{
    private IPage page;
    private  int count=0;
    public PageProxy(IPage page)
    {
        super();
        this.page=page;
    }
    public void display()
    {
        updateCount();
        page.display();
    }
    private void updateCount()
    {
        System.out.println("Visits:"+(++count));
    }
}
package Guide;

public class Client {
    public static void main(String arg[])
    {
        IPage pageProxy=new PageProxy(new Page(" www.baidu.com"));
        pageProxy.display();
        pageProxy.display();
    }
}

7.桥接模式

将抽象部分与它的实现部分分离,使它们都可以独立地变化。

7.1 桥接模式简单示例

假如你有一个几何形状Shape类,从它能扩展出两个子类: 圆形Circle和 方形Square 。 你希望对这样的类层次结构进行扩展以使其包含颜色,所以你打算创建名为红色Red和蓝色Blue的形状子类。 但是, 由于你已有两个子类, 所以总共需要创建四个类才能覆盖所有组合, 例如 蓝色圆形Blue­Circle和 红色方形Red­Square 。
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第31张图片

在层次结构中新增形状和颜色将导致代码复杂程度指数增长。 例如添加三角形状, 你需要新增两个子类, 也就是每种颜色一个; 此后新增一种新颜色需要新增三个子类, 即每种形状一个。 照这样下去,所有组合类的数量将以几何级数增长,情况会越来越糟糕。

解决方案:
问题的根本原因在于我们试图在两个独立的维度——形状与颜色上进行扩展。这在处理继承时是很常见的问题。

桥接模式 通过将继承改为组合的方式来解决这个问题。 具体来说, 就是抽取其中一个维度并使之成为独立的类层次, 这样就可以在初始类中引用这个新层次的对象, 从而使得一个类不必拥有所有的状态和行为
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第32张图片
根据该方法, 我们可以将颜色相关的代码抽取到拥有 红色和 蓝色两个子类的颜色类中, 然后在 形状类中添加一个指向某一颜色对象的引用成员变量。 现在, 形状类可以将所有与颜色相关的工作委派给连入的颜色对象。 这样的引用就成为了 形状和 颜色之间的桥梁。 此后, 新增颜色将不再需要修改形状的类层次, 反之亦然。

我们就以上述形状与颜色这两个独立的维度来实现给不同的形状刷上不同颜色的例子来讲解:
ColorAPI :用于画各种颜色的接口

public interface ColorAPI {
public void paint();
}

BlueColorAPI :画蓝色的实现类

public class BlueColorAPI implements ColorAPI {
@Override
public void paint() {
    System.out.println("画上蓝色");
}
}

RedColorAPI :画红色的实现类

public class RedColorAPI implements ColorAPI
{
@Override
public void paint() {
    System.out.println("画上红色");
}
}

Shape :抽象形状类

public abstract class Shape {
protected ColorAPI colorAPI;    //添加一个颜色的成员变量以调用ColorAPI 的方法来实现给不同的形状上色

public void setDrawAPI(ColorAPI colorAPI) {      //注入颜色成员变量
    this.colorAPI= colorAPI;
}
public abstract void draw();        
}

Circle :圆形类

public class Circle extends Shape {
@Override
public void draw() {
    System.out.print("我是圆形");
    colorAPI.paint();
}
}

Rectangle :长方形类

public class Rectangle extends Shape {
@Override
public void draw() {
    System.out.print("我是长方形");
    colorAPI.paint();
}
}

Client:客户端

 * public class Client {

   public static void main(String[] args) {
       //创建一个圆形
       Shape shape = new Circle();
       //给圆形蓝色的颜料
       shape.setDrawAPI(new BlueColorAPI());
       //上色
       shape.draw();


        //创建一个长方形
        Shape shape1 = new Rectangle();
        //给长方形红色的颜料
        shape1.setDrawAPI(new RedColorAPI());
        //上色
        shape1.draw();
    
    }

}

打印输出:

我是圆形画上蓝色
我是长方形画上红色

假如现在客户让我们增了一个三角形,我们只需要新增一个三角形类就可以了,而无需把每一种颜色都增加一个,我们在客户端调用时只需按照需求来挑选即可:

public class Triangle extends Shape {
@Override
public void draw() {
    System.out.println("我是三角形");
    colorAPI.paint();
}
}

增加颜色也是一样,我们只需要增加一个新的颜色并实现ColorAPI的接口即可,而无需更改类的层次,例如增加一个绿色:

  • public class GreenColorAPI implements ColorAPI {
       @Override
       public void paint() {
           System.out.println("画上绿色");
    

    现在再来看“将抽象部分与他的实现部分分离”这句话,实际上就是在说实现系统可能有多个角度分类(例如例子中的形状与颜色),每一种分类都有可能变化,那么把这种多角度分离出来让他们独立变化,减少他们之间的耦合。

7.2 使用环境

  • 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
  • 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
  • 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第33张图片

其中包含如下角色:

Abstraction(抽象类):用于定义抽象类的接口,它一般是抽象类而不是接口,其中定义了一个 Implementor(实现类接口)类型的对象并可以维护该对象,它与 Implementor 之间具有关联关系。
RefinedAbstraction(提炼抽象类):扩充由 Abstraction 定义的接口,通常情况下它不再是抽象类而是具体类,它实现了在 Abstraction 中声明的抽象业务方法,在 RefinedAbstraction 中可以调用在 Implementor 中定义的业务方法。
Implementor(实现类接口):定义实现类的接口,这个接口不一定要与 Abstraction 的接口完全一致,事实上这两个接口可以完全不同,一般而言,Implementor 接口仅提供基本操作,而 Abstraction 定义的接口可能会做更多更复杂的操作。Implementor 接口对这些基本操作进行了声明,而具体实现交给其子类。通过关联关系,在 Abstraction 中不仅拥有自己的方法,还可以调用到 Implementor 中定义的方法,使用关联关系来替代继承关系。
ConcreteImplementor(具体实现类):具体实现 Implementor 接口,在不同的 ConcreteImplementor 中提供基本操作的不同实现,在程序运行时,ConcreteImplementor 对象将替换其父类对象,提供给抽象类具体的业务操作方法。

7.3 优缺点分析

优点

实现抽象和实现的分离
桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统
桥接模式有时类似于多继承方案,但是多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差,而且多继承结构中类的个数非常庞大,桥接模式是比多继承方案更好的解决方法
缺点

桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。

8. 装饰器模式

动态地给一个对象添加一些额外的职责,别名也WrapperDecorator必须和要包装的的对象具有相同的接口,有时我们希望给某个对象而不是整个类添加一些功能。

8.1 结构图

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第34张图片

  • Component为统一接口,也是装饰类和被装饰类的基本类型。
  • ConcreteComponent为具体实现类,也是被装饰类,他本身是个具有一些功能的完整的类。
  • Decorator是装饰类,实现了Component接口的同时还在内部维护了一个ConcreteComponent的实例,并可以通过构造函数初始化。而Decorator本身,通常采用默认实现,他的存在仅仅是一个声明:我要生产出一些用于装饰的子类了。 而其子类才是赋有具体装饰效果的装饰产品类。
  • ConcreteDecorator是具体的装饰产品类,每一种装饰产品都具有特定的装饰效果。可以通过构造器声明装饰哪种类型的ConcreteComponent,从而对其进行装饰。

8.2 简单示例

使用: 为了加大商城的优惠力度,开发往往要设计红包 + 限时折扣或红包 + 抵扣券等组合来实现多重优惠。而在平时,由于某些特殊原因,商家还会赠送特殊抵扣券给购买用户,而特殊抵扣券 + 各种优惠又是另一种组合方式。

这里的示例是装修房子

1.接口定义:去定义具体需要实现的相关方法

/**

描述:定义一个基本装修接口
*

@author yanfengzhang

@date 2020-04-19 13:32
*/
public interface IDecorator {
/**

装修方法
*/
void decorate();
}

2.具体对象:针对需要实现的方法做初始化操作,即基本的实现

/**

描述:装修基本类
*/
public class Decorator implements IDecorator {
/**

基本实现方法
*/
@Override
public void decorate() {
System.out.println("水电装修、天花板以及粉刷墙.");
}
}

3.装饰类:抽象类,初始化具体对象

/**

描述:基本装饰类
*/
public abstract class BaseDecorator implements IDecorator {
private IDecorator decorator;
public BaseDecorator(IDecorator decorator) {
    this.decorator = decorator;
}
/**
调用装饰方法
*/
@Override
public void decorate() {
if (decorator != null) {
    decorator.decorate();
}
}
}

4.其他具体装饰类实现自己特性的需求
如果我们想要在基础类上添加新的装修功能,只需要基于抽象类 BaseDecorator 去实现继承类,通过构造函数调用父类,以及重写装修方法实现装修窗帘的功能即可。

/**

描述:窗帘装饰类
*

@author yanfengzhang

@date 2020-04-19 13:35
*/
public class CurtainDecorator extends BaseDecorator {
public CurtainDecorator(IDecorator decorator) {
    super(decorator);
}

/**

窗帘具体装饰方法
*/
@Override
public void decorate() {
System.out.println("窗帘装饰。。。");
super.decorate();
}
}

5.实际使用

public class Test {
public static void main(String[] args) {
    IDecorator decorator = new Decorator();
    IDecorator curtainDecorator = new CurtainDecorator(decorator);
    curtainDecorator.decorate();
}
}

8.3 咖啡加糖加奶示例

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第35张图片

举例(咖啡馆订单项目):

1)咖啡种类:Espresso、ShortBlack、LongBlack、Decaf

2)调料(装饰者):Milk、Soy、Chocolate),类图如上。

被装饰的对象和装饰者都继承自同一个超类(饮料类)

public abstract class Drink {
       public String description="";
       private float price=0f;
       public void setDescription(String description)
       {
              this.description=description;
       }
       
       public String getDescription()
       {
              return description+"-"+this.getPrice();
       }
       public float getPrice()
       {
              return price;
       }
       public void setPrice(float price)
       {
              this.price=price;
       }
       public abstract float cost();
}

被装饰的对象,不用去改造。原来怎么样写,现在还是怎么写

public  class Coffee extends Drink {
       @Override
       public float cost() {
              // TODO Auto-generated method stub
              return super.getPrice();
       }
       
}

coffee类的实现

public class Decaf extends Coffee {
       public Decaf()
       {
              super.setDescription("Decaf");
              super.setPrice(3.0f);
       }
}

装饰者

装饰者不仅要考虑自身,还要考虑被它修饰的对象,它是在被修饰的对象上继续添加修饰。例如,咖啡里面加牛奶,再加巧克力。加糖后价格为coffee+milk。再加牛奶价格为coffee+milk+chocolate。

public class Decorator extends Drink {
       private Drink Obj;
       public Decorator(Drink Obj) {
              this.Obj = Obj;
       };
       @Override
       public float cost() {
              // TODO Auto-generated method stub
              return super.getPrice() + Obj.cost();
       }
       @Override
       public String getDescription() {
              return super.description + "-" + super.getPrice() + "&&" + Obj.getDescription();
       }
}

装饰者实例化(加牛奶)。这里面要对被修饰的对象进行实例化。

public class Milk extends Decorator {
       public Milk(Drink Obj) {          
              super(Obj);
              // TODO Auto-generated constructor stub
              super.setDescription("Milk");
              super.setPrice(2.0f);
       }
}

coffee店:初始化一个被修饰对象,修饰者实例需要对被修改者实例化,才能对具体的被修饰者进行修饰

public class CoffeeBar {
       public static void main(String[] args) {
              Drink order;
              order = new Decaf();
              System.out.println("order1 price:" + order.cost());
              System.out.println("order1 desc:" + order.getDescription());
              System.out.println("****************");
              order = new LongBlack();
              order = new Milk(order);
              order = new Chocolate(order);
              order = new Chocolate(order);
              System.out.println("order2 price:" + order.cost());
              System.out.println("order2 desc:" + order.getDescription());
       }
}

装饰者和被装饰者之间必须是一样的类型,也就是要有共同的超类。在这里应用继承并不是实现方法的复制,而是实现类型的匹配。因为装饰者和被装饰者是同一个类型,因此装饰者可以取代被装饰者,这样就使被装饰者拥有了装饰者独有的行为。根据装饰者模式的理念,我们可以在任何时候,实现新的装饰者增加新的行为。如果是用继承,每当需要增加新的行为时,就要修改原程序了。

代理、桥接、装饰器、适配器的区别

代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

桥接模式:桥接模式的目的是将接口部分和实现部分分离,而让它们可以较为容易、独立地加以改变。

装饰器模式:装饰器模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

适配器模式:适配器模式是一种时候的补救策略,适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原来类相同的接口。

9. 职责链模式

定义:如果有多个对象有机会处理请求,责任链可使请求的发送者和接受者解耦,请求沿着责任链传递,直到有一个对象处理了它为止。

主要解决:职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。

何时使用:在处理消息的时候以过滤很多道。

如何解决:拦截的类都实现统一接口。

关键代码:Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。

9.1 结构图

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第36张图片

1.抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。

2.具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。

3.客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

9.2 简单示例

举例(购买请求决策,价格不同要由不同的级别决定:组长、部长、副部、总裁)。类图如下:
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第37张图片

1 决策者抽象类,包含对请求处理的函数,同时还包含指定下一个决策者的函数:

public abstract class Approver {
	 Approver successor;
	 String Name;
	public Approver(String Name)
	{
		this.Name=Name;
	}
	public abstract void ProcessRequest( PurchaseRequest request);
	public void SetSuccessor(Approver successor) {
		// TODO Auto-generated method stub
		this.successor=successor;
	}
}

2 客户端以及请求

public class PurchaseRequest {
	private int Type = 0;
	private int Number = 0;
	private float Price = 0;
	private int ID = 0;
 
	public PurchaseRequest(int Type, int Number, float Price) {
		this.Type = Type;
		this.Number = Number;
		this.Price = Price;
	}
 
	public int GetType() {
		return Type;
	}
 
	public float GetSum() {
		return Number * Price;
	}
 
	public int GetID() {
		return (int) (Math.random() * 1000);
	}
}

public class Client {
 
	public Client() {
 
	}
 
	public PurchaseRequest sendRequst(int Type, int Number, float Price) {
		return new PurchaseRequest(Type, Number, Price);
	}
 
}

3 组长、部长。。。继承决策者抽象类

public class GroupApprover extends Approver {
 
	public GroupApprover(String Name) {
		super(Name + " GroupLeader");
		// TODO Auto-generated constructor stub
 
	}
 
	@Override
	public void ProcessRequest(PurchaseRequest request) {
		// TODO Auto-generated method stub
 
		if (request.GetSum() < 5000) {
			System.out.println("**This request " + request.GetID() + " will be handled by " + this.Name + " **");
		} else {
			successor.ProcessRequest(request);
		}
	}
 
}
public class DepartmentApprover extends Approver {
 
	public DepartmentApprover(String Name) {
		super(Name + " DepartmentLeader");
 
	}
 
	@Override
	public void ProcessRequest(PurchaseRequest request) {
		// TODO Auto-generated method stub
 
		if ((5000 <= request.GetSum()) && (request.GetSum() < 10000)) {
			System.out.println("**This request " + request.GetID()
					+ " will be handled by " + this.Name + " **");
		} else {
			successor.ProcessRequest(request);
		}
 
	}
 
}

4测试

public class MainTest {
 
	public static void main(String[] args) {
 
		Client mClient = new Client();
		Approver GroupLeader = new GroupApprover("Tom");
          Approver VicePresident = new VicePresidentApprover("Kate");
		Approver DepartmentLeader = new DepartmentApprover("Jerry");
		Approver President = new PresidentApprover("Bush");
 
		GroupLeader.SetSuccessor(VicePresident);	
		VicePresident.SetSuccessor(DepartmentLeader);
           DepartmentLeader.SetSuccessor(President);
		President.SetSuccessor(GroupLeader);
 
		GroupLeader.ProcessRequest(mClient.sendRequst(1, 10000, 40));
	}
}

9.3 使用情况

系统已经有一个由 处理者对象组成的链。这个链 可能由复合模式给出

第一、当有多于一个的处 理者对象会处理一个请求,而 且在事先并不知道到底由哪一 个处理者对象处理一个请求。 这个处理者对象是动态确定的。

第二、当系统想发出一个请求给多个处理者对象中的某一个,但是不明显指定是哪一 个处理者对象会处理此请求。

第三、当处理一个请求的处理者对象集合需要动态地指定时。

10. 观察者模式

定义: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

如何解决:使用面向对象技术,可以将这种依赖关系弱化。

关键代码:在抽象类里有一个 ArrayList 存放观察者们。

优点: 1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。

缺点: 1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

10.1 结构图

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第38张图片

10.2 示例

举例(有一个微信公众号服务,不定时发布一些消息,关注公众号就可以收到推送消息,取消关注就收不到推送消息。)类图如下:
山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第39张图片

1、定义一个抽象被观察者接口

public interface Subject {
	
	  public void registerObserver(Observer o);
	  public void removeObserver(Observer o);
	  public void notifyObserver();
 
}

2、定义一个抽象观察者接口

public interface Observer {
	
	public void update(String message);
 
}

3、定义被观察者,实现了Observerable接口,对Observerable接口的三个方法进行了具体实现,同时有一个List集合,用以保存注册的观察者,等需要通知观察者时,遍历该集合即可。

public class WechatServer implements Subject {
 
	private List<Observer> list;
	private String message;
 
	public WechatServer() {
		list = new ArrayList<Observer>();
	}
 
	@Override
	public void registerObserver(Observer o) {
		// TODO Auto-generated method stub
		list.add(o);
	}
 
	@Override
	public void removeObserver(Observer o) {
		// TODO Auto-generated method stub
		if (!list.isEmpty()) {
			list.remove(o);
		}
	}
 
	@Override
	public void notifyObserver() {
		// TODO Auto-generated method stub
		for (Observer o : list) {
			o.update(message);
		}
	}
 
	public void setInfomation(String s) {
		this.message = s;
		System.out.println("微信服务更新消息: " + s);
		// 消息更新,通知所有观察者
		notifyObserver();
	}
}

4、定义具体观察者,微信公众号的具体观察者为用户User

public class User implements Observer {
 
	private String name;
	private String message;
 
	public User(String name) {
		this.name = name;
	}
 
	@Override
	public void update(String message) {
		this.message = message;
		read();
	}
 
	public void read() {
		System.out.println(name + " 收到推送消息: " + message);
	}
 
}

5、编写一个测试类

public class MainTest {
	
	 public static void main(String[] args) {
		 
	        WechatServer server = new WechatServer();
	        
	        Observer userZhang = new User("ZhangSan");
	        Observer userLi = new User("LiSi");
	        Observer userWang = new User("WangWu");
	        
	        server.registerObserver(userZhang);
	        server.registerObserver(userLi);
	        server.registerObserver(userWang);
	        server.setInfomation("PHP是世界上最好用的语言!");
	        
	        System.out.println("----------------------------------------------");
	        server.removeObserver(userZhang);
	        server.setInfomation("JAVA是世界上最好用的语言!");
	        
	    }
 
}

10.3 模式优缺点

1 ) 目标和观察者间的抽象耦合一个目标所知道的仅仅是它有一系列观察者, 每个都符合抽象的O b s e r v e r类的简单接口。目标不知道任何一个观察者属于哪一个具体的类。这样目标和观察者之间的耦合是抽象的和最小的。因为目标和观察者不是紧密耦合的, 它们可以属于一个系统中的不同抽象层次。一个处于较低层次的目标对象可与一个处于较高层次的观察者通信并通知它, 这样就保持了系统层次的完整。

2)支持广播通信 不像通常的请求, 目标发送的通知不需指定它的接收者。通知被自动广播给所有已向该目标对象登记的有关对象。目标对象并不关心到底有多少对象对自己感兴趣;它唯一的责任就是通知它的各观察者。这给了你在任何时刻增加和删除观察者的自由。处理还是忽略一个通知取决于观察者。

3)意外的更新 因为一个观察者并不知道其它观察者的存在, 它可能对改变目标的最终代价一无所知。在目标上一个看似无害的的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。

11. 策略模式

比如说对象的某个行为,在不同场景中有不同的实现方式,这样就可以将这些实现方式定义成一组策略,每个实现类对应一个策略,在不同的场景就使用不同的实现类,并且可以自由切换策略。

11.1 结构图

山东大学软件学院 - 面向对象开发技术 - 期末复习知识点总结_第40张图片

策略模式需要一个策略接口,不同的策略实现不同的实现类,在具体业务环境中仅持有该策略接口,根据不同的场景使用不同的实现类即可。

面向接口编程,而不是面向实现。

11.2 简单示例

举个实际的例子,XX 公司是做支付的,根据不同的客户类型会有不同的支付方式和支付产品,比如:信用卡、本地支付,而本地支付在中国又有微信支付、支付宝、云闪付、等更多其他第三方支付公司,这时候策略模式就派上用场了。

传统的 if/ else/ switch 等判断写法大家都会写,这里就不贴代码了,直接看策略模式怎么搞!

1、定义策略接口
定义一个策略接口,所有支付方式的接口。

策略接口:

public interface IPayment {
    public String pay(double money);
}
2、定义各种策略

定义各种支付策略,微信支付、支付宝、云闪付等支付实现类都实现这个接口。

微信支付实现:

@Service("WechatPay")
public class WechatPay implements IPayment {

   public String pay(double money) {
        return "微信支付成功,共计支付"+money;
    }

}

支付宝实现:

@Service("Alipay")
public class Alipay implements IPayment {

    
    public String pay(double money) {
        return "支付宝支付成功,共计支付"+money;
    }

}

上下文类

也叫做上下文类或环境类,起承上启下封装作用。

public class enviroment{
      private IPayment ipayment;
     
     public enviroment(IPayment ipayment){
          this.ipayment = ipayment;
     }
     
     public String myresult(){
          return ipayment.pay();
     }
}

测试类:

public class test{
     public static void main(String[] args){
          Ipayment wechat = new WechatPay();
          enviroment en = new enviroment(wechat);   
     }
}

11.3 使用范围

在以下情况下可以使用策略模式:

如果在一个系统里面有许多类,它们之间的区别仅在于它们 的行为,那么使用策略模式可以动态地让一个对象在许多行 为中选择一种行为。
一个系统需要动态地在几种算法中选择一种。
如果一个对象有很多的行为,如果不用恰当的模式,这些行 为就只好使用多重的条件选择语句来实现。
不希望客户端知道复杂的、与算法相关的数据结构,在具体 策略类中封装算法和相关的数据结构,提高算法的保密性与 安全性。
在我们生活中比较常见的应用模式有:

1、电商网站支付方式,一般分为银联、微信、支付宝,可以采用策略模式
2、电商网站活动方式,一般分为满减送、限时折扣、包邮活动,拼团等可以采用策略模式

你可能感兴趣的:(学习)