摘要:Java面试之三大特征、六大基本原则总结
前言:Java面试基础内容的一部分,虽然看似简单,不过如果没有对其理解清楚,在写代码的时候经常会使自己的代码漏洞百出。
四大特征:抽象、封装、继承、多态
六大基本原则:单一职责原则(SRP)、里氏替换原则(LSP)、依赖倒置原则(DIP)、接口隔离原则(ISP)、迪米特原则(LOD)、开放封闭原则(OCP)
四大特征:抽象、封装、继承、多态
抽象
抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
封装
通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。
继承
继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为 父类(超类、基类);得到继承信息的类被称为 子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段(桥梁模式)。
多态
多态性是指允许不同类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当A系统访问B系统提供的服务时,B系统有多种提供服务的方式,但一切对A系统来说都是透明的(就像电动剃须刀是A系统,它的供电系统是B系统,B系统可以使用电池供电或者用交流电,甚至还有可能是太阳能,A系统只会通过B类对象调用供电的方法,但并不知道供电系统的底层实现是什么,究竟通过何种方式获得了动力)。 方法重载(overload) 实现的是 编译时的多态性(也称为前绑定),而 方法重写(override) 实现的是 运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:1). 方法重写(子类继承父类并重写父类中已有的或抽象的方法);2). 对象造型(用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
总结:
在同一个方法中,这种由于参数类型不同而导致执行效果各异的现象就是多态。
Java实现多态有三个必要条件:继承、重写、向上转型。
- 继承:在多态中必须存在有继承关系的子类和父类。
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的,尽管是重载该方法。若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。
instanceof可以判断一个对象是否为某个类(或接口)的实例或者子类实例。
语法格式:对象(或者对象引用变量) instanceof 类(或接口)JAVA运行过程:编译,类的加载,类的执行。
多态机制遵循的原则概括为:当超类对象引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法,但是它仍然要根据继承链中方法调用的优先级来确认方法,该优先级为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
只有方法具有多态性,变量不具有多态性。
例如下面的代码所示
1 | class A{ |
六大基本原则:单一职责原则(SRP)、里氏替换原则(LSP)、依赖倒置原则(DIP)、接口隔离原则(ISP)、迪米特原则(LOD)、开放封闭原则(OCP)
单一职责原则(SRP)
单一职责原则的定义:应该有且仅有一个原因引起类的变更。
单一职责原则的好处:
类的复杂性降低
可读性提高
可维护性提高
变更引起的风险降低:变更时必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
1 | SRP的原话解释是:There should never be more than one reason for a class to change. |
因为在日常实践中“职责”和“变化原因”都是不可度量的,所以建议接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
里氏替换原则(LSP)
里氏替换原则的定义是:
- 第一种定义:
1 | If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substitued for o2 then S is a subtype of T. |
- 第二种定义:
1 | Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. |
通俗的说:只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
四层含义:
子类必须完全实现父类的方法(注意:如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系替代。),但不能覆盖父类的非抽象方法。
子类中可以增加自己特有的方法。
当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数要更宽松。
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
1 | 解释:父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说S和T是同一个类型,要么S是T的子类。 |
1 | 友情插入: |
依赖倒置原则(DIP)
依赖倒置原则的定义是:
1 | High level modules should not depend upon low level modules. Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions. |
依赖倒置原则在Java语言中的表现就是:
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
- 接口或抽象类不依赖于实现类。
- 实现类依赖接口或抽象类。
更精简的定义:“面向接口编程”。
最佳实践:
- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备。
- 变量的表面类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 尽量不要覆写基类的方法。
- 结合里氏替换原则使用。
接口隔离原则(ISP)
依赖倒置原则的定义是:
- 第一种定义:
1 | Clients should not be forced to depend upon interfaces that they don't use. |
- 第二种定义:
1 | The dependency of one class to another one should depend on the smallest possible interface. |
概括:建立单一接口,不要建立臃肿庞大的接口,即接口尽量细化,同时接口中的方法尽量少(少而精);
与单一职责的不同:单一职责要求类和接口职责单一,注重的是职责,这时业务逻辑上的划分。接口隔离原则要求接口的方法尽量简练。
四层含义:
- 接口要尽量小
- 接口要高内聚
- 定制服务
- 接口设计是有限度
最佳实践:
- 一个接口只服务于一个子模块或业务逻辑。
- 通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法。
- 已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理。
- 了解环境,拒绝盲从。每个项目或产品都有特定的环境因素,别看到大师是这样做的,你就照抄。环境不同,接口的拆分条件就不同。
迪米特原则(LOD)
迪米特原则定义:
迪米特原则也称为最少知识原则,即一个对象对其他对象有最少的了解。通俗的讲,一个类应该对自己需要耦合或调用的类知道最少。
使用原则:如果一个方法放在本类中,即不增加类间关系,也对本类不产生负面影响,那就放置在本类中。
最佳实践:
- 类间解耦,弱耦合。
- 类间跳转次数尽量不要高于2次。
开放封闭原则(OCP)
开放封闭原则定义:
1 | Software entities like classes, modules and functions should be open for extension but closed for modifications. |
变化的三种类型:
逻辑变化:只变化一个逻辑,而不涉及到其他模块,比如原有的一个算法是 a * b + c ,现在需要修改为 a * b * c, 可以通过修改原有的类中的方法的方式来完成,前提条件是所有依赖或关联类都按照相同的逻辑处理。
子模块变化:一个模块变化,会对其他的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的,刚刚的书籍打折处理就是类似的处理模块,该部分的变化甚至会引起界面的变化。
可见视图变化:可见视图是提供给客户使用的界面,如 jsp 程序,swing 界面等,该部分的变化一般会引起连锁反应。如果仅仅是界面上按钮、文字的重新排布倒是简单,最司空见惯的是业务耦合变化,什么意思呢?一个展示数据的列表,按照原有的需求是六列,突然有一天要增加一列,而且这一列要跨度 N 张表,处理 M 个逻辑才能展现出来,这样的变化是比较恐怖的,但是我们还是可以通过扩展来完成变化,这就依赖我们原有的设计是否灵活。
最佳实践:
抽象约束
- 一是通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public 方法。
- 二是在参数类型定义、输入输出参数尽量使用接口或者抽象类,而不是实现类。
- 三是抽象层尽量保持稳定。
元数据(配置参数)控制模块行为
制定项目章程
封装变化
- 将相同的变化封装到一个接口或抽象类中。
- 将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。
- 本文作者: th3ee9ine
- 本文链接: https://www.blog.ajie39.top/2021/05/05/Java面试之三大特征、六大基本原则/
- 版权声明: 本博客所有文章除特别声明外,均采用 LICENSE 下的许可协议。转载请注明出处!