文章目录
因为疫情回不了校,5月份打算学设计模式,《HeadFirst设计模式》这本书放在学校,只能看电子版
关于源码,跟设计模式联系只是分析了一点点,比如装饰者模式,还有aop,spring,springmvc等还没跟学的设计模式联系起来,这几个月搞秋招,还是好好打基础,有时间就把一些简单的源码跟设计模式联系起来。
设计模式的类图除了代理模式,其他都是仿造《HeadFirst设计模式》的,就是不太正规,以后有时间再一一修改。
1. 前言
设计模式体现了代码的耦合性, 内聚性以及可维护性,可扩展性,重用性,灵活性。
- 代码重用性(即:相同功能的代码,不用多次编写)
- 可读性(即:编程规范性,便于其他程序员的阅读和理解)
- 可扩展性(即:当需要增加新的功能时,非常的方便,称为可维护)
- 可靠性(即:当我们增加新的功能后,对原来的功能没有影响)
- 使程序呈现高内聚,低耦合的特性。
内聚:描述一个类或模块的功能关系。比如当一个模块或一个类被设计成只支持一组相关的功能时,可以说具有高内聚;反之,如果被设计成支持一组不相关的功能时,可以说具有低内聚。
耦合:描述模块之间的依赖程度。耦合跟内聚密切相关,一般程序结构中各模块的内聚程度越高,模块间的耦合度越低。
我记得在软件工程这门课程有学过内聚的几种程度和耦合的几种程度,顺便复习一下。
根据内聚的程度从低到高还可以分为:
- 偶然内聚:模块中的各个成分毫无关系。
- 逻辑内聚:把几种相关功能放在一起,每次被调用时,由传送给模块参数来确定该模块应完成哪一种功能 。
- 时间内聚:把需要同时执行的动作组合在一起形成的模块。
- 过程内聚:一个模块内的处理元素是相关的,而且必须以特定次序执行则称为过程内聚。构件或者操作的组合方式时,允许在调用前面的构件或操作之后,马上调用后面的构件或操作,即使两者之间没有数据进行传递。比如程序流程图。
- 通信内聚:模块内各个组成部分都使用相同的数据结构或产生相同的数据结构。
- 顺序内聚:一个模块中各个处理元素和同一个功能密切相关,而且这些处理必须顺序执行,通常前一个处理元素的输出时后一个处理元素的输入。例如要完成获取订单信息的功能,前一个功能获取用户信息,后一个执行计算均价操作,显然该模块内两部分紧密关联。顺序内聚的内聚度比较高,但缺点是不如功能内聚易于维护。
- 功能内聚:模块内所有元素的各个组成部分全部都为完成同一个功能而存在,共同完成一个单一的功能,模块已不可再分。
耦合的强度依赖于:
- 一个模块对另一个模块的调用。
- 一个模块向另一个模块传递的数据量。
- 一个模块施加到另一个模块的控制的多少。
- 模块之间接口的复杂程度。
根据耦合的程度从高到低还可以分为:
- 内容耦合:当一个模块直接修改另一个模块的内容(数据)或直接传入到另一个模块中,这两个模块的依赖程度最高。
- 公共耦合:一组模块都访问同一个全局数据结构。
- 外部耦合:模块间通过软件之外的环境关联。
- 控制耦合:模块之间传递的不是数据信息,而是控制信息例如标志、开关量等,一个模块控制了另一个模块的功能。
- 标记耦合:调用模块和被调用模块之间传递数据结构而不是简单数据,同时也称作特征耦合。类似于高级语言中的地址(引用)传递。
- 数据耦合:调用模块和被调用模块之间只传递简单的数据项参数。类似于高级语言中的值传递。
- 非直接耦合:两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的。
2. UML类图
首先,Eclipse中可以画类图,安装请看这里;也可以在Idea中安装PlantUML,但是需要学习一点语法;也可以使用PowerDesign。
UML类图:用于描述系统中的类(对象)本身的组合和类(对象)之间的各种静态关系,这些关系有:泛化、实现、依赖、关联、组合、聚合。
2.1 泛化(Generalization)
指的是一个类继承另一个类,并且可以在子类添加它自己的新功能,是依赖关系的特例。
代码体现形式:继承。
箭头指向:实线,指向父类/基类
2.2 实现(Realization)
体现的是类与接口的关系,是依赖关系的特例。
代码体现形式:实现。
箭头指向:虚线,指向接口。
2.3 依赖(Dependency)
一个类使用到了另一个类,表现的是一种弱依赖(临时性)。
代码体现形式:方法中的参数引用到另一个类、局部变量,调用另一个类的静态方法,返回值、成员变量。
箭头指向:虚线,指向被使用的。
2.4 关联(Association)
一个类知道另一个类的属性和方法,表现的是一种强依赖(长期性),是依赖关系的特例。比如老师与学生。关联有多种:一对一、一对多、多对多。
代码体现形式:类B以属性的形式出现来类A中(成员变量)。
箭头指向:实线,指向被关联的类。
2.5 组合(Composite)
体现整体与部分的关系,但此时整体与部分是不可分的,整体生命周期的结束也意味着部分生命周期的结束。比如人和心脏。组合是聚合中的一种特例(或者是依赖关系的特例),比聚合的关系强,关联和聚合在语法上无法区分,必须考察具体的逻辑关系。组合有多种:一对一、一对多、多对多。
代码体现形式:成员变量。
箭头指向:实线,箭头指向部分,菱形指向整体。
2.6 聚合(Aggregation)
体现整体与部分的关系,但此时整体与部分是可分的,都拥有各自的生命周期。比如人和手机。聚合是关联中的一种特例(或者是依赖关系的特例),比关联的关系强,关联和聚合在语法上无法区分,必须考察具体的逻辑关系。聚合有多种:一对一、一对多、多对多。
代码体现形式:成员变量。
箭头指向:虚线,箭头指向部分,菱形指向整体。
由强到弱:依赖 < 关联 < 聚合 < 组合。
3. 其他原则
3.1 单一责任原则
单一责任原则:应该尽量让每个类保持单一责任。
如果违背单一责任原则:
public class SingleResponsibilityTest {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("汽车");
vehicle.run("飞机"); // 这里就不符合实际
}
}
/**
* 如同上面所示,在调用第二次run传入“飞机”,打印出:飞机在地上驾驶
* 这话是错的,原因就是违背了单一责任原则
* 这种情况下,因为一个类出现多种责任,那么需要把该类分解成多个单一责任的类
*/
class Vehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在地上驾驶");
}
}
现在来遵守单一责任原则:
public class PerfectSingleResponsibilityTest {
public static void main(String[] args) {
RoadVehicle roadVehicle = new RoadVehicle();
roadVehicle.run("汽车");
SkyVehicle skyVehicle = new SkyVehicle();
skyVehicle.fly("飞机");
}
}
/**
* 按照要求,分解单类多责任成多个单一责任的类
*/
class RoadVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在地上驾驶");
}
}
class SkyVehicle {
public void fly(String vehicle) {
System.out.println(vehicle + "在天上驾驶");
}
}
其实因为此类的责任就两个(也可以三个,比如水上驾驶的),可以从一个类中把多个责任分解成多个方法,虽然跟单一责任原则规定的是类责任单一,但这里的方法遵守单一责任原则:
public class SingleResponsibilityTest {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("汽车");
vehicle.fly("飞机");
}
}
class Vehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在地上驾驶");
}
public void fly(String vehicle) {
System.out.println(vehicle + "在天上驾驶");
}
}
单一原则的注意:
- 降低类的复杂度,一个类只负责一项责任,注意并不是代表一个类只能有一个方法,比如一个订单类,那么它就只管订单相关的东西;
- 提高类的可读性,可维护性;
- 降低变更引起的风险;
- 通常情况下,应当遵守单一职责原则, 只有逻辑足够简单,才可以在方法级违反单一责任原则;只有类中 的方法数量足够少,也可以在方法级别保持单一责任原则。
3.2 接口隔离原则
接口隔离原则:一个类对另一个类的依赖应该建立在最小的接口上。这个最小的接口的意思是:比如一个接口有5个方法,此时有一个类B来实现此接口,而还有另一个类C会通过此接口去依赖类B(这里体现多态),但类C会用到此接口中的3个方法,所以可以说此接口不是最小的接口。
解决方法:应该把接口更细化,或者说该类只会用到此接口中的3个方法,那么就把这3个方法独立出来称为另一个接口A,而另外两个也独立成一个接口D,然后让类B去实现这些接口,此时,类A通过接口A去依赖类B,那么A接口就可以说是最小的接口。
public class ATest {
public static void main(String[] args) {
A a = new B();
C c = new C();
c.test1(a);
c.test2(a);
c.test3(a);
}
}
/**
* 以下这样写,违背了接口隔离原则
*/
interface A {
void operation1();
void operation2();
void operation3();
void operation4();
void operation5();
}
class B implements A {
@Override
public void operation1() {
System.out.println("B实现了A的operation1");
}
@Override
public void operation2() {
System.out.println("B实现了A的operation2");
}
@Override
public void operation3() {
System.out.println("B实现了A的operation3");
}
@Override
public void operation4() {
System.out.println("B实现了A的operation4");
}
@Override
public void operation5() {
System.out.println("B实现了A的operation5");
}
}
class C {
public void test1(A a) {
a.operation1();
}
public void test2(A a) {
a.operation2();
}
public void test3(A a) {
a.operation3();
}
}
现在来遵守接口隔离原则:
public class BTest {
public static void main(String[] args) {
InterfaceA a = new ClassB();
ClassC c = new ClassC();
c.test1(a); // C类通过接口A去依赖类B(多态性)
c.test2(a);
c.test3(a);
}
}
/**
* 应该这样写
*/
interface InterfaceA {
void operation1();
void operation2();
void operation3();
}
interface InterfaceD {
void operation4();
void operation5();
}
class ClassB implements InterfaceA, InterfaceD {
@Override
public void operation1() {
System.out.println("B实现了A的operation1");
}
@Override
public void operation2() {
System.out.println("B实现了A的operation2");
}
@Override
public void operation3() {
System.out.println("B实现了A的operation3");
}
@Override
public void operation4() {
System.out.println("B实现了D的operation4");
}
@Override
public void operation5() {
System.out.println("B实现了D的operation5");
}
}
class ClassC {
public void test1(InterfaceA a) {
a.operation1();
}
public void test2(InterfaceA a) {
a.operation2();
}
public void test3(InterfaceA a) {
a.operation3();
}
}
更通俗来说就是:不要在一个接口里面放很多的方法,这样会让这个类非常臃肿,接口应该尽量细化,一个接口对应一个功能模块,同时接口里面的方法应该尽可能的少,使接口更加轻便灵活。
其实就是为了不造成浪费,搞自己需要的,别把一些不需要的也加进来,虽然说以后可能也会用到啊?但是想想,一个软件如果可扩张性好,那么就应该交给未来需要才扩展,而不是现在不需要就扩展了,并且有谁能考虑得那么全面呢?肯定是一步步慢慢来。像比如以后还想加东西,那么就新创建一个接口,然后实现即可。
3.3 依赖倒置(倒转)原则
依赖倒转原则:**针对接口编程,依赖于抽象而不依赖于具体。**网上就是前面那样说的,但是这样可能会误解,把抽象当成抽象类而已忽略接口。所以说针对接口编程,其实应该这样说更加准确:针对超类型(supertype)编程,即抽象类和接口都可以,而具体指的是具体类或实现类。
依赖倒转原则也有另外几种说法:
- 高层模块不应该依赖低层模块,二者都应该依赖抽象。比如一个类A中有类B(类B有父接口)的引用,类A的方法中有一段这样的代码:B b = new B(); b.xx(); ,那么就说类A是高层模块,而类B是底层模块;方法参数也是,比如参数设置为B b,那么就只能传 B b = new B(),然后方法内调用 b.xx();。正确的做法是:因为类B有一个父接口C,那么对于类A应该去引用接口C,而使用应该这样:C c =new B(); c.xx(); 或者对于方法参数应该这样设置:C c,然后可以传入时运用Java的多态到达目的。
- 抽象不应该依赖具体(或细节),具体应该依赖抽象。因为具体有多变性,而抽象是稳定的,回想接口和它的实现类,接口的方法名一般不变并且是没方法体,而方法实现交给子类去实现,子类来实现的方法有时可能会改动方法内部的细节。只要一改动,就很有可能出现bug,此时如果使用在另一个类引用的是具体类,那么那个类也可能会不能使用,这其实是高耦合的表现。
想想,其实也是为了方便扩展,如果一个方法依赖于具体类,那么该方法也就是写“死”了,来看下面的例子:
/**
* 代码这样写没问题!
* 但是,想想篮子只允许放苹果吗,还可以放别的吧?而且不止水果,手机,衣服都可以放对不对?
* 此时解决的思路是什么?可能会想到重载方法,当然没问题,如果此时有很多东西可以放在里面,那么就得去写很多重载方法,有1000个物品就得写1000个方法(别拿可以copy的思想来解决)
* 未来如果有新增还得去写类和添方法。
* 通过依赖倒转原则,可以想到使用抽象类或者接口来当引用,如果未来物品多了,也不怕,只要实现类就可以了。
*/
public class ATest {
public static void main(String[] args) {
Basket basket = new Basket();
basket.inside(new Apple());
}
}
class Apple {
public String get() {
return "苹果";
}
}
class Basket {
public void inside(Apple apple) {
System.out.println(apple.get());
}
}
使用依赖倒转原则:
/**
* 现在来看,使用接口比去写很多重载方法更加方便,也达到快速开发,代码也不臃肿
*/
public class BTest {
public static void main(String[] args) {
Basket basket = new Basket();
basket.inside(new Apple());
basket.inside(new Clothes());
}
}
interface Goods {
String get();
}
class Apple implements Goods {
@Override
public String get() {
return "苹果";
}
}
class Clothes implements Goods {
@Override
public String get() {
return "衣服";
}
}
class Basket {
// 这里的引用改成接口
public void inside(Goods goods) {
System.out.println(goods.get());
}
}
3.4 里氏替换原则
谈谈继承:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏。这就是继承的弊端,耦合度也高,因为父类需要修改时,必须考虑到所有子类,而且父类修改后,子类的功能也有可能出错。
所以想要正确的使用继承,得使用里氏替换原则。
里氏替换原则:指出尽量不要修改父类中的方法,换句话说:子类可以在父类的基础上扩展功能,但是子类尽量别去修改父类原先写好的功能。如果是抽象类,那么抽象方法就应该由子类实现,而非抽象方法在子类尽量别去修改。
如果迫不得已要从子类去修改父类的方法,那么提出几种解决:可以先让它们去继承同一个基类,原先它们间的继承关系去掉,然后使用聚合、组合、依赖来代替。
比如:
public class ATest {
public static void main(String[] args) {
B b = new B();
System.out.println("11-3=" + b.func1(11, 3));
}
}
class A {
public int func1(int a, int b) {
return a - b;
}
}
class B extends A {
// 不小心重写了类B,继承中的重写可以没有@Override
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return a * b;
}
}
使用里氏替换原则,这里可以让类A和类B去继承同一个基类,然后通过组合的关系让类B去使用类A的方法:
public class ATest {
public static void main(String[] args) {
B b = new B();
System.out.println("11-3=" + b.func1(11, 3));
}
}
class Base {
// 省略其他方法和属性
}
class A extends Base{
public int func1(int a, int b) {
return a - b;
}
}
// 使用了里氏替换原则
class B extends Base {
// 使用组合
A a = new A();
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return a * b;
}
public int func3(int a, int b) {
return this.a.func1(a, b);
}
}
3.5 开闭原则
开闭原则:一个类的函数或模块应该对扩展开放(对提供方来说),对修改关闭(对使用方来说)。用抽象构建框架,用具体实现扩展细节。
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化,因为修改就有可能影响到其他有关联的类。
使用设计模式的目的就是为了遵循开闭原则,开闭原则是编程中最基础的设计原则。
示例:
public class TestA {
public static void main(String[] args) {
// 省略
}
}
class Draw {
// 这里简单点,用i来表示要画什么图形,1为圆,2为矩形
public void draw(int i) {
if(i == 1) {
new Circular().get();
} else if(i == 2) {
new Rectangle().get();
}
}
}
class Circular {
public void get() {
System.out.println("画圆");
}
}
class Rectangle {
public void get() {
System.out.println("画矩形");
}
}
现在,假设新增三角形的类,那么代码得做出如下修改:
public class TestA {
public static void main(String[] args) {
// 省略
}
}
class Draw {
// 这里简单点,用i来表示要画什么图形,1为圆,2为矩形
public void draw(int i) {
// 原先代码
// if(i == 1) {
// new Circular().get();
// } else if(i == 2) {
// new Rectangle().get();
// }
// 得修改使用方的原有代码,这就违背开闭原则:
if(i == 1) {
new Circular().get();
} else if(i == 2) {
new Rectangle().get();
} else if(i == 3) {
new Triangle().get();
}
}
}
class Circular {
public void get() {
System.out.println("画圆");
}
}
class Rectangle {
public void get() {
System.out.println("画矩形");
}
}
// 此时新增新图形
class Triangle {
public void get() {
System.out.println("画三角形");
}
}
那么要如何遵守呢?其实在上面的其他原则也有一直提到,就是使用接口或者抽象类,如下:
/**
* 比较一下之前的代码,真的更加间洁
*/
public class TestB {
public static void main(String[] args) {
Triangle triangle = new Triangle();
Draw draw = new Draw();
draw.draw(triangle);
}
}
class Draw {
// 现在使用接口来做参数,因为java的多态性,以后有新增的图形类都不用去修改以下代码而直接使用
// 而且也不用去使用if..else判断
public void draw(Shape shape) {
shape.get();
}
}
interface Shape {
void get();
}
class Circular implements Shape {
public void get() {
System.out.println("画圆");
}
}
class Rectangle implements Shape {
public void get() {
System.out.println("画矩形");
}
}
// 此时新增新图形
class Triangle implements Shape{
public void get() {
System.out.println("画三角形");
}
}
3.6 迪米特原则
迪米特法则,又称最少知道原则(Demeter Principle):一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立,简称“朋友圈小点!”。想想如果许多类之间相互依赖,那么这个系统就会变成一个易碎的系统,这样的维护成本很高。
那么为了避免这样的情况,该原则提供了一些方针:就任何对象而言,在该对象的方法内,我们只能调用属于以下范围的方法:
-
该对象本身;
-
被当作方法的参数而传递进来的对象;
-
此方法所创建或实例化的任何对象(指的是返回值);
-
对象的任何组件(成员变量)。
也有另一种说法:只与直接的朋友通信。意思是每个对象都会与其他对象有耦合关系,只要两个对象间有耦合关系,那么就说这两个对象是朋友关系。耦合的方式有很多种,其中称:成员变量、方法参数、方法返回值的是直接关系,而出现在局部变量的类不是直接朋友。
/**
* 这里的代码没问题,但是违背了迪米特法则,因为在HeadquartersCompanyManager的printTotal中,出现了一个非直接关系的类:BranchCompanyPeople
*/
public class ATest {
public static void main(String[] args) {
HeadquartersCompanyManager headquartersCompanyManager = new HeadquartersCompanyManager();
BranchCompanyManager branchCompanyManager = new BranchCompanyManager();
headquartersCompanyManager.printTotal(branchCompanyManager);
}
}
class HeadquartersCompanyPeople {
String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
class BranchCompanyPeople {
String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
// 分部管理
class BranchCompanyManager {
// 录入分部信息,这里就简单使用id
public List<BranchCompanyPeople> input() {
List<BranchCompanyPeople> list = new ArrayList<>();
for(int i = 1; i <= 5; i++) {
BranchCompanyPeople people = new BranchCompanyPeople();
people.setId("分部员工id:" + i);
list.add(people);
}
return list;
}
}
// 总部管理
class HeadquartersCompanyManager {
// 录入学生信息,这里就简单使用id
public List<HeadquartersCompanyPeople> input() {
List<HeadquartersCompanyPeople> list = new ArrayList<>();
for(int i = 1; i <= 10; i++) {
HeadquartersCompanyPeople people = new HeadquartersCompanyPeople();
people.setId("总部员工id:" + i);
list.add(people);
}
return list;
}
// 打印出公司的所有员工(包括分公司), 传入分公司的对象
public void printTotal(BranchCompanyManager branchCompanyManager) {
System.out.println("----------总部----------");
List<HeadquartersCompanyPeople> headquartersCompanyPeople = this.input();
for(HeadquartersCompanyPeople people : headquartersCompanyPeople) {
System.out.println(people.getId());
}
// 分公司
System.out.println("----------分公司----------");
List<BranchCompanyPeople> branchCompanyPeople = branchCompanyManager.input();
for(BranchCompanyPeople people : branchCompanyPeople) {
System.out.println(people.getId());
}
}
}
解决的方法就是让BranchCompanyManager类去封装它的功能,让类只与它的直接朋友通信,并且对外提供一个方法调用。
// 修改以下两个类
// 分部管理
class BranchCompanyManager {
// 录入分部信息,这里就简单使用id
public List<BranchCompanyPeople> input() {
List<BranchCompanyPeople> list = new ArrayList<>();
for(int i = 1; i <= 5; i++) {
BranchCompanyPeople people = new BranchCompanyPeople();
people.setId("分部员工id:" + i);
list.add(people);
}
return list;
}
// 新增print方法
public void print() {
System.out.println("----------分公司----------");
List<BranchCompanyPeople> branchCompanyPeople = this.input();
for(BranchCompanyPeople people : branchCompanyPeople) {
System.out.println(people.getId());
}
}
}
// 总部管理
class HeadquartersCompanyManager {
// 录入学生信息,这里就简单使用id
public List<HeadquartersCompanyPeople> input() {
List<HeadquartersCompanyPeople> list = new ArrayList<>();
for(int i = 1; i <= 10; i++) {
HeadquartersCompanyPeople people = new HeadquartersCompanyPeople();
people.setId("总部员工id:" + i);
list.add(people);
}
return list;
}
// 打印出公司的所有员工(包括分公司), 传入分公司的对象
public void printTotal(BranchCompanyManager branchCompanyManager) {
System.out.println("----------总部----------");
List<HeadquartersCompanyPeople> headquartersCompanyPeople = this.input();
for(HeadquartersCompanyPeople people : headquartersCompanyPeople) {
System.out.println(people.getId());
}
// 修改这里
// 分公司
branchCompanyManager.print();
}
}
注意:只是降低耦合度,并不是说要完全没有依赖。
3.7 合成复用原则
合成复用原则:如果要让类与类间有关系(但关系并不是很密切,比如类调用另一个类的方法),尽量使用组合(引用)/聚合(setter方法),而不是使用继承。继承会让耦合度更高。
4. 总结
整个设计模式原则学习下来,会发现,设计模式跟Java中的多态密切相关,总是提倡使用接口/抽象类,多用组合,而且减少if…else的写法(因为if…else一多就很臃肿)。不过低耦合的话,也会使类会变得很多。
设计模式的核心思想:(《HeadFirst设计模式》也有提到)
-
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
-
为了交互对象之间的松耦合设计而努力。
-
针对接口编程,而不是针对实现编程。
5. 设计模式
一共有23种,但不可能种种都用。设计模式不仅可以用在项目上,也可以用在分析源码上。
单例模式:面试常考
装饰者模式:java的IO
代理模式:springboot的AOP就是用到动态代理。我不喜欢用《HeadFirst设计模式》中的实现,所以另找资源学了。
推荐资源:
《HeadFirst设计模式》:非常好的一本书,通俗易懂。
《B站尚硅谷韩顺平老师的设计模式》:老师的原则讲得很清楚,网上大多数就硬生生的给我几个原则的概念不解释为什么或者反例!!死背是不可能死背的。