相信很多同行小伙伴会因为许多原因想跳槽,不论是干得不开心还是想跳槽涨薪,在如此内卷的行业,我们都面临着“面试造火箭,上班拧螺丝”的局面,鉴于当前形势博主呕心沥血整理的干货满满的造火箭的技巧来了,本博主花费2个月时间,整理归纳java全生态知识体系常见面试题!总字数高达百万! 干货满满,每天更新,关注我,不迷路,用强大的归纳总结,全新全细致的讲解来留住各位猿友的关注,希望能够帮助各位猿友在应付面试笔试上!当然如有归纳总结错误之处请各位指出修正!如有侵权请联系博主QQ1062141499!
目录
3 如何使用双重检查锁定在 Java 中创建线程安全的单例?
8 什么是单例模式?有什么作用和特点?可以解决哪些问题?懒汉式和饿汉式的区别?如何保证线程安全?
1 列出常见几种设计模式并简要概述
设计模式主要分三个类型:创建型、结构型和行为型。
其中创建型有:
一、Singleton,单例模式:保证一个类只有一个实例,并提供一个访问它的全局访问点
二、Abstract Factory,抽象工厂:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们的具体类。
三、Factory Method,工厂方法:定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method使一个类的实例化延迟到了子类。
四、Builder,建造模式:将一个复杂对象的构建与他的表示相分离,使得同样的构建过程可以创建不同的表示。
五、Prototype,原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型来创建新的对象。
行为型有:
六、Iterator,迭代器模式:提供一个方法顺序访问一个聚合对象的各个元素,而又不需要暴露该对象的内部表示。
七、Observer,观察者模式:定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知自动更新。
八、Template Method,模板方法:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,TemplateMethod使得子类可以不改变一个算法的结构即可以重定义该算法得某些特定步骤。
九、Command,命令模式:将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队和记录请求日志,以及支持可撤销的操作。
十、State,状态模式:允许对象在其内部状态改变时改变他的行为。对象看起来似乎改变了他的类。
十一、Strategy,策略模式:定义一系列的算法,把他们一个个封装起来,并使他们可以互相替换,本模式使得算法可以独立于使用它们的客户。
十二、China of Responsibility,职责链模式:使多个对象都有机会处理请求,从而避免请求的送发者和接收者之间的耦合关系
十三、Mediator,中介者模式:用一个中介对象封装一些列的对象交互。
十四、Visitor,访问者模式:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这个元素的新操作。
十五、Interpreter,解释器模式:给定一个语言,定义他的文法的一个表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
十六、Memento,备忘录模式:在不破坏对象的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
结构型有:
十七、Composite,组合模式:将对象组合成树形结构以表示部分整体的关系,Composite使得用户对单个对象和组合对象的使用具有一致性。
十八、Facade,外观模式:为子系统中的一组接口提供一致的界面,fa?ade提供了一高层接口,这个接口使得子系统更容易使用。
十九、Proxy,代理模式:为其他对象提供一种代理以控制对这个对象的访问
二十、Adapter,适配器模式:将一类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作那些类可以一起工作。
二十一、Decrator,装饰模式:动态地给一个对象增加一些额外的职责,就增加的功能来说,Decorator模式相比生成子类更加灵活。
二十二、Bridge,桥模式:将抽象部分与它的实现部分相分离,使他们可以独立的变化。
二十三、Flyweight,享元模式
2 设计模式的六大原则
- 开闭原则:实现热插拔,提高扩展性。
- 里氏代换原则:实现抽象的规范,实现子父类互相替换;
- 依赖倒转原则:针对接口编程,实现开闭原则的基础;
- 接口隔离原则:降低耦合度,接口单独设计,互相隔离;
- 迪米特法则,又称不知道原则:功能模块尽量独立;
- 合成复用原则:尽量使用聚合,组合,而不是继承;
1、开闭原则(Open Close Principle)
开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。
2、里氏代换原则(Liskov Substitution Principle)
里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
3、依赖倒转原则(Dependence Inversion Principle)
这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。
4、接口隔离原则(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。
5、迪米特法则,又称最少知道原则(Demeter Principle)
最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
6、合成复用原则(Composite Reuse Principle)
合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。
3 如何使用双重检查锁定在 Java 中创建线程安全的单例?
这个 Java 问题也常被问: 什么是线程安全的单例,你怎么创建它。好吧,在Java 5之前的版本, 使用双重检查锁定创建单例 Singleton 时,如果多个线程试图同时创建 Singleton 实例,则可能有多个 Singleton 实例被创建。从 Java 5 开始,使用 Enum 创建线程安全的Singleton很容易。但如果面试官坚持双重检查锁定,那么你必须为他们编写代码。记得使用volatile变量。
为什么枚举单例在 Java 中更好
枚举单例是使用一个实例在 Java 中实现单例模式的新方法。虽然Java中的单例模式存在很长时间,但枚举单例是相对较新的概念,在引入Enum作为关键字和功能之后,从Java5开始在实践中。本文与之前关于 Singleton 的内容有些相关, 其中讨论了有关 Singleton 模式的面试中的常见问题, 以及 10 个 Java 枚举示例, 其中我们看到了如何通用枚举可以。这篇文章是关于为什么我们应该使用Eeame作为Java中的单例,它比传统的单例方法相比有什么好处等等。
Java 枚举和单例模式
Java 中的枚举单例模式是使用枚举在 Java 中实现单例模式。单例模式在 Java 中早有应用, 但使用枚举类型创建单例模式时间却不长. 如果感兴趣, 你可以了解下构建者设计模式和装饰器设计模式。
1) 枚举单例易于书写
这是迄今为止最大的优势,如果你在Java 5之前一直在编写单例, 你知道, 即使双检查锁定, 你仍可以有多个实例。虽然这个问题通过 Java 内存模型的改进已经解决了, 从 Java 5 开始的 volatile 类型变量提供了保证, 但是对于许多初学者来说, 编写起来仍然很棘手。与同步双检查锁定相比,枚举单例实在是太简单了。如果你不相信, 那就比较一下下面的传统双检查锁定单例和枚举单例的代码:
在 Java 中使用枚举的单例
这是我们通常声明枚举的单例的方式,它可能包含实例变量和实例方法,但为了简单起见,我没有使用任何实例方法,只是要注意,如果你使用的实例方法且该方法能改变对象的状态的话, 则需要确保该方法的线程安全。默认情况下,创建枚举实例是线程安全的,但 Enum 上的任何其他方法是否线程安全都是程序员的责任
/**
* 使用 Java 枚举的单例模式示例
*/
public enum EasySingleton{
INSTANCE;
}
你可以通过EasySingleton.INSTANCE来处理它,这比在单例上调用getInstance()方法容易得多。
具有双检查锁定的单例示例
下面的代码是单例模式中双重检查锁定的示例,此处的 getInstance() 方法检查两次,以查看 INSTANCE 是否为空,这就是为什么它被称为双检查锁定模式,请记住,双检查锁定是代理之前Java 5,但Java5内存模型中易失变量的干扰,它应该工作完美。
/**
* 双检锁/双重校验锁(DCL,即 double-checked locking)
* JDK 版本:JDK1.5 起
* 是否 Lazy 初始化:是
* 是否多线程安全:是
* 实现难度:较复杂
* 描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
* getInstance() 的性能对应用程序很关键。
*/
public class DCLSingleton {
private volatile static DCLSingleton dclSingleton;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (dclSingleton == null) {
synchronized (DCLSingleton.class) {
if (dclSingleton == null) {
dclSingleton = new DCLSingleton();
}
}
}
return dclSingleton;
}
}
你可以调用DoubleCheckedLockingSingleton.getInstance() 来获取此单例类的访问权限。
现在,只需查看创建延迟加载的线程安全的 Singleton 所需的代码量。使用枚举单例模式, 你可以在一行中具有该模式, 因为创建枚举实例是线程安全的, 并且由 JVM 进行。
人们可能会争辩说,有更好的方法来编写 Singleton 而不是双检查锁定方法, 但每种方法都有自己的优点和缺点, 就像我最喜欢在类加载时创建的静态字段 Singleton, 如下面所示, 但请记住, 这不是一个延迟加载单例:
单例模式用静态工厂方法
这是我最喜欢的在 Java 中影响 Singleton 模式的方法之一,因为 Singleton 实例是静态的,并且最后一个变量在类首次加载到内存时初始化,因此实例的创建本质上是线程安全的。
/**
* 静态工厂方法单例模式
*/
public class StaticFactorySingleton {
private static final StaticFactorySingleton instance = new StaticFactorySingleton();
private StaticFactorySingleton() {}
//静态工厂方法
public static StaticFactorySingleton getInstance() {
return instance;
}
}
你可以调用 Singleton.getSingleton() 来获取此类的访问权限。
2) 枚举单例自行处理序列化
传统单例的另一个问题是,一旦实现可序列化接口,它们就不再是 Singleton, 因为 readObject() 方法总是返回一个新实例, 就像 Java 中的构造函数一样。通过使用 readResolve() 方法, 通过在以下示例中替换 Singeton 来避免这种情况:
//readResolve to prevent another instance of Singleton
private Object readResolve(){
return INSTANCE;
}
如果 Singleton 类保持内部状态, 这将变得更加复杂, 因为你需要标记为 transient(不被序列化),但使用枚举单例, 序列化由 JVM 进行。
3) 创建枚举实例是线程安全的
如第 1 点所述,因为 Enum 实例的创建在默认情况下是线程安全的, 你无需担心是否要做双重检查锁定。
总之, 在保证序列化和线程安全的情况下,使用两行代码枚举单例模式是在 Java 5 以后的世界中创建 Singleton 的最佳方式。你仍然可以使用其他流行的方法, 如你觉得更好, 欢迎讨论。
4 简单工厂和抽象工厂有什么区别?
- 简单工厂模式
是由一个工厂对象创建产品实例,简单工厂模式的工厂类一般是使用静态方法,通过不同的参数的创建不同的对象实例可以生产结构中的任意产品,不能增加新的产品
- 抽象工厂模式
提供一个创建一系列相关或相互依赖对象的接口,而无需制定他们具体的类,生产多个系列产品生产不同产品族的全部产品,不能新增产品,可以新增产品族
5 浅谈简单工厂,工厂方法,抽象工厂的区别和使用
工厂模式是分为三种,分别是简单工厂,工厂方法,抽象工厂。其中工厂方法和抽象工厂是GoF23种设计模式中的一种,而简单工厂则不是一种设计模式,更加可以理解的是一种编码时候预定俗称的一种习惯。那么,就在接下来三点中分别去分析理解工厂模式。
一 简单工厂:
通过实例化一个工厂类,来获取对应的产品实例。我们不需要关注产品本身如何被创建的细节,只需要通过相应的工厂就可以获得相应的实例。简单工厂包括三种角色:
1.工厂:简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
2.抽象产品 :简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
3.具体产品:是简单工厂模式的创建目标,所有创建的对象都是充当这个角色的某个具体类的实例。
比如以下例子:
1.Drinks作为产品的抽象类并且有抽象方法produce();(抽象产品)
public abstract class Drinks {
protected abstract void produce();
}
2.Sprite继承Drinks是要被具体生产出来的产品,他重写了produce()方法。(具体产品)
public class Sprite extends Drinks {
@Override
protected void produce() {
System.out.println("drink sprite");
}
}
3.Cola同样也继承了Drinks,是要被生产出来的具体产品。
(具体产品)
public class Cola extends Drinks {
@Override
protected void produce() {
System.out.println("Drink Cola");
}
}
4.DrinksFactory为简单工厂,向外暴露produceDrink方法来获取产品的实例(工厂)
public class DrinksFactory {
public Drinks produceDrink(Class className){
try {
return (Drinks) Class.forName(className.getName()).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
5.Client为应用层,Client端想要获取到Cola或者Sprite对象,只要通过DrinkFactory中的produceDrink方法传入相对应的对应的产品
public class Client {
public static void main(String[] args) {
DrinksFactory factory = new DrinksFactory();
Cola cola = (Cola) factory.produceDrink(Cola.class);
cola.produce();
}
}
简单工厂的优点:
1.不需要关心类的创建细节。
2.减轻类之间的耦合依赖,具体类的实现只是依赖于简单工厂,而不依赖其他类。
简单工厂的缺点:
1.扩展复杂,当简单工厂需要生产出另外一种产品的时候,需要扩展工厂的内部创建逻辑,比较有可能引起较大的故障
2.由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中
二.工厂方法
工厂方法的定义是:定义一个创建对象的接口,让实现这个接口的的类去决定实例化具体的类。工厂方法让类的实例化推迟到实现接口的子类中进行。
比如说,我现在需要一瓶可乐,有可口可乐公司生产的可乐也有百事可乐公司生产的可乐,那么对于可乐这个产品等级(抽象工厂中会具体说明),具体生产什么可乐并不是在一个工厂实现,而是由一个可乐工厂指定一个标准(接口里面的抽象方法),可口可乐公司百事可乐公司只要按照这个标准去生产就可以了。
(2)工厂方法的Uml类图
1.Cola此类是产品的父类
public abstract class Cola {
protected abstract void drinks();
}
2.PepsiCola继承Cola,是要生产的产品之一
public class PepsiCola extends Cola {
@Override
protected void drinks() {
System.out.println("Drinks PepsiCola");
}
}
3.CoCoCola同样继承Cola,也是要生产的产品之一
public class CoCoCola extends Cola {
@Override
protected void drinks() {
System.out.println("Drinks cococoLa");
}
}
4.ColaFacotry定义抽象工厂,指定要生产此类产品的规范(存在的方法与属性),指定工厂方法
public interface ColaFacotry {
Cola produce(Class<Cola> cola);
}
5.PepsiColaFactory定义子类工厂,它继承抽象工厂,实现了对某一产品等级的产品的获得
public class PepsiColaFactory implements ColaFacotry {
public PepsiCola produce(Class cola) {
if (cola.isInstance(PepsiCola.class)){
return new PepsiCola();
}
return null;
}
}
6.ColaFacotry,是规定工厂方法去获得拿一些的产品等级的商品,比如说,我规定生产的产品等级是可乐和草莓可乐,那么对于它的实现类来说,也就是其子类中的重载方法来说去具体实现获取产品的具体实现。
public interface ColaFacotry {
Cola produce(Class<Cola> cola);
}
三.抽象工厂
抽象工厂是提供了创建一系列服务的的对象的接口。那么问题就来了,怎么区分和工厂方法中的服务对象呢?此时就需要对一组概念有所理解,即产品等级和产品族,我从网上找到下面这张图,进行解释说明。在图(4)中,我们可以通过横向和纵向的比较,横向是某一个手机厂商如苹果,小米等,他们不仅仅生产手机,还生产电脑,耳机等一系类产品,那么我们把苹果,小米,华为这样的厂商可以认为他们生产的是一个产品族,而他们自己本身就是一个抽象工厂的具体实现;那么纵向来看,不管是小米华为还是苹果,他们生产的产品是按照一定的规则来生产,显示屏,电池,处理器等等,所以对于纵向的产品来说,他们又是属于同一个产品等级,我们亦可以称他们的实现为工厂方法。
综上所述,抽象工厂解决的是横向的产品族,工厂方法解决的是纵向的产品等级。具体抽象工厂请看代码。
1.AbstractFactory 抽象工厂
public interface AbstractFactory {
public Phone producePhone();
public Computer producaComputer();
}
2.AppleFactory具体工厂的实现一
public class AppleFactory implements AbstractFactory {
public Phone producePhone() {
return new Iphone();
}
public Computer producaComputer() {
return new Mac();
}
}
3.MiFactory具体工厂的实现二
public class MiFactory implements AbstractFactory {
public Phone producePhone() {
return new MiPhone();
}
public Computer producaComputer() {
return new MiComputer();
}
}
4.Phone抽象产品等级一
public abstract class Phone {
public abstract void call();
}
5.Iphone具体产品一
public class Iphone extends Phone {
@Override
public void call() {
System.out.println("Iphone call");
}
}
6.MiPhone具体产品一
public class MiPhone extends Phone {
@Override
public void call() {
System.out.println("Mi Phone call");
}
}
7.Computer抽象产品等级二
public abstract class Computer {
public abstract void work();
}
8.Mac具体产品一
public class Mac extends Computer {
@Override
public void work() {
System.out.println("MAC work");
}
}
9.MiComputer 具体产品二
public class MiComputer extends Computer {
@Override
public void work() {
System.out.println("MI computer word");
}
}
10.客户端
public class Clint {
public static void main(String[] args) {
AppleFactory appleFactory = new AppleFactory();
appleFactory.producaComputer().work();
appleFactory.producePhone().call();
MiFactory miFactory = new MiFactory();
miFactory.producaComputer().work();
miFactory.producePhone().call();
}
}
从UML类图中不难看出,我们如果需要拓展抽象工厂里面的方法会比较麻烦,因为我们必须修改抽象类以及添加对应的产品等级,这样修改量比较大,但是每种产品之间相互解耦,符合程序设计的“高内聚低耦合”的思想。
最后,不管是抽象工厂还是工厂方法甚至是简单工厂,他们的存在都有一定的优缺点,在设计程序的时候要根据具体情况进行取舍,不存在那种设计好那种设计特别差,只是针对不同的业务场景的不一样的处理。
6 三大工厂模式的优缺点
简单工厂模式,工厂方法模式和抽象工厂模式都是属于创建型设计模式,这三种创建型模式都不需要知道具体类。我们掌握一种思想,就是在创建一个对象时,需要把容易发生变化的地方给封装起来,来控制变化(哪里变化,封装哪里),以适应客户的变动,项目的扩展。用这三种设计模式都可以实现,那究竟这三种设计模式有什么异同呢?下面根据这三者之间的特点,优点,缺点,适用范围进行比较。
一.特点
简单工厂模式:专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。它又称为静态工厂方法模式。它的实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类(这些产品类继承自一个父类或接口)的实例。简单工厂模式的创建目标,所有创建的对象都是充当这个角色的某个具体类的实例。在这个模式中,工厂类是整个模式的关键所在。它包含必要的判断逻辑,能够根据外界给定的信息,决定究竟应该创建哪个具体类的对象。用户在使用时可以直接根据工厂类去创建所需的实例,而无需了解这些对象是如何创建以及如何组织的。有利于整个软件体系结构的优化。
工厂方法模式:工厂方法是粒度很小的设计模式,因为模式的表现只是一个抽象的方法。提前定义用于创建对象的接口,让子类(具体工厂)决定实例化具体的某一个类,即在工厂和产品中间增加接口(抽象工厂),工厂不再负责产品的创建,由接口针对不同条件返回具体的类实例,由具体类实例(具体工厂)去实现。工厂方法模式是简单工厂模式的衍生,解决了许多简单工厂模式的问题。首先完全实现ocp,实现了可扩展。其次实现更复杂的层次结构,可以应用于产品结果复杂的场合。工厂方法模式是对简单工厂模式进行了抽象。有一个抽象的Factory类(可以是抽象类和接口),这个类将不在负责具体的产品生产,而是只制定一些规范,具体的生产工作由其子类去完成。在这个模式中,工厂类和产品类往往可以依次对应。即一个抽象工厂对应一个抽象产品,一个具体工厂对应一个具体产品,这个具体的工厂就负责生产对应的产品。
抽象工厂模式:抽象工厂模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。它有多个抽象产品类,每个抽象产品类可以派生出多个具体产品类,一个抽象工厂类,可以派生出多个具体工厂类,每个具体工厂类可以创建多个具体产品类的实例。每一个模式都是针对一定问题的解决方案,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式针对的是多个产品等级结果。
二.优点
简单工厂模式:工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅"消费"产品。简单工厂模式通过这种做法实现了对责任的分割。简单工厂模式能够根据外界给定的信息,决定究竟应该创建哪个具体类的对象。通过它,外界可以从直接创建具体产品对象的尴尬局面中摆脱出来。外界与具体类隔离开来,偶合性低。明确区分了各自的职责和权力,有利于整个软件体系结构的优化。
工厂方法模式:工厂方法模式是为了克服简单工厂模式的缺点(主要是为了满足OCP)而设计出来的。 OCP原则(Open Close Principle),核心思想是封闭修改(隔离变化),支持扩展(继承,目的是复用)。简单工厂模式的工厂类随着产品类的增加需要增加很多方法(或代码),而工厂方法模式每个具体工厂类只完成单一任务,代码简洁。工厂方法模式完全满足OCP,即它有非常良好的扩展性。
抽象工厂模式:抽象工厂模式主要在于应对“新系列”的需求变化。分离了具体的类,抽象工厂模式帮助你控制一个应用创建的对象的类,因为一个工厂封装创建产品对象的责任和过程。它将客户和类的实现分离,客户通过他们的抽象接口操纵实例,产品的类名也在具体工厂的实现中被分离,它们不出现在客户代码中。它使得易于交换产品系列。一个具体工厂类在一个应用中仅出现一次——即在它初始化的时候。这使得改变一个应用的具体工厂变得很容易。它只需改变具体的工厂即可使用不同的产品配置,这是因为一个抽象工厂创建了一个完整的产品系列,所以整个产品系列会立刻改变。它有利于产品的一致性。当一个系列的产品对象被设计成一起工作时,一个应用一次只能使用同一个系列中的对象,这一点很重要,而抽象工厂很容易实现这一点。抽象工厂模式有助于这样的团队的分工,降低了模块间的耦合性,提高了团队开发效率。
三.缺点
简单工厂模式:当产品有复杂的多层等级结构时,工厂类只有自己,以不变应万变,就是模式的缺点。因为工厂类集中了所有产品创建逻辑,一旦增加产品或者删除产品,整个系统都要受到影响。系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,有可能造成工厂逻辑过于复杂,违背了"开放--封闭"原则(OCP).另外,简单工厂模式通常使用静态工厂方法,这使得无法由子类继承,造成工厂角色无法形成基于继承的等级结构。
工厂方法模式:假如某个具体产品类需要进行一定的修改,很可能需要修改对应的工厂类。当同时需要修改多个产品类的时候,对工厂类的修改会变得相当麻烦。比如说,每增加一个产品,相应的也要增加一个子工厂,会加大了额外的开发量。
抽象工厂模式:抽象工厂模式在于难于应付“新对象”的需求变动。难以支持新种类的产品。难以扩展抽象工厂以生产新种类的产品。这是因为抽象工厂几乎确定了可以被创建的产品集合,支持新种类的产品就需要扩展该工厂接口,这将涉及抽象工厂类及其所有子类的改变。
四.适用范围
简单工厂模式:工厂类负责创建的对象比较少,客户只知道传入了工厂类的参数,对于始何创建对象(逻辑)不关心。
工厂方法模式:当一个类不知道它所必须创建对象的类或一个类希望由子类来指定它所创建的对象时,当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候,可以使用工厂方法,支持多扩展少修改的OCP原则。
抽象工厂模式:一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有形态的工厂模式都是重要的。这个系统有多于一个的产品族,而系统只消费其中某一产品族。同属于同一个产品族的产品是在一起使用的,这一约束必须在系统的设计中体现出来。系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于实现。
其实,无论是简单工厂模式、工厂模式还是抽象工厂模式,它们本质上都是将不变的部分提取出来,将可变的部分留作接口,以达到最大程度上的复用。究竟用哪种设计模式更适合,这要根据具体的业务需求来决定。
7 什么是UML?
- UML是统一建模语言,Unified Modeling Language的缩写
- 综合了面向对象的建模语言、方法和过程,是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持
- 可以帮助沟通与交流、辅助应用设计、文档的生成、阐释系统的结构和行为
- 定义了多种图形化的符号来描述软件系统部分或全部的静态结构和动态结构
- 包括:用例图(use case diagram)、类图(class diagram)、时序图(sequence diagram)、协作图(collaboration diagram)、状态图(statechart diagram)、活动图(activity diagram)、构件图(component diagram)、部署图(deployment diagram)
8 什么是单例模式?有什么作用和特点?可以解决哪些问题?懒汉式和饿汉式的区别?如何保证线程安全?
单例模式:
一个类只允许创建一个实例对象,并提供访问其唯一的对象的方式。这个类就是一个单例类,这种设计模式叫作单例模式。
作用:
避免频繁创建和销毁系统全局使用的对象。
单例模式的特点:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例的访问
应用场景:
- 全局唯一类,如 系统配置类、系统硬件资源访问类
- 序列号生成器
- Web 计数器
饿汉式与懒汉式的区别:
- 饿汉式是类一旦加载,就把单例初始化完成,保证 getInstance() 方法被调用时的时候,单例已经初始化完成,可以直接使用。
- 懒汉式比较懒,只有当被调用 getInstance() 方法时,才会去初始化这个单例。
线程安全性问题:
饿汉式,在被调用 getInstance() 方法时,单例已经由 jvm 加载初始化完成,所以并发访问 getInstance() 方法返回的都是同一实例对象,线程安全。
懒汉式,要保证线程安全,可以有以下几种方式:
- 给静态 getInstance() 方法加锁,性能差
- getInstance() 方法双重检查给类加锁后创建对象(以上两种低版本 JDK,由于指令重排,需要加 volatile 关键字,否则创建出多个对象;JDK 1.5 内存模型加强后解决了对象 new 操作和初始化操作的原子性问题)
- 通过静态内部类实现
- 通过枚举实现
示例代码:1、饿汉式
/**
* 单例模式 饿汉式
* @author Lee
*/
public class TestSingleton {
private static final TestSingleton instance = new TestSingleton();
private TestSingleton() {}
public static TestSingleton getInstance() {
return instance;
}
}
2、懒汉式:线程不安全
/**
* 单例模式 懒汉式-线程不安全
* @author Lee
*/
public class TestSingleton {
private static TestSingleton instance;
private TestSingleton() {
}
public static TestSingleton getInstance() {
if (instance == null) {
instance = new TestSingleton();
}
return instance;
}
}
3、懒汉式:getInstance() 方法加锁,线程安全,性能差
/**
* 单例模式 懒汉式-加锁
* @author Lee
*/
public class TestSingleton {
private static volatile TestSingleton instance;
private TestSingleton() {
}
public static synchronized TestSingleton getInstance() {
if (instance == null) {
instance = new TestSingleton();
}
return instance;
}
}
4、懒汉式:双重检查 + 对类加锁
/**
* 单例模式 懒汉式-双重检查 + 对类加锁
* @author Lee
*/
public class TestSingleton {
private static volatile TestSingleton instance;
private TestSingleton() {
}
public static TestSingleton getInstance() {
if (instance == null) {
synchronized (TestSingleton.class) {
if (instance == null) {
instance = new TestSingleton();
}
}
}
return instance;
}
}
5、懒汉式:静态内部类
/**
* 单例模式 懒汉式-静态内部类
* @author Lee
*/
public class TestSingleton {
private static class SingletonHolder {
private static final TestSingleton instance = new TestSingleton();
}
private TestSingleton() {
}
public static TestSingleton getInstance() {
return SingletonHolder.instance;
}
6、懒汉式:枚举
import java.util.concurrent.atomic.AtomicLong;
/**
* 单例模式 懒汉式-枚举,id生成器
* @author Lee
*/
public enum TestSingleton {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
实现方式的选择建议:
- 没有特殊要求,建议使用 1、饿汉式,提前初始化好对象,虽然提前占用内存资源和提前了初始化的时间,但避免了懒加载过程中程序出现内存不够、超时等问题,符合 fail-fast 原则。
- 明确要求懒加载,可以使用 5、静态内部类的方式
- 有其他特殊要求,使用 4、双重检查 + 对类加锁的方法
9 构造方法的参数太多,如何解决?
开发中经常会遇到构造方法的参数很多,需要确认参数个数和位置;容易出现参数传错位的问题,而且 bug 不好排查。
如果使用默认构造方法,提供 public set 方法,又会把构造对象属性的修改权限放开,导致对象的属性数据安全问题。
这时候,可以使用 Builder 者模式。
/**
* 对象人
* @author Lee
*/
public class Person {
/**
* id
*/
private final int id;
/**
* 姓名
*/
private final String name;
/**
* 性别
*/
private final String sex;
/**
* 身高
*/
private final Double height;
/**
* 体重
*/
private final Double weight;
public static class Builder {
private int id;
private String name;
private String sex;
private Double height;
private Double weight;
public Builder() {
}
public Builder id(int id) {
this.id = id;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder sex(String sex) {
this.sex = sex;
return this;
}
public Builder height(Double height) {
this.height = height;
return this;
}
public Builder weight(Double weight) {
this.weight = weight;
return this;
}
public Person build() {
return new Person(this);
}
}
private Person(Builder builder) {
this.id = builder.id;
this.name = builder.name;
this.sex = builder.sex;
this.height = builder.height;
this.weight = builder.weight;
}
}
创建 Person 对象的代码
Person person = new Person.Builder()
.id(1)
.name("Lee")
.sex("男")
.height(1.70)
.weight(150.0)
.build();
Builder 模式需要注意是,Builder 类是静态内部类、类的构造方法是 private 的且参数为 Builder 对象。
Builder 模式不仅可以解决构造过程数据安全、参数过多、可读性的问题,还可以自动填充参数、为生成对象前对参数之间的关系进行合法校验等...
Builder 模式也带了新的问题:
- 创新对象前,必须创建 Builder 对象,多一些性能开销,对性能要求极高的场景下慎用。
- Builder 模式跟 1、2 两种方式比,代码行数更多,显得有点啰嗦。
10 模板方法模式
- 父类抽象出子类共有的方法,并且自己实现他
- 子类实现各自不同的业务
- 父类实现的方法按照一定的逻辑调用抽象方法
- 为了反之子类重写父类实现的方法父类定义为final方法
以上就是一个基本的模板方法。存在一个问题,父类按顺序调用的过程无法定制。也就是所有的子类都是按照固定的顺序执行自己的方法。所以需要钩子方法来动态的改变父类执行方法的流程
- 在父类中增加一个钩子方法,提供默认的实现
- 在父类中final方法可以判断一下这个钩子方法的逻辑,最终决定是否需要改变方法的顺序
- 子类可以重写钩子方法,进而达到动态改变父类执行方法的顺序
模板方法是一个很简单的设计模式,通过下面简单的实例来说明模板方法模
public abstract class AbstractCar {
private boolean didiFlag = true;
protected abstract void star();
protected abstract void didi();
protected abstract void shaChe();
protected abstract void stop();
public final void run(){
//按照固定顺序执行
this.star();
//判断是否设置了钩子属性
if(didiFlag){
this.didi();
}
this.shaChe();
this.stop();
}
//钩子方法,子类可以设置属性动态的改变父类执行的顺序
public void isDiDi(boolean didi){
didiFlag = didi;
}
}
抽象的父类实现类run方法,run方法的逻辑是固定的按照已经设置好的顺序执行,并且提供了一个钩子方法,让子类来操作是否需要嘀嘀作响的声音
public class BMWCar extends AbstractCar {
@Override
protected void star() {
System.out.println("宝马一键启动");
}
@Override
protected void didi() {
System.out.println("遇上碰瓷的,按喇叭");
}
@Override
protected void shaChe() {
System.out.println("试试刹车");
}
@Override
protected void stop() {
System.out.println("到达目的地,熄火");
}
}
实现了一个宝马的小汽车,具体如何启动,如何响都由自己决定
public class BenchiCar extends AbstractCar {
@Override
protected void star() {
System.out.println("奔驰一键启动");
}
@Override
protected void didi() {
System.out.println("开车喇叭就响");
}
@Override
protected void shaChe() {
System.out.println("刹车很灵");
}
@Override
protected void stop() {
System.out.println("到达目的地,熄火");
}
实现奔驰汽车,同样的可以自己决定自己的启动,响声
public class Client {
public static void main(String[] args) {
AbstractCar bmw = new BMWCar();
bmw.run();
AbstractCar benchi = new BenchiCar();
//调用钩子方法改变run执行的顺序
benchi.isDiDi(false);
benchi.run();
}
}
可以通过钩子方法改变父类执行方法的顺序