基本概念
封装
通过对类的封装,仅仅暴露少许必要的方法给调用方使用,调用方不需要了解太多背后的业务细节,大大降低了用错的概率,提高了代码的易用性。
抽象
通过抽象化只暴露出方法定义,而隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。
作用:通过抽象化手段,没有暴露出来实现细节,所以当后来改变具体的实现细节时,对于调用方来说不需要或仅有少量修改,提高了代码的可扩展性和可维护性。
继承
表示类之间is-a的关系,比如猫是一种哺乳动物。
作用:最大的一个好处就是提高了代码的复用性。
多态
一个父类有多个不同的子类,而且多个子类都可以替换父类去使用。
作用:提高代码的可扩展性和复用性。
面向对象和面向过程
面向对象编程:以类为组织代码的基本单元,面向过程编程:以过程(或方法)作为组织代码的基本单元。
面向过程编程:数据和方法相分离。不支持丰富的面向对象编程特性,比如继承、多态、封装。
文章内容收录到个人网站,方便阅读:hardyfish.top/
设计原则
单一职责原则(Single Responsibility Principle)
对类来说的,即一个类应该只负责一项职责。如类A负责两个不同职责:职责1,职责2。当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为A1,A2。
接口隔离原则(Interface Segregation Principle)
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上
类 A 通过接口 Interface1 依赖(就是使用)类 B,类 C 通过接口 Interface1 依赖类 D,如果接口 Interface1 对于类 A 和类 C来说不是最小接口,那么类 B 和类 D 必须去实现他们不需要的方法。
将接口 Interface1 拆分为独立的几个接口,类 A 和类 C 分别与他们需要的接口建立依赖关系。
依赖倒转原则(Dependency Inversion Principle)
高层模块不应该依赖低层模块,二者都应该依赖其抽象(接口或者抽象类)。
抽象不应该依赖细节,细节应该依赖抽象。
依赖倒转(倒置)的中心思想是面向接口编程。
里氏替换原则(Liskov Substitution principle)
开闭原则(Open Closed Principle)
迪米特法制(Demeter Principle)
一个对象应该对其他对象保持最少的了解。
合成复用原则(Composite Reuse principle)
尽量使用合成/聚合的方式,而不是使用继承。
工厂模式
简单工厂模式
简单工厂模式指由一个工厂对象决定创建哪一种产品类型的实例。
只适用于工厂类负责创建的对象较少的场景,且客户端只需要传入工厂类的参数,对于如何创建对象不需要关心。
工厂方法模式
工厂方法定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。
当创建对象需要大量重复的代码,客户端(应用层)不依赖与产品类型实例如何被创建,实现等细节时可以考虑工厂方法。
用户只需关心所需产品对应的工厂,无须关心创建细节。加入新产品符合开闭原则,提高可扩展性。
但是工厂方法会导致类的个数过多,增加复杂度。增加系统的抽象性和理解难度。
代码体现
某平台先拥有Java和Python学习视频,将来需要拓展业务开放FE(前端)视频学习。
public abstract class Video { public abstract void produce(); }
public class JavaVideo extends Video { @Override public void produce() { System.out.println("录制Java课程视频"); } }
public class PythonVideo extends Video { @Override public void produce() { System.out.println("录制Python课程视频"); } }
public abstract class VideoFactory { public abstract Video getVideo(); }
public class JavaVideoFactory extends VideoFactory { public Video getVideo() { return new JavaVideo(); } }
public class PythonVideoFactory extends VideoFactory { public Video getVideo() { return new PythonVideo(); } }
public class Test { public static void main(String[] args) { VideoFactory videoFactory = new JavaVideoFactory(); Video video = videoFactory.getVideo(); video.produce(); System.out.println(video); } }
当拓展业务时,可以仿照
Java
与Python
。可以直接添加FEVideo
与FEVideoFactory
,可以体现出开放封闭原则。
抽象工厂模式
建造者模式
建造者模式适用于创建对象需要很多步骤,但是步骤的顺序不一定固定。如果一个对象有非常复杂的内部结构(很多属性),可以将复杂对象的创建和使用进行分离。
桥接模式
桥接模式就是把抽象和实现分离出来,然后中间通过组合来搭建他们之间的桥梁。
业务场景:中国有很多银行,有中国农业银行和中国工商银行;关于我们的账号,有定期账号和活期账号,一个就是银行一个就是我们的账号。
public interface Account { /** 打开我们的账号,打开账号,就要返回账号 */ Account openAccount(); /** 打开我们的账号,查看为什么账户类型,是定期类型还是活期类型 */ void showAccountType(); }
/** 定期的账号 */ public class DepositAccount implements Account { @Override public Account openAccount() { System.out.println("定期账号"); return new DepositAccount(); } @Override public void showAccountType() { System.out.println("这是一个定期账号"); } }
/** 活期账号 */ public class SavingAccount implements Account { @Override public Account openAccount() { System.out.println("打开活期账号"); return new SavingAccount(); } @Override public void showAccountType() { System.out.println("这是一个活期账号"); } }
public abstract class Bank { /** 只有子类能拿到这个Account的这个接口 */ protected Account account; /** 组合的时候,可以通过构造器的方式来进行注入也可以通过set方法的方式来进行注入 */ public Bank(Account account) { this.account = account; } /** 这里声明成和接口里面的方法名一致,只是方便理解,Bank里面的方法要委托给Account接口里面的方法 */ abstract Account openAccount(); }
public class ABCBank extends Bank { /** * 组合的时候,可以通过构造器的方式来进行注入也可以通过set方法的方式来进行注入 * * @param account */ public ABCBank(Account account) { super(account); } @Override Account openAccount() { System.out.println("打开中国农业银行账号"); return account; } }
public class ICBCBank extends Bank { /** * 组合的时候,可以通过构造器的方式来进行注入也可以通过set方法的方式来进行注入 * * @param account */ public ICBCBank(Account account) { super(account); } @Override Account openAccount() { System.out.println("打开中国工商银行账号"); return account; } }
public class Test { public static void main(String[]args){ Bank icbcBank = new ICBCBank(new DepositAccount()); Account icbcAccount = icbcBank.openAccount(); icbcAccount.showAccountType(); Bank icbcBank2 = new ICBCBank(new SavingAccount()); Account icbcAccount2 = icbcBank2.openAccount(); icbcAccount2.showAccountType(); Bank abcBank = new ABCBank(new SavingAccount()); Account abcAccount = abcBank.openAccount(); abcAccount.showAccountType(); } }
单例模式
单例模式就是在程序运行中只实例化一次,创建一个全局唯一对象。
饿汉模式
//在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快 public class SingletonObject { // 利用静态变量来存储唯一实例 private static final SingletonObject instance = new SingletonObject(); // 私有化构造函数 private SingletonObject(){ // 里面可能有很多操作 } // 提供公开获取实例接口 public static SingletonObject getInstance(){ return instance; } }
饿汉模式优缺点:
缺点:
- 不能实现懒加载,造成空间浪费,如果一个类比较大,我们在初始化的时就加载了这个类,但是我们长时间没有使用这个类,这就导致了内存空间的浪费。
懒汉模式
在程序初始化时不会创建实例,只有在使用实例的时候才会创建实例,所以懒汉模式解决了饿汉模式带来的空间浪费问题。
public class SingletonObject { // 定义静态变量时,未初始化实例 private static SingletonObject instance; // 私有化构造函数 private SingletonObject(){ } public static SingletonObject getInstance(){ // 使用时,先判断实例是否为空,如果实例为空,则实例化对象 // 这段代码在多线程的情况下是不安全的 if (instance == null) instance = new SingletonObject(); return instance; } }
public class SingletonObject { private static SingletonObject instance; private SingletonObject(){ } public synchronized static SingletonObject getInstance(){ /** * 添加class类锁,影响了性能,加锁之后将代码进行了串行化 * 我们的代码块绝大部分是读操作,在读操作的情况下,代码线程是安全的 */ if (instance == null) instance = new SingletonObject(); return instance; } }
懒汉模式的优缺点:
优点:
- 实现了懒加载,节约了内存空间
缺点:
- 在不加锁的情况下,线程不安全,可能出现多份实例
- 在加锁的情况下,会是程序串行化,使系统有严重的性能问题
双重检查锁模式
public class SingletonObject { private static SingletonObject instance; private SingletonObject(){ } public static SingletonObject getInstance(){ // 第一次判断,如果这里为空,不进入抢锁阶段,直接返回实例 if (instance == null) synchronized (SingletonObject.class){ // 抢到锁之后再次判断是否为空 if (instance == null){ instance = new SingletonObject(); } } return instance; } }
在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
private SingletonObject(){ 1 int x = 10; 2 int y = 30; 3 Object o = new Object(); }
我们编写的顺序是1、2、3,JVM 会对它进行指令重排序,所以执行顺序可能是3、1、2,也可能是2、3、1,不管是那种执行顺序,JVM 最后都会保证所以实例都完成实例化。
如果构造函数中操作比较多时,为了提升效率,JVM 会在构造函数里面的属性未全部完成实例化时,就返回对象。
双重检测锁出现空指针问题的原因就是出现在这里,当某个线程获取锁进行实例化时,其他线程就直接获取实例使用,由于JVM指令重排序的原因,其他线程获取的对象也许不是一个完整的对象,所以在使用实例的时候就会出现空指针异常问题。
要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile
关键字。
// 添加volatile关键字 private static volatile SingletonObject instance; private SingletonObject(){ } public static SingletonObject getInstance(){ if (instance == null) synchronized (SingletonObject.class){ if (instance == null){ instance = new SingletonObject(); } } return instance; } }
静态内部类单例模式
静态内部类单例模式实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。
静态属性由static
修饰,保证只被实例化一次,并且严格保证实例化顺序。
public class SingletonObject { private SingletonObject(){ } // 单例持有者 private static class InstanceHolder{ private final static SingletonObject instance = new SingletonObject(); } // public static SingletonObject getInstance(){ // 调用内部类属性 return InstanceHolder.instance; } }
枚举类单例模式
枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
public class SingletonObject { private SingletonObject(){ } /** * 枚举类型是线程安全的,并且只会装载一次 */ private enum Singleton{ INSTANCE; private final SingletonObject instance; Singleton(){ instance = new SingletonObject(); } private SingletonObject getInstance(){ return instance; } } public static SingletonObject getInstance(){ return Singleton.INSTANCE.getInstance(); } }
破坏单例模式的方法及解决办法
除枚举方式外,其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例。
private SingletonObject(){ if (instance !=null){ throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取"); } }
如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。
public Object readResolve() throws ObjectStreamException { return instance; }
策略模式
适配器模式
适配器模式它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。
适配器模式有两种实现方式:类适配器和对象适配器。
其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。
具体的代码实现如下所示。其中,ITarget 表示要转化成的接口定义。Adaptee 是一组不兼容 ITarget 接口定义的接口,Adaptor 将 Adaptee 转化成一组符合 ITarget 接口定义的接口。
// 类适配器: 基于继承 public interface ITarget { void f1(); void f2(); void fc(); } public class Adaptee { public void fa() { //... } public void fb() { //... } public void fc() { //... } } public class Adaptor extends Adaptee implements ITarget { public void f1() { super.fa(); } public void f2() { //...重新实现f2()... } // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点 } // 对象适配器:基于组合 public interface ITarget { void f1(); void f2(); void fc(); } public class Adaptee { public void fa() { //... } public void fb() { //... } public void fc() { //... } } public class Adaptor implements ITarget { private Adaptee adaptee; public Adaptor(Adaptee adaptee) { this.adaptee = adaptee; } public void f1() { adaptee.fa(); //委托给Adaptee } public void f2() { //...重新实现f2()... } public void fc() { adaptee.fc(); } }
到底该如何选择使用哪一种?
如果 Adaptee 接口并不多,那两种实现方式都可以。
如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那我们推荐使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。
如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那我们推荐使用对象适配器,因为组合结构相对于继承更加灵活。
应用场景
封装有缺陷的接口设计:
假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。
为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。
统一多个类的接口设计:
假设我们的系统要对用户输入的文本内容做敏感词过滤,为了提高过滤的召回率,我们引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。
但是,每个系统提供的过滤接口都是不同的。这就意味着我们没法复用一套逻辑来调用各个系统。这个时候,我们就可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样我们可以复用调用敏感词过滤的代码。
代理模式
代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
静态代理
在编译时就已经实现,编译完成后代理类是一个实际的class文件。
使用方式
创建一个接口,然后创建被代理的类实现该接口并且实现该接口中的抽象方法。
之后再创建一个代理类,同时使其也实现这个接口。在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。
public interface UserDao { void save(); }
public class UserDaoImpl implements UserDao { @Override public void save() { System.out.println("正在保存用户..."); } }
public class TransactionHandler implements UserDao { //目标代理对象 private UserDao target; //构造代理对象时传入目标对象 public TransactionHandler(UserDao target) { this.target = target; } @Override public void save() { //调用目标方法前的处理 System.out.println("开启事务控制..."); //调用目标对象的方法 target.save(); //调用目标方法后的处理 System.out.println("关闭事务控制..."); } }
public class Main { public static void main(String[] args) { //新建目标对象 UserDaoImpl target = new UserDaoImpl(); //创建代理对象, 并使用接口对其进行引用 UserDao userDao = new TransactionHandler(target); //针对接口进行调用 userDao.save(); } }
使用JDK静态代理很容易就完成了对一个类的代理操作。但是JDK
静态代理的缺点也暴露了出来:由于代理只能为一个类服务,如果需要代理的类很多,那么就需要编写大量的代理类,比较繁琐。
动态代理
JDK动态代理
使用JDK动态代理的五大步骤:
- 通过实现InvocationHandler接口来自定义自己的InvocationHandler;
- 通过
Proxy.getProxyClass
获得动态代理类;- 通过反射机制获得代理类的构造方法,方法签名为
getConstructor(InvocationHandler.class)
;- 通过构造函数获得代理对象并将自定义的
InvocationHandler
实例对象传为参数传入;- 通过代理对象调用目标方法;
public interface IHello { void sayHello(); }
public class HelloImpl implements IHello { @Override public void sayHello() { System.out.println("Hello world!"); } }
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class MyInvocationHandler implements InvocationHandler { /** 目标对象 */ private Object target; public MyInvocationHandler(Object target){ this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("------插入前置通知代码-------------"); // 执行相应的目标方法 Object rs = method.invoke(target,args); System.out.println("------插入后置处理代码-------------"); return rs; } }
import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; public class MyProxyTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { // =========================第一种========================== // 1、生成$Proxy0的class文件 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); // 2、获取动态代理类 Class proxyClazz = Proxy.getProxyClass(IHello.class.getClassLoader(),IHello.class); // 3、获得代理类的构造函数,并传入参数类型InvocationHandler.class Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class); // 4、通过构造函数来创建动态代理对象,将自定义的InvocationHandler实例传入 IHello iHello1 = (IHello) constructor.newInstance(new MyInvocationHandler(new HelloImpl())); // 5、通过代理对象调用目标方法 iHello1.sayHello(); // ==========================第二种============================= /** * Proxy类中还有个将2~4步骤封装好的简便方法来创建动态代理对象, *其方法签名为:newProxyInstance(ClassLoader loader,Class<?>[] instance, InvocationHandler h) */ IHello iHello2 = (IHello) Proxy.newProxyInstance(IHello.class.getClassLoader(), // 加载接口的类加载器 new Class[]{IHello.class}, // 一组接口 new MyInvocationHandler(new HelloImpl())); // 自定义的InvocationHandler iHello2.sayHello(); } }
JDK静态代理与JDK动态代理不同之处:
在静态代理中我们需要对哪个接口和哪个被代理类创建代理类,所以我们在编译前就需要代理类实现与被代理类相同的接口,并且直接在实现的方法中调用被代理类相应的方法;
但是动态代理我们不知道要针对哪个接口、哪个被代理类创建代理类,因为它是在运行时被创建的。
CGLIB
CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM
,来转换字节码并生成新的类。
CGLIB代理实现如下:
- 首先实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。
- 然后在需要使用的时候,通过CGLIB动态代理获取代理对象。
public class HelloService { public HelloService() { System.out.println("HelloService构造"); } /** * 该方法不能被子类覆盖,Cglib是无法代理final修饰的方法的 */ final public String sayOthers(String name) { System.out.println("HelloService:sayOthers>>"+name); return null; } public void sayHello() { System.out.println("HelloService:sayHello"); } }
import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * 自定义MethodInterceptor */ public class MyMethodInterceptor implements MethodInterceptor{ /** * sub:cglib生成的代理对象 * method:被代理对象方法 * objects:方法入参 * methodProxy: 代理方法 */ @Override public Object intercept(Object sub, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("======插入前置通知======"); Object object = methodProxy.invokeSuper(sub, objects); System.out.println("======插入后者通知======"); return object; } }
import net.sf.cglib.core.DebuggingClassWriter; import net.sf.cglib.proxy.Enhancer; public class Client { public static void main(String[] args) { // 代理类class文件存入本地磁盘方便我们反编译查看源码 System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\code"); // 通过CGLIB动态代理获取代理对象的过程 Enhancer enhancer = new Enhancer(); // 设置enhancer对象的父类 enhancer.setSuperclass(HelloService.class); // 设置enhancer的回调对象 enhancer.setCallback(new MyMethodInterceptor()); // 创建代理对象 HelloService proxy= (HelloService)enhancer.create(); // 通过代理对象调用目标方法 proxy.sayHello(); } }
JDK代理要求被代理的类必须实现接口,有很强的局限性。
CGLIB
会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。
CGLIB在进行代理的时候都进行了哪些工作
生成的代理类继承被代理类。在这里我们需要注意一点:如果委托类被final修饰,那么它不可被继承,即不可被代理;同样,如果委托类中存在final修饰的方法,那么该方法也不可被代理
代理类会为委托方法生成两个方法,一个是与委托方法签名相同的方法,它在方法中会通过
super
调用委托方法;另一个是代理类独有的方法当执行代理对象的方法时,会首先判断一下是否存在实现了
MethodInterceptor
接口的CGLIB$CALLBACK_0
,如果存在,则将调用MethodInterceptor
中的intercept
方法在
intercept
方法中,我们除了会调用委托方法,还会进行一些增强操作。在Spring AOP中,典型的应用场景就是在某些敏感方法执行前后进行操作日志记录
在CGLIB中,方法的调用并不是通过反射来完成的,而是直接对方法进行调用:
通过FastClass机制对Class对象进行特别的处理,比如将会用数组保存method的引用,每次调用方法的时候都是通过一个index下标来保持对方法的引用
Fastclass机制
CGLIB采用了FastClass的机制来实现对被拦截方法的调用。
FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法。
三种代理方式之间对比
代理方式 | 实现 | 优点 | 缺点 | 特点 |
JDK静态代理 | 代理类与委托类实现同一接口,并且在代理类中需要硬编码接口 | 实现简单,容易理解 | 代理类需要硬编码接口,在实际应用中可能会导致重复编码,浪费存储空间并且效率很低 | 好像没啥特点 |
JDK动态代理 | 代理类与委托类实现同一接口,主要是通过代理类实现InvocationHandler并重写invoke 方法来进行动态代理的,在invoke方法中将对方法进行增强处理 |
不需要硬编码接口,代码复用率高 | 只能够代理实现了接口的委托类 | 底层使用反射机制进行方法的调用 |
CGLIB动态代理 | 代理类将委托类作为自己的父类并为其中的非final委托方法创建两个方法,一个是与委托方法签名相同的方法,它在方法中会通过super 调用委托方法;另一个是代理类独有的方法。在代理方法中,它会判断是否存在实现了MethodInterceptor 接口的对象,若存在则将调用intercept方法对委托方法进行代理 |
可以在运行时对类或者是接口进行增强操作,且委托类无需实现接口 | 不能对final 类以及final方法进行代理 |
底层将方法全部存入一个数组中,通过数组索引直接进行方法调用 |
CGlib比JDK快?
- 使用CGLiB实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类, 在jdk6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的方法进行代理, 因为CGLib原理是动态生成被代理类的子类。
- 在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率。只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理,总之,每一次jdk版本升级,jdk代理效率都得到提升。
Spring如何选择用JDK还是CGLIB?
- 当Bean实现接口时,Spring就会用JDK的动态代理。
- 当Bean没有实现接口时,Spring使用CGlib实现。
- 可以强制使用CGlib。