java,从入土到出棺——1.代码复用(继承、实现、组合、代理)
- 1 面向对象
- 2 三大基本特征与五大基本原则
- 2.1 三大基本特征
- 2.1.1 封装
- 2.1.2 继承
- 2.1.3 多态
- 2.2 五大基本原则
- 2.2.1 单一职责原则
- 2.2.2 开放封闭原则
- 2.2.3 里氏替换原则
- 2.2.4 依赖倒置原则
- 2.2.5 接口隔离原则
- 3 代码的复用
- 3.1 继承(inheritance)
- 3.1.1 代码演示
- 3.1.2 说明
- 3.2 实现(interface)
- 3.2.1 代码演示
- 3.2.2 说明
- 3.3 组合(composition)
- 3.3.1 代码演示
- 3.3.2 说明
- 3.4 代理(proxy)
- 3.4.1 代码实现
- 3.4.2 说明
- 总结
写在前面:
其实这个系列的文章更加偏向于总结向,是对以往一些知识做个系统的总结,也是为已经有一定基础的人铺路,提供一个系统的思考。
对新人可能是很不友好的,可能有些人是刚接触,上来就是组合代理的,建议新人上网先做个学习,再把我这个对比记忆,说不定还能发现我的错误呢,说不准哦,三人行,必有我师;如果想和我讨论的,可以直接留言,也可以和我私信,我会很乐意和大家来讨论的。
同时也请大佬们不吝赐教!
1 面向对象
面向对象具体的讲解就不赘述了,只简单地列举一下面向过程和面向对象的表达方式,一下就能看懂:
面向过程 | 面向对象 |
---|---|
是一种以过程为中心的编程思想,实现功能的每一步,都是自己实现的 | 是一种以对象为中心的编程思想,通过指挥对象实现具体的功能 |
如果想看看最基础的相对于面向对象的知识,随便搜搜就好啦,我这只是相当于一个总结。
2 三大基本特征与五大基本原则
如果面试官问你:面向对象的三大特征与五大原则是什么?你如果回答“封装、继承、多态!单一职责原则、开放封闭原则、里氏替换原则、依赖倒置原则、接口隔离原则”,我建议你面试的时候戴个面具,防止被面试官啐一脸口水 ,人家问你是什么,你不能实诚地单单回答是什么,还应该做一个具体的描述,最起码让面试官知道“嗯,这小子懂”,是吧?哈哈
2.1 三大基本特征
2.1.1 封装
- 定义:是指属性私有化,根据需要提供setter和getter方法来访问属性。即隐藏具体属性和实现细节。
- 目的:增强数据安全性,不能让其他用户随意访问和修改数据,和简化编程,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员。
2.1.2 继承
- 定义:将多个相同的属性和方法提取出来,新建一个基类,子类继承基类,可以重写基类中的方法。
- 目的:实现代码地复用。
2.1.3 多态
- 定义:同一个实体同时具有多种形式。表现形式为:基类引用指向子类对象。
- 目的:增加代码的灵活度。
2.2 五大基本原则
2.2.1 单一职责原则
就一个类而言,应该仅有一个引起它变化的原因。应该只有一个职责。每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。
2.2.2 开放封闭原则
对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的。这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。
2.2.3 里氏替换原则
任何基类可以出现的地方,子类一定可以出现。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
2.2.4 依赖倒置原则
程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
2.2.5 接口隔离原则
客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。使用多个专门的接口比使用单个接口要好的多
3 代码的复用
主要列举一下怎么去复用代码,以方便做出对比,这种对比并不是要在这些方法中比较出来孰优孰劣,而是要根据他们的优缺点、特点,有目的性地配合使用,这才是关键。
对于两个类的复用,我尽量用两个类分开去写,一是比较规范,二是对稍微有些基础的人比较善意。
3.1 继承(inheritance)
3.1.1 代码演示
通过关键字extends来创建新的类来复用基类的非私有(private)的属性和方法:
基类Father:package com.summary.inheritance520;
public class Father {
public void introduce(){
System.out.println("Father is coming~~");
}
}
子类Father:
package com.summary.inheritance520;
public class Son extends Father {
public void show(){
System.out.println("son is here...");
}
}
测试类:
package com.summary.inheritance520;
import org.junit.Test;
public class DemoTest {
@Test
public void showInheritance() {
Son son = new Son();
System.out.print("基类方法:");
son.introduce();
System.out.print("子类方法:");
son.show();
}
}
结果输出:
基类方法:Father is coming~~
子类方法:son is here...
3.1.2 说明
通过extends来实现继承关系,子类继承基类,创建子类对象,可以调用所有基类的非私有方法。一般应用于对于几个重复功能的类的统一抽取,来简化代码,同时提高基类代码的复用性。
特别注意:
- 使用继承,需要考虑类与类之间是否存在is…a的关系,不能盲目使用继承;
- this:代表本类对象的引用;super:代表基类存储空间的标识(可以理解为基类对象引用);
- 子类初始化之前,一定要先完成基类数据的初始化;
- 当子类需要基类的功能,而功能主体子类有自己特有内容时,可以重写基类中的方法,这样,即沿袭了基类的功能,又定义了子类特有的内容;
- 当我们认为某个类,不应该被继承,就可以使用final关键字来修饰;
- 权限修饰符:
- 类中如果有抽象方法,该类必须定义为抽象类(抽象类就不赘述了,有兴趣的童鞋可以自己查一下)。
3.2 实现(interface)
3.2.1 代码演示
接口Inter:package com.summary.interface520;
public interface Inter {
int NUM = 10;
void show();
}
实现类InterImpl:
package com.summary.interface520;
class InterImpl implements Inter{
public void method(){
System.out.println("NUM:" + NUM + "是接口中定义的成员变量,只能为常量");
}
@Override
public void show() {
System.out.println("我是重写之后的实现类");
}
}
测试类:
package com.summary.interface520;
import org.junit.Test;
public class DemoTest {
@Test
public void showInterface() {
InterImpl demo = new InterImpl();
demo.method();
demo.show();
}
}
结果输出:
NUM:10是接口中定义的成员变量,只能为常量
我是重写之后的实现类
3.2.2 说明
通过implements来实现实现关系(实现关系是一种关系,第一个实现是动词),实现类实现了接口(非抽象类的实现类必须重写所有方法),创建实现类对象,对象可以调用所有接口的非私有方法(注意是可以)。一般应用于对代码的规范使用,通过实现多个接口,来解决单一继承的不足(单继承无法提供多种方法——这里指的多种方法是不同功能的方法,因为我们一般一个类只提供一种方法,比如一个飞机,需要“起飞”、“转向”、“降落”等多种功能,这些功能一般写在不同的类里,而不是一个,这是变成的规范),同时也提高代码的复用性。
特别注意:
- 接口中定义的成员变量只能是常量 系统会默认加入三个关键字 public static final;定义的成员方法只能是抽象方法, 系统会默认加入两个关键字 public abstract;
- 如果实现了多个接口,多个接口中存在相同的方法声明,子类就必须对该方法进行重写;
- 静态方法只能通过接口名调用,不能通过实现类名或者对象名调用;
- 默认方法 default void show3(){} 不是抽象方法,所以不强制被重写。但是可以被重写,重写的时候去掉default关键字;
- 默认方法可以调用私有的静态方法和非静态方法(私有方法——java9新特性);
- 类与接口的关系:
3.3 组合(composition)
3.3.1 代码演示
下面这段代码模仿组合的形式,写了这么几个类:Airframe机身类、Empennage尾翼类、Wing机翼类(组合了Engine引擎类)然后Plane飞机类将他们全都组合在一起(以上的类也可以继承,也可以实现接口,无所谓的,重点是飞机类将他们组合在了一起)
Airframe机身类:package com.summary.composition520;
public class Airframe {
public void function(){
System.out.println("我是Airframe,我的功能是载客");
}
}
Empennage尾翼类:
package com.summary.composition520;
public class Empennage {
public void function(){
System.out.println("我是Empennage,我的功能是转向");
}
}
Engine引擎类:
package com.summary.composition520;
public class Engine {
public void function(){
System.out.println("我是Engine,我的功能是提供动力");
}
}
Wing机翼类:
package com.summary.composition520;
public class Wing {
Engine engine = new Engine();
public void function(){
engine.function();
System.out.println("我是Wing,我的功能是滑翔和调整高度;而且我已经装载了Engine");
}
}
Plane飞机类:
package com.summary.composition520;
public class Plane {
Airframe airframe = new Airframe();
Empennage empennage = new Empennage();
Wing wing = new Wing();
void function(){
System.out.println("Plane已装载Airframe");
empennage.function();
System.out.println("Plane已装载Empennage");
wing.function();
System.out.println("Plane已装载Wing");
System.out.println("Plane已就绪,请求出库");
}
}
测试类:
package com.summary.composition520;
import org.junit.Test;
public class DemoTest {
@Test
public void showComposition() {
Plane plane = new Plane();
plane.function();
}
}
结果输出:
Plane已装载Airframe
我是Empennage,我的功能是转向
Plane已装载Empennage
我是Engine,我的功能是提供动力
我是Wing,我的功能是滑翔和调整高度;而且我已经装载了Engine
Plane已装载Wing
Plane已就绪,请求出库
3.3.2 说明
组合是一个很灵活的方式,他可以容纳很多对象,这些对象所依赖的类又可以随意去继承基类、实现接口,最终构成了一个复杂的对象,而不用破坏其中任何一个方法,而且每个类只专注于一项任务,没有破化开闭原则;也就是说其他类依旧不影响被另外的类使用,比如汽车要来继承引擎类,不需要重写整个引擎类,只需要继承引擎,然后定义自己的特有方法就可以了。
特别注意:
- 与继承的is…a不同,这里组合强调的是has…a;
- 组合是在组合类和被包含类之间的一种低耦合关系,有利于开发与维护;
- 使用组合的时候要考虑到被包含类的属性,若一般很难被改变,那就可以放心的来使用了。
3.4 代理(proxy)
这里并非着重讲述代理,而静态代理有太多的局限性,所以只采用动态代理的方法来说明问题。
3.4.1 代码实现
work接口:package com.summary.proxy520;
public interface Work {
void work();
}
work实现类:
package com.summary.proxy520;
public class WorkImpl implements Work {
@Override
public void work() {
System.out.println("我要干活了");
}
}
动态代理类:
package com.summary.proxy520;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyHandler implements InvocationHandler {
private Object object;
public Object setObject(Object object) {
this.object = object;
return Proxy.newProxyInstance(object.getClass().getClassLoader(),object.getClass().getInterfaces(),this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("我先喝口水");
Object invoke = method.invoke(this.object, args);
System.out.println("做完了,休息休息");
return invoke;
}
}
测试类:
package com.summary.proxy520;
import org.junit.Test;
public class ProxyTest {
@Test
public void showProxy() {
WorkImpl work = new WorkImpl();
ProxyHandler proxyHandler = new ProxyHandler();
Work workOne = (Work) proxyHandler.setObject(work);
workOne.work();
System.out.println("===============");
Work workTwo = (Work) proxyHandler.setObject(work);
workTwo.work();
}
}
结果输出:
我先喝口水
我要干活了
做完了,休息休息
===============
我先喝口水
我要干活了
做完了,休息休息
3.4.2 说明
代理,很容易看得出来,首先,调用者和被调用者实现了分离,降低了耦合,其次就是在保持开闭原则的前提下,增加而新的方法,也可以调用多个方法,我没有写第二个方法,所以连续调用了两次Work,大家应该可以理解本质的意思的。
特别注意:
- 代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能用动态代理;
- 代理的模式和组合有点类似,但又有区别:代理使得已有类的方法不直接暴露在新类面前。
总结
这里不再将封装拿进来进行说明,因为封装思想是通过构建一个类来进行属性的集成,而其他的方法是类(或者接口)与类(或者接口)之间的关系。
好处 | 弊端 | |
---|---|---|
继承 | ①提高了代码的复用性 ②提高了代码的维护性(相对于没有面向对象来说) |
①类的耦合性增强了 ②只能单继承(接口与接口可以多继承), 但是可以多层继承 ③代码的维护性较差(相对于其他面向对象的方式) |
实现 | ①可以单实现,也可以多实现 ②降低程序的耦合性 ③对外提供规则 |
不能用于实例化对象(new一个对象) |
组合 | ①整体类与局部类之间松耦合,相互独立 ②支持动态扩展(扩展性优于继承) ③相较于实现,可以实例化对象 |
①创建整体类对象时,需要创建所有局部类对象。 |
代理 | ①隔离调用者和被调用者,降低耦合 ②可以增添新的操作以限制对被调用者的使用 |
①实现代理类需要额外的工作,从而增加了系统实现复杂度 |