lukaliou123 / lukaliou123.github.io

lukaliou123在2022年的面试用知识点总结
Other
5 stars 0 forks source link

设计模式篇 #12

Open lukaliou123 opened 2 years ago

lukaliou123 commented 2 years ago

1.什么是设计模式

设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

2.设计模式六大原则(SOLID+迪米特)

image 1.单一职责原则(single responsibility) 原则思想:一个方法只负责一件事情。 描述:单一职责原则很简单,一个方法 一个类只负责一个职责,各个职责的程序改动,不影响其它程序。 优点:降低类和类的耦合,提高可读性,增加可维护性和可拓展性,降低可变性的风险。

2.开放封闭原则(Open Close Principle) 原则思想:尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化。 描述:个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性。

3.里氏代换原则(Liskov Substitution Principle) 原则思想:使用的基类可以在任何地方使用继承的子类,完美的替换基类。 大概意思是:子类可以扩展父类的功能,但不能改变父类原有的功能。子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法,子类中可以增加自己特有的方法。

4.接口隔离原则(Interface Segregation Principle) 原理:这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。

5.依赖倒转原则(Dependence Inversion Principle) 定义:高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。 java中抽象指接口或抽象类,两者都不能直接被实例化的;细节就是实现类,实现接口或者集成抽象类而产生的也就细节,也就是可以可以加上一个 关键字new产生的对象。高层模块就是调用端,低层模块就是具体实现类。 依赖倒置原则在java中表现就是,模块间依赖通过抽象发生,实现类之间不发生直接依赖关系,其依赖关系是通过接口或者抽象类产生的。如果类与类直接依赖细节,那么久会直接耦合。如此一来当修改时,就会同时修改依赖者代码,这样限制了可拓展性。

6.迪米特法则(最少知道原则)(Demeter Principle) 也叫最少知道原则,每个模块对其他模块都要尽可能少地了解和依赖,降低代码耦合度。

lukaliou123 commented 2 years ago

3.单例模式

单例模式属于创建型模式,一个单例类在任何情况下都只存在一个实例,构造方法必须是私有的、由自己创建一个静态变量存储实例,对外提供一个静态公有方法获取实例。 优点是内存中只有一个实例,减少了开销,尤其是频繁创建和销毁实例的情况下并且可以避免对资源的多重占用。缺点是没有抽象层,难以扩展,与单一职责原则冲突

3.1.单例模式的集中实现

1.饿汉式:在类加载时就初始化创建单例对象,线程安全,但不管是否使用都创建对象可能会浪费内存。 image

2.懒汉式:在外部调用时才会加载,线程不安全,可以加锁保证线程安全但效率低。 image

加了锁的懒汉 image

双重检查锁:使用 volatile 以及多重检查来减小锁范围,提升效率。 image uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton();

这段代码其实是分为三步执行: 1.为 uniqueInstance 分配内存空间 2.初始化 uniqueInstance 3.将 uniqueInstance 指向分配的内存地址 但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

静态内部类:同时解决饿汉式的内存浪费问题和懒汉式的线程安全问题。 image

补充:为什么静态内部类是安全的:

当使用静态内部类实现单例模式时,单例对象的实例化被延迟到静态内部类被加载时。这是由Java的类加载机制保证的,静态内部类只会在首次使用时被加载,且加载过程由类加载器进行加锁,保证了线程安全

Java类加载机制是基于类加载器(ClassLoader)的工作原理。当需要加载一个类时,类加载器会先检查该类是否已经加载,如果没有加载,则会触发类的加载过程。在加载过程中,类加载器会执行一系列步骤,包括查找类的字节码文件、验证字节码的正确性、分配内存空间、执行静态初始化等。

类加载器在执行这些步骤时会进行加锁操作,确保只有一个线程进行类加载。这就保证了类的加载过程是线程安全的,不会出现并发访问的问题。

在类加载完成后,生成的类对象会被存放在方法区(Metaspace)中,并被所有线程共享。由于类对象是只读的,不会被修改,因此在多线程环境下并发访问类对象是安全的。

需要注意的是,类加载器的锁粒度是类级别的,而不是实例级别的。也就是说,当加载一个类时,只有该类对应的类加载器进行加锁,不会影响其他类的加载。这使得多个线程可以并发地加载不同的类,提高了加载的效率。

因此,由于类加载器的加锁机制和类对象的只读特性,类加载是线程安全的,保证了多线程环境下的正确加载和共享访问。

最好的单例模式实现法:枚举实现

1689700469342 线程安全:Java在每个枚举类型的第一次使用时,会自动创建其定义的枚举对象,而且仅创建一次,由JVM保证。

防止反射攻击:在其它实现单例的方法中,通过setAccessible方法,可以将私有构造函数的访问级别设置为public,然后调用构造函数进行实例化。但是枚举类型没有这个问题,因为它没有构造函数(至少对你来说是这样的)。

自带序列化机制:防止反序列化重新创建新的对象。

lukaliou123 commented 2 years ago

4.工厂模式

4.1.简单工厂模式

简单工厂模式指由一个工厂对象来创建实例,客户端不需要关注创建逻辑,只需提供传入工厂的参数。 适用于工厂类负责创建对象较少的情况,缺点是如果要增加新产品,就需要修改工厂类的判断逻辑,违背开闭原则,且产品多的话会使工厂类比较复杂。 Calendar 抽象类的 getInstance 方法,调用 createCalendar 方法根据不同的地区参数创建不同的日历对象。 image

4.2. 工厂方法模式

工厂方法模式指定义一个创建对象的接口,让接口的实现类决定创建哪种对象,让类的实例化推迟到子类中进行。 客户端只需关心对应工厂而无需关心创建细节,主要解决了产品扩展的问题,在简单工厂模式中如果产品种类变多,工厂的职责会越来越多,不便于维护。 例子:Collection 接口这个抽象工厂中定义了一个抽象的 iterator 工厂方法,返回一个 Iterator 类的抽象产品。该方法通过 ArrayList 、HashMap 等具体工厂实现,返回 Itr、KeyIterator 等具体产品。 image

4.3. 抽象工厂模式

抽象工厂模式指提供一个创建一系列相关或相互依赖对象的接口,无需指定它们的具体类。 客户端不依赖于产品类实例如何被创建和实现的细节,主要用于系统的产品有多于一个的产品族,而系统只消费其中某一个产品族产品的情况。抽象工厂模式的缺点是不方便扩展产品族,并且增加了系统的抽象性和理解难度image

lukaliou123 commented 2 years ago

5.代理模式

简单来说就是,我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。 代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。 image 代理模式有静态代理和动态代理两种实现方式,我们 先来看一下静态代理模式的实现。

5.1.静态代理

静态代理中,我们对目标对象的每个方法的增强都是手动完成的(后面会具体演示代码)非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。

上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件

静态代理实现步骤:

  1. 定义一个接口及其实现类;
  2. 创建一个代理类同样实现这个接口
  3. 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情

5.2.动态代理

相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。 从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

动态代理在我们日常开发中使用的相对较少,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助

就 Java 来说,动态代理的实现方式有很多种,比如 JDK 动态代理、CGLIB 动态代理等等

1.JDK动态代理机制 通过 Proxy 类的 newInstance 方法获取一个动态代理对象,需要传入三个参数, 被代理对象的类加载器、被代理对象实现的接口,及一个 InvocationHandler 调用处理器来指明具体的逻辑, image 相比静态代理的优势是接口中声明的所有方法都被转移到 InvocationHandler 的 invoke 方法集中处理。 image 也就是说:你通过Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler 接口的类的 invoke()方法。 你可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。

过程参考: https://javaguide.cn/java/basis/proxy/#_3-1-jdk-%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E6%9C%BA%E5%88%B6

2.CGLib 动态代理:

JDK 动态代理要求实现被代理对象的接口,而 CGLib 要求继承被代理对象,如果一个类是 final 类则不能使用 CGLib 代理。两种代理都在运行期生成字节码,JDK 动态代理直接写字节码,而 CGLib 动态代理使用 ASM 框架写字节码,ASM 的目的是生成、转换和分析以字节数组表示的已编译 Java 类。JDK 动态代理调用代理方法通过反射机制实现,而 GCLib 动态代理通过 FastClass 机制直接调用方法,它为代理类和被代理类各生成一个类,该类为代理类和被代理类的方法分配一个 int 参数,调用方法时可以直接定位,因此调用效率更高。 image

cglib与动态代理最大的区别就是

使用动态代理的对象必须实现一个或多个接口 使用cglib代理的对象则无需实现接口,达到代理类无侵入

动态代理必须实现InvocationHandler接口,通过反射代理方法,比较消耗系统性能,但可以减少代理类的数量,使用更灵活

cglib代理无需实现接口,通过生成类字节码实现代理,比反射稍快,不存在性能问题,但cglib会继承目标对象,需要重写方法,所以目标对象不能为final类。

lukaliou123 commented 2 years ago

6.策略模式

策略模式属于行为型模式,定义了一系列算法并封装起来,之间可以互相替换。策略模式主要解决在有多种算法相似的情况下,使用 if/else 所带来的难以维护。 应用场景:策略模式的用意是针对一组算法或逻辑,将每一个算法或逻辑封装到具有共同接口的独立的类中,从而使得它们之间可以相互替换。

例子:我要做一个不同会员打折力度不同的三种策略,初级会员,中级会员,高级会员(三种不同的计算)。 优点:1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性非常良好。

缺点:1、策略类会增多。 2、所有策略类都需要对外暴露。 image

lukaliou123 commented 2 years ago

7.更容易懂的代理模式讲解

代理模式在 Java 开发中是一种比较常见的设计模式。设计目的旨在为服务类与客户类之间插入其他功能,插入的功能对于调用者是透明的,起到伪装控制的作用。如租房的例子:房客、中介、房东。对应于代理模式中即:客户类、代理类 、委托类(被代理类)

某一个对象(委托类)提供一个代理(代理类),用来控制对这个对象的访问。委托类和代理类有一个共同的父类或父接口。代理类会对请求做预处理、过滤,将请求分配给指定对象

生活中常见的代理情况: 租房中介、婚庆公司

代理模式的两个设计原则

1.代理类与委托类具有相似的行为(共同) 2.代理类增强委托类的行为 image

代理的三要素

有共同的行为(结婚) - 接口 目标角色(新人) - 实现行为 代理角色(婚庆公司) - 实现行为 增强目标对象行为

JDK代理和CGLIB代理的区别

JDK 的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能使用 JDK 的动态代理,cglib 是针对类来实现代理的,它的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对 final 修饰的类进行代理

lukaliou123 commented 1 year ago

8.备忘录模式

忘录模式是一种行为设计模式,主要用来在不破坏对象的封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样就可以将该对象恢复到原先保存的状态。它的主要角色有三种:Originator(原发器)、Caretaker(看管人)和Memento(备忘录)

实际应用例子:在计算机游戏中,备忘录模式常常被用来实现游戏进度的保存和加载。原发器就是游戏角色,备忘录是角色的某个状态看管人则是保存和加载进度的系统。当玩家选择保存游戏时,系统会让角色创建一个包含当前状态(比如位置、生命值等)的备忘录,并保存下来;当玩家选择加载进度时,系统则会将角色恢复到备忘录所保存的状态。

实现例子 image 1690350533929 这个例子模拟了一个简单的游戏进度保存和加载的过程。在这个例子中,Game 类就是原发器,它有一个状态(游戏进度)和两个操作(保存进度和加载进度)。Memento 类就是备忘录,它保存了原发器的状态。GameSaver 类就是看管人,它控制了保存和加载的过程。

lukaliou123 commented 1 year ago

9.观察者模式

观察者模式是一种行为设计模式,定义了对象之间的依赖关系,使得当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。观察者模式的主要角色有两种:Subject(主题或被观察者)和Observer(观察者)

实际应用例子:在微博上,用户可以关注别人,被关注的人发布微博后,关注他的人会收到新微博的通知。在这个例子中,被关注的人就是Subject,关注他的用户就是Observer。当Subject的状态(发布了新微博)改变时,所有的Observer(关注他的用户)都会收到通知(新微博)。

实际例子 观察者模式常被用于实现事件处理系统。例如,我们有一个天气数据站点,当气象观测数据发生改变时,我们希望把这些改变实时的更新给公告板进行显示。

首先定义一个Subject接口,和一个Observer接口1690350792531 然后我们创建WeatherData类,实现Subject接口NCV8ZGHQV9V1{(Z3KB9S4N5 1690350869891 然后,创建CurrentConditionsDisplay类,实现Observer接口1690350996531 在这个例子中,WeatherData就是主题,它在气象数据发生改变时会通知所有注册的观察者。CurrentConditionsDisplay就是观察者,它会显示最新的气象数据。当你希望增加新的公告板时,只需要再实现一个Observer接口的类,并在WeatherData中注册即可。