创建型模式
请解释什么是单例模式,并给出一个使用场景
单例模式(Singleton Pattern) 就是一个类只能有一个实例,它主要用于资源管理(避免资源冲突)保证全局唯一的场景。
具体使用场景
配置管理
基本上应用都会有一个全局配置,这个配置从理论上来说需要保证唯一性,确保读取到的配置是同一份,是一致的,所以天然适合单例实现。
连接池、线程池
池化资源需要保证唯一性,不然就没有池化的意义了,总不能每次访问池化资源都新建一个吧? 需要保持单例,控制具体池化资源的数量,便于管理和监控。
还有日志、缓存等需要全局唯一避免资源冲突的场景。
单例模式有哪几种实现?如何保证线程安全?
单例模式有很多种,首先最简单的是懒汉式单例
懒汉式
(线程不安全) 单例:【不可用】
为了避免内存空间浪费,采用懒汉式单例,即用到该单例对象的时候再创建。但是,存在很大问题,单线程下这段代码没有问题,但是在多线程下有很大问题。
public class LazyMan { private LazyMan(){}; public static LazyMan lazyMan; public static LazyMan getInstance(){ if (lazyMan==null){ lazyMan = new LazyMan(); //用到该单例对象的时候再创建,这里没加锁,如果多个线程进入,会并发产生多个实例 } return lazyMan; } }
这时可以改进为下面的线程安全版懒汉式
(线程安全)单例:【不推荐使用】
同步方法
public class LazyMan { private LazyMan(){}; //私有化构造函数,防止外部实例化 public static LazyMan lazyMan; public static synchroized LazyMan getInstance(){ //加锁 if (lazyMan==null){ lazyMan = new LazyMan(); //用到该单例对象的时候再创建 } return lazyMan; } }
缺点:效率低,每次getInstance时都要同步处理,存在性能问题,实际开发不推荐
双重检查单例(线程安全):【推荐使用】
实例化代码只用执行一次,后面再次访问时,判断
if (singleton == null)
,不为空则直接return实例化对象。利用
volatile
关键字保证了可见性,利用双重检查机制减少了同步带来的性能损耗。
public class Singleton { private static volatile Singleton instance; // 私有构造函数,防止外部实例化 private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
饿汉式
(静态常量)【可用】
类一旦加载就创建一个单例,保证在调用getInstance
方法之前单例已经存在,即没有延迟加载,这种饿汉式单例会造成空间浪费。
public class Hungry { private Hungry(){} private final static Hungry HUNGRY = new Hungry(); //在类内部就创建了一个静态对象,并且get方法返回该对象 public static Hungry getInstance(){ return HUNGRY; } } //或者 静态代码块形式 public class Hungry { static{ private final static Hungry HUNGRY = new Hungry(); } private Hungry(){} public static Hungry getInstance(){ return HUNGRY; } } Hungry in1=Hungry.getInstance() Hungry in2=Hungry.getInstance() //in1==in2
改进方法,使用下面的静态内部类形式
静态内部类
(线程安全)单例:【推荐使用】
不仅线程安全,还实现了延迟加载
public class Inner { private Inner(){} //直到调用 getInstance() 方法之前,Inner 实例都不会被创建,实现了延迟初始化 public static Inner getInstance(){ return InnerClass.INNER; } private static class InnerClass{ private static final Inner INNER = new Inner(); //静态字段只会在类加载时被初始化一次,线程安全 } }
枚举
(线程安全)【推荐使用】
不仅线程安全,还能防止反序列化导致重新创建新的对象
class SingletonEnum{ // 1、创建一个枚举 public enum CreateInstance{ // 枚举实例,底层变量定义是public static final,因此它在 JVM 中只会被初始化一次,并且是线程安全的 INSTANCE; private SingletonEnum instance; // 保证不能在类外部通过new构造器来构造对象 private CreateInstance() { instance = new SingletonEnum(); System.out.println(Thread.currentThread().getName()); } // 创建一个公共的方法,由实例调用返回单例类 public SingletonEnum getInstance() { return instance; } } public static void main(string[] args){ Singleton instance = CreateInstance.INSTANCE.getInstance(); singleton instance2= CreateInstance.INSTANCE.getInstance(); System.out.println(instance == instance2); //输出 true } }
工厂模式和抽象工厂模式有什么区别?
工厂模式
工厂模式定义了一个创建对象的接口,一个具体的工厂类负责生产一种产品,如果需要添加新的产品,仅需新增对应的具体工厂类而不需要修改原有的代码实现。
// 抽象产品 interface Product { void use(); } // 具体产品A class ConcreteProductA implements Product { public void use() { System.out.println("Using ConcreteProductA"); } } // 具体产品B class ConcreteProductB implements Product { public void use() { System.out.println("Using ConcreteProductB"); } } // 抽象工厂 interface Factory { Product createProduct(); } // 具体工厂A class ConcreteFactoryA implements Factory { public Product createProduct() { return new ConcreteProductA(); } } // 具体工厂B class ConcreteFactoryB implements Factory { public Product createProduct() { return new ConcreteProductB(); } } // 使用工厂方法创建产品 public class Client { public static void main(String[] args) { new ConcreteFactoryA().createProduct(); new ConcreteFactoryB().createProduct() ; } }
抽象工厂模式(Abstract Factory)
是工厂模式的一种变体,创建一系列相关或相互依赖对象的接口,即 生产一系列产品
在工厂模式基础上,给具体工厂类 添加一个工厂接口,使得工厂类可以多实现
// 抽象产品A public interface ProductA { void use(); } // 具体产品A1 public class ConcreteProductA1 implements ProductA { @Override public void use() { System.out.println("Using ConcreteProductA1"); } } // 具体产品A2 public class ConcreteProductA2 implements ProductA { @Override public void use() { System.out.println("Using ConcreteProductA2"); } } // 抽象产品B public interface ProductB { void eat(); } // 具体产品B1 public class ConcreteProductB1 implements ProductB { @Override public void eat() { System.out.println("Eating ConcreteProductB1"); } } // 具体产品B2 public class ConcreteProductB2 implements ProductB { @Override public void eat() { System.out.println("Eating ConcreteProductB2"); } } // 抽象工厂 public interface AbstractFactory { ProductA createProductA(); ProductB createProductB(); } // 具体工厂1 public class ConcreteFactory1 implements AbstractFactory { @Override public ProductA createProductA() { return new ConcreteProductA1(); } @Override public ProductB createProductB() { return new ConcreteProductB1(); } } // 具体工厂2 public class ConcreteFactory2 implements AbstractFactory { @Override public ProductA createProductA() { return new ConcreteProductA2(); } @Override public ProductB createProductB() { return new ConcreteProductB2(); } } // 使用抽象工厂创建产品 public class Client { public static void main(String[] args) { AbstractFactory factory1 = new ConcreteFactory1(); ProductA productA1 = factory1.createProductA(); ProductB productB1 = factory1.createProductB(); productA1.use(); productB1.eat(); AbstractFactory factory2 = new ConcreteFactory2(); ProductA productA2 = factory2.createProductA(); ProductB productB2 = factory2.createProductB(); productA2.use(); productB2.eat(); } }
简单工厂模式的工作原理。
前面提到的工厂模式是 通过创建对应工厂来创建产品,允许子类决定实例化哪一个类,符合开放封闭原则
而简单工厂模式(Simple Factory Pattern)不属于 GoF 23 种经典设计模式之一,但是在实际开发中非常常见
其通过传入的不同的参数来控制创建哪个具体产品。它的实现较为简单,但不够灵活,违反了开放封闭原则。
// 产品接口 public interface Product { void use(); } // 具体产品A public class ConcreteProductA implements Product { @Override public void use() { System.out.println("Using ConcreteProductA"); } } // 具体产品B public class ConcreteProductB implements Product { @Override public void use() { System.out.println("Using ConcreteProductB"); } } // 简单工厂类 public class SimpleFactory { public static Product createProduct(String type) { switch (type) { case "A": return new ConcreteProductA(); case "B": return new ConcreteProductB(); default: throw new IllegalArgumentException("Unknown product type"); } } } // 客户端代码 public class Client { public static void main(String[] args) { Product productA = SimpleFactory.createProduct("A"); productA.use(); // Output: Using ConcreteProductA Product productB = SimpleFactory.createProduct("B"); productB.use(); // Output: Using ConcreteProductB } }
什么是建造者模式?一般用在什么场景?
建造者模式(Builder)使用相同的代码基础构建不同类型的对象,通过将对象构建过程分解为多个较小的步骤来实现此目的,如StringBuilder
两个主要组成部分:建造者和产品 建造者是负责构建产品的类,产品则是最终构建的对象
public class Product { //最终构建的对象,即产品 private String partA; private String partB; private String partC; public void setPartA(String partA) { this.partA = partA; } public void setPartB(String partB) { this.partB = partB; } public void setPartC(String partC) { this.partC = partC; } // other product-related methods } public interface Builder { //Builder接口或抽象类,定义了构建过程的关键步骤 void buildPartA(); void buildPartB(); void buildPartC(); Product getResult(); } public class ConcreteBuilder implements Builder { //实现了Builder接口中定义的方法,以构建具体的Product对象 private Product product = new Product(); public void buildPartA() { product.setPartA("Part A"); } public void buildPartB() { product.setPartB("Part B"); } public void buildPartC() { product.setPartC("Part C"); } public Product getResult() { return product; } } public class Director { //指导类,它负责使用Builder对象来构建最终的Product对象 private Builder builder; public Director(Builder builder) { //实现依赖倒置原则,依赖于抽象 this.builder = builder; } public void construct() { //确保Product对象按照指定的顺序创建 builder.buildPartA(); builder.buildPartB(); builder.buildPartC(); } } Builder builder = new ConcreteBuilder(); //创建建造者 Director director = new Director(builder); //创建指导 director.construct(); Product product = builder.getResult(); //这将构建一个Product对象,并将其存储在product变量中。
适用场景
一般情况下,只有复杂对象的创建才需要使用建造者模式,比如一个对象有十几个参数,如果我们用构造器填参,可读性很差,还可能会搞混参数的赋值
例如 hutool 内的 ExecutorBuilder 就提供了建造者模式创建线程池的方法
public ExecutorService buildTaskPool() { return ExecutorBuilder.create() .setCorePoolSize(10) .setMaxPoolSize(20) .setWorkQueue(new LinkedBlockingQueue<>(100)) .setKeepAliveTime(3L, TimeUnit.SECONDS) .setThreadFactory(new ThreadFactoryBuilder().setNamePrefix("task-pool-").build()) .build(); }
建造者模式可以很容易地增加新的类型,只需要创建新的子类即可,不需要修改现有的代码。
通过建造者模式,可以对 对象的各个部分进行细致的控制,从而更好地管理对象的创建过程。
什么是原型模式 ? 一般用在什么场景?
原型模式也是一种创建型设计模式,主要通过复制(克隆)现有的实例来创建新的对象,避免复杂创建过程,提升创建对象的效率。
在 java 中 可以利用 clone 实现对象的拷贝,
注意:Java 的 clone 仅是浅拷贝,默写场景需要 使用深拷贝避免共享数据的导致错乱
在 Spring 中,将 Bean 的作用范围设置为 prototype,这样每次从容器中获取 Bean 时,都会返回一个新的实例。
浅拷贝:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型(指的是该对象内部的引用类型属性),就会拷贝指向原有对象的内存地址
深拷贝:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。
简单来说,就是深复制进行了完全彻底的复制,而浅拷贝不彻底。
一个简单的原型模式实现例子如下
先创建一个原型类:
public class Prototype implements Cloneable { //一个原型类,只需要实现Cloneable接口,覆写clone方法 @Override public Object clone() throws CloneNotSupportedException { Prototype proto = (Prototype) super.clone(); //因为此处的重点是super.clone()这句话 return proto; } } Prototype pro=new Prototype(); Prototype pro1=(Prototype)pro.clone(); //克隆 二者地址不同 //不管深浅,拷贝出来的对象的地址是不一样的,只是若对象里面有引用,那么深浅拷贝会不一样
@Data public class Prototype implements Cloneable, Serializable { private static final long serialVersionUID = 1L; private String string; private SerializableObject obj; /* 浅拷贝 */ public Object clone() throws CloneNotSupportedException { Prototype proto = (Prototype) super.clone(); return proto; } /* 深拷贝 */ public Object deepClone() throws IOException, ClassNotFoundException { /* 写入当前对象的二进制流 */ ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); /* 读出二进制流产生的新对象 */ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } } class SerializableObject implements Serializable { private static final long serialVersionUID = 1L; }
结构型模式
什么是适配器模式?一般用在什么场景?
相当于转接头,比如整合老系统的时候,由于老系统的接口已经跟不上新接口的设计,此时就可以利用适配器模式作为中间层,兼容老系统的接口调用。
适配器模式可以分为两种实现方式:类适配器和对象适配器。
类适配器: 通过多重继承,让适配器类同时继承目标接口和现有类,从而实现接口的适配。但在Java等语言中,由于不支持多重继承,类适配器的实现较为复杂,通常不常用。
// 目标接口 public interface Target { void request(); } // 适配者类 public class Adaptee { public void specificRequest() { System.out.println("Adaptee's specificRequest"); } } // 类适配器 public class Adapter extends Adaptee implements Target { @Override public void request() { specificRequest(); } } // 客户端代码 public class Client { public static void main(String[] args) { Target target = new Adapter(); target.request(); // Output: Adaptee's specificRequest } }
对象适配器: 通过组合,让适配器类持有现有类的实例,并实现目标接口。对象适配器使用组合关系来实现接口的适配,较为常用。
// 目标接口 public interface Target { void request(); } // 适配者类 public class Adaptee { public void specificRequest() { System.out.println("Adaptee's specificRequest"); } } // 对象适配器 public class Adapter implements Target { //这里不再是和类适配器一样继承适配器,而是在类中装配适配器对象 private Adaptee adaptee; public Adapter(Adaptee adaptee) { this.adaptee = adaptee; } @Override public void request() { adaptee.specificRequest(); } } // 客户端代码 public class Client { public static void main(String[] args) { Adaptee adaptee = new Adaptee(); Target target = new Adapter(adaptee); target.request(); // Output: Adaptee's specificRequest } }
Java 日志中的 slf4j 其实就是使用了适配器模式来统一不同日志框架接口,使得我们不需要修改代码就可以替换不同的底层日志实现。
什么是桥接模式?一般用在什么场景?
桥接模式使用场景比较少,它主要的作用是将抽象和实现解耦,使它们可以独立地变化。
我们熟知的JDBC 就使用了桥接模式,借着它我们来理解下这个设计模式。
JDBC定义了抽象的规范,不同的数据库厂商遵循这些规范,但是它们各自又会有不同的实现。 在使用中,如果我们数据库是 mysql,则传入 com.mysql.jdbc.Driver 驱动实现类,如果要换成 oracle替换实现类为oracle.jdbc.driver.OracleDriver 即可,这就是典型的抽象与实现解耦。
实际执行数据库操作,例如获取连接,实际上就是委托给具体的实现类,JDBC源码如下:
这样设计后,如果更换数据库,除了替换实现类,关于JDBC的使用代码我们不需要做任何修改,这就是桥接模式带来的好处,提升了系统的可扩展性,也符合开闭原则。
代码实例
实现形状颜色解耦
实现化(Implementor):定义颜色接口
public interface Color { void applyColor(); }
具体实现化(Concrete Implementor):实现具体颜色
public class RedColor implements Color { @Override public void applyColor() { System.out.println("Applying red color"); //红色 } } public class BlueColor implements Color { //蓝色 @Override public void applyColor() { System.out.println("Applying blue color"); } }
抽象化(Abstraction):定义形状接口
public abstract class Shape { protected Color color; public Shape(Color color) { this.color = color; } public abstract void draw(); }
细化抽象化(Refined Abstraction):实现具体形状
public class Circle extends Shape { public Circle(Color color) { super(color); } @Override public void draw() { System.out.print("Drawing a circle with "); color.applyColor(); } } public class Rectangle extends Shape { public Rectangle(Color color) { super(color); } @Override public void draw() { System.out.print("Drawing a rectangle with "); color.applyColor(); } }
客户端代码
public class Client { public static void main(String[] args) { // 创建红色圆形 Shape redCircle = new Circle(new RedColor()); redCircle.draw(); // 输出: Drawing a circle with Applying red color // 创建蓝色矩形 Shape blueRectangle = new Rectangle(new BlueColor()); blueRectangle.draw(); // 输出: Drawing a rectangle with Applying blue color } }
什么是组合模式?一般用在什么场景?
又叫部分整体模式,将对象组合成树状结构以表示“整体——部分”的层次关系
注意,数据必须是树形结构,也因此这个设计模式的应用场景有限,但在业务上还是比较常见,因为很多业务都有后台管理系统,含菜单管理、部门管理等。 例如菜单、子菜单、权限等关系就可以组合成树形结构,部门、子部分以及人员也可以组合成树形结构,这种场景就很契合组合模式的实现。
以部门为示例,展示组织内的部门和人员信息,我们简单看下实现代码:
// 抽象组织 interface OrganizationComponent { void showDetails(); } // 组织人员 class Employee implements OrganizationComponent { private String name; private String position; public Employee(String name, String position) { this.name = name; this.position = position; } @Override public void showDetails() { System.out.println("Employee: " + name + ", Position: " + position); } } // 组织部门 class Department implements OrganizationComponent { private String name; private List<OrganizationComponent> components = new ArrayList<>(); public Department(String name) { this.name = name; } @Override public void showDetails() { System.out.println("Department: " + name); for (OrganizationComponent component : components) { component.showDetails(); } } public void add(OrganizationComponent component) { //往部门添加人员 或 部门 components.add(component); } public void remove(OrganizationComponent component) { components.remove(component); } } // 使用代码 public class Client { public static void main(String[] args) { // 创建人员 OrganizationComponent employee1 = new Employee("John Doe", "Developer"); OrganizationComponent employee2 = new Employee("Jane Smith", "Designer"); OrganizationComponent employee3 = new Employee("Emily Davis", "Manager"); // 创建部门 Department engineeringDepartment = new Department("Engineering Department"); Department designDepartment = new Department("Design Department"); Department headDepartment = new Department("Head Department"); // 添加人员到部门 engineeringDepartment.add(employee1); designDepartment.add(employee2); headDepartment.add(employee3); // 添加子部门到上级部门 headDepartment.add(engineeringDepartment); headDepartment.add(designDepartment); // 显示整个组织结构的详情 headDepartment.showDetails(); // 输出 // Department: Head Department // Employee: Emily Davis, Position: Manager // Department: Engineering Department // Employee: John Doe, Position: Developer // Department: Design Department // Employee: Jane Smith, Position: Designer } }
通过组合模式,提供统一的接口,简化对层次结构的处理,使得使用方代码更加简洁与灵活。
什么是装饰器模式?一般用在什么场景?
装饰器模式(Decorator Pattern) 主要作用是通过创建包装类来实现功能的增强,而不是修改原始类。
通过创建一个装饰器类,该类实现了与原始对象相同的接口,并持有一个原始对象的引用。通过将原始对象传递给装饰器
最典型的装饰器实现就是 Java 中的I/O类库,示例代码如下:
import java.io.*; public class IOExample { public static void main(String[] args) throws IOException { File file = new File("test.txt"); FileInputStream fis = new FileInputStream(file); //读取文件流 BufferedInputStream bis = new BufferedInputStream(fis); //fis被BufferedlnputStream 装饰,提供了缓存的功能 DataInputStream dis = new DataInputStream(bis); //被 DatalnputStream 装饰,提供了按数据类型读取的功能 while (dis.available() > 0) { System.out.println(dis.readLine()); } dis.close(); } }
可以看到,多个装饰器叠加在一起,实现了多个功能的增强,且不会增加类的实现复杂度,每个装饰器仅需关注自己的加强功能即可,提高代码的灵活性和可维护性。
文件流的代码比较复杂,这里就不展示了,我们简单看个多装饰器叠加的代码实现:
// 组件 interface Component { void operation(); } // 具体构件 class ConcreteComponent implements Component { @Override public void operation() { System.out.println("ConcreteComponent operation"); } } // 装饰器 abstract class Decorator implements Component { protected Component component; public Decorator(Component component) { this.component = component; } @Override public void operation() { component.operation(); } } // 具体装饰器 A class ConcreteDecoratorA extends Decorator { public ConcreteDecoratorA(Component component) { super(component); } @Override public void operation() { super.operation(); addedBehavior(); } private void addedBehavior() { System.out.println("ConcreteDecoratorA added behavior"); } } // 具体装饰器 B class ConcreteDecoratorB extends Decorator { public ConcreteDecoratorB(Component component) { super(component); } @Override public void operation() { super.operation(); addedState(); } private void addedState() { System.out.println("ConcreteDecoratorB added state"); } } // 客户端代码 public class Client { public static void main(String[] args) { Component component = new ConcreteComponent(); Component decoratorA = new ConcreteDecoratorA(component); Component decoratorB = new ConcreteDecoratorB(decoratorA); decoratorB.operation(); // Output: // ConcreteComponent operation // ConcreteDecoratorA added behavior // ConcreteDecoratorB added state } }
装饰器、适配器、代理、桥接这四种设计模式有什么区别?
这四种设计模式看起来非常相像,有些甚至从代码上来看差不了多少,那它们之间有什么区别呢?
其实设计模式最大的区别就是 设计思路,它们的出发点都是不一样的。
装饰器模式: 目的是在不改变原始类接口、不对原始类复杂化的情况下,对原始类进行本身功能增强,并且支持嵌套多装饰器多功能增强。
适配器模式: 目的是面对不兼容的接口进行适配,主要是兼容的问题,不得已的做法。好比我们出国旅游,每国的插头标准不一样,我们只能做转接头适配。
代理模式: 目的是在不改变原始类接口的情况下,为原始类提供代理类,对其进行访问控制,实现额外功能,它和装饰器的区别在于不是增强原始类本身对应的功能。
桥接模式: 它的目的是将抽象与实现分离,使它们可以独立地变化,例如JDBC场景,不同数据库厂商的不同实现。
什么是外观模式 ? 一般用在什么场景?
外观模式(Facade Pattern)也叫门面模式,是一种结构型设计模式,它提供了一个简化的接口,用于访问复杂系统中的一组接口。将复杂系统的功能封装起来,让客户端可以更方便地使用系统功能而不需要了解其内部复杂结构。
举个例子就清晰了。例如 A系统有 a、b、c三个接口,B需要分别调用 a、b、c三个接口,这样比较麻烦,所以A将 a、b、c封装成一个接口给B调用,对B来说使用起来就方便了,这就是外观模式
不过在使用外观模式的同时,我们也需要考虑接口的设计粒度,如果接口的粒度太大,可复用性就低了,接口的粒度太小,易用性就低了。所以设计是一个权衡的过程,我们需要综合考虑,理论上可复用性为主,然后特殊情况利用外观模式封装一个大接口提供出去。
class CPU { //CPU public void start() { System.out.println("CPU 启动"); } public void shutdown() { System.out.println("CPU 关闭"); } } class Memory { //内存 public void start() { System.out.println("内存启动"); } public void shutdown() { System.out.println("内存关闭"); } } class HardDrive { //硬盘 public void start() { System.out.println("硬盘启动"); } public void shutdown() { System.out.println("硬盘关闭"); } } class ComputerFacade { //外观类,封装了计算机系统的一组复杂接口,包括 CPU、内存和硬盘的启动和关闭 private CPU cpu; private Memory memory; private HardDrive hardDrive; public ComputerFacade() { cpu = new CPU(); memory = new Memory(); hardDrive = new HardDrive(); } public void start() { //启动计算机 System.out.println("计算机启动开始"); cpu.start(); memory.start(); hardDrive.start(); System.out.println("计算机启动完成"); } public void shutdown() { //关闭计算机 System.out.println("计算机关闭开始"); cpu.shutdown(); memory.shutdown(); hardDrive.shutdown(); System.out.println("计算机关闭完成"); } }
ComputerFacade computerFacade = new ComputerFacade(); computerFacade.start(); // 输出: // 计算机启动开始 // CPU 启动 // 内存启动 // 硬盘启动 // 计算机启动完成 computerFacade.shutdown(); // 输出: // 计算机关闭开始 // CPU 关闭 // 内存关闭 // 硬盘关闭 // 计算机关闭完成
什么是享元模式?一般用在什么场景?
享元模式(Flyweight Pattern)是一种结构型设计模式,主要用于通过共享来减少内存占用,提高内存效率。
例如一个系统中可能会存在大量的重复对象,并且这些对象实际是不可变的,在设计上我们就可以仅在内存中保留一份实例,然后多处引用即可,这样就能节省内存的开销。
在 Integer 中就采用了享元模式,即 integer 中缓冲池。即 Integer -128 到 127 之内的相等,而超过这个范围用 == 就不对了,因为这个范围内采用了享元模式,本质就是同一个对象,所以用 == 判断相等。
具体原理可看 Java基础 29.什么是Java的Integer缓冲池?
示例:
假设我们要绘制棋盘上的棋子,棋子的种类有很多,但是每种棋子的形状和颜色是固定的。我们可以使用享元模式来共享相同种类的棋子对象,从而减少对象的创建数量。
// 棋子接口 interface ChessPiece { void setColor(String color); void display(int x, int y); } // 具体棋子类 class ConcreteChessPiece implements ChessPiece { private String color; public ConcreteChessPiece(String color) { this.color = color; } @Override public void setColor(String color) { this.color = color; } @Override public void display(int x, int y) { System.out.println("Chess Piece color: " + color + ", position: (" + x + "," + y + ")"); } }
// 享元工厂类 class ChessPieceFactory { private Map<String, ChessPiece> chessPieces; public ChessPieceFactory() { this.chessPieces = new HashMap<>(); //用集合存chessPieces } public ChessPiece getChessPiece(String color) { //返回单例 ChessPiece chessPiece = chessPieces.get(color); if (chessPiece == null) { chessPiece = new ConcreteChessPiece(color); chessPieces.put(color, chessPiece); } return chessPiece; } }
// 客户端代码 public class Client { public static void main(String[] args) { ChessPieceFactory chessPieceFactory = new ChessPieceFactory(); ChessPiece blackPiece1 = chessPieceFactory.getChessPiece("black"); ChessPiece blackPiece2 = chessPieceFactory.getChessPiece("black"); ChessPiece whitePiece1 = chessPieceFactory.getChessPiece("white"); ChessPiece whitePiece2 = chessPieceFactory.getChessPiece("white"); blackPiece1.display(1, 2); // 输出:Chess Piece color: black, position: (1,2) blackPiece2.display(3, 4); // 输出:Chess Piece color: black, position: (3,4) whitePiece1.display(5, 6); // 输出:Chess Piece color: white, position: (5,6) whitePiece2.display(7, 8); // 输出:Chess Piece color: white, position: (7,8) } }
什么是代理模式?一般用在什么场景?
代理模式就是 通过一个代理对象来控制对另一个对象的访问,在不改变原始对象的情况下,提供了对原始对象的间接访问,实现额外的功能。
这个模式在我们业务中太常见了,例如动态代理,Spring AOP ,RPC 框架也是使用了动态代理才使得调用远程方法和本地方法一样。
所以,统一报错、监控、限流、鉴权等等,需要跟业务解耦的功能,我们基本上都是使用代理类进行统一处理的。 简单举个计数代理的例子,比如统计文档展示的次数,通过 DocumentProxy 代理类就附加了这个功能:
核心是创建一个代理类,该类实现了与原始对象相同的接口,并持有一个原始对象的引用。在代理类的方法中,我们可以添加额外的功能,然后将请求转发给原始对象进行处理。
// 图片接口 interface Image { void display(); } // 具体图片类 class RealImage implements Image { private String filename; public RealImage(String filename) { this.filename = filename; loadImageFromDisk(); } private void loadImageFromDisk() { System.out.println("Loading " + filename + " from disk."); } @Override public void display() { System.out.println("Displaying " + filename); } }
// 代理图片类 class ProxyImage implements Image { private RealImage realImage; //引入具体图片类 private String filename; //实现了对原始图片对象的控制,而不需要修改原始图片类的代码 public ProxyImage(String filename) { this.filename = filename; } @Override public void display() { if (realImage == null) { realImage = new RealImage(filename); } beforeDisplay(); realImage.display(); } private void beforeDisplay() { System.out.println("Before displaying " + filename + ", do some pre-processing."); } }
// 客户端代码 public class Client { public static void main(String[] args) { Image image1 = new ProxyImage("image1.jpg"); Image image2 = new ProxyImage("image2.jpg"); image1.display(); // 输出:Before displaying image1.jpg, do some pre-processing. // Loading image1.jpg from disk. // Displaying image1.jpg image2.display(); // 输出:Before displaying image2.jpg, do some pre-processing. // Loading image2.jpg from disk. // Displaying image2.jpg } }
行为型模式
什么是观察者模式?一般用在什么场景?
观察者模式其实也称为发布订阅模式,它定义了对象之间的一种一对多的依赖关系,让多个观察者对象 同时监听某一个主题对象。当主题对象状态发生变化时,它会通知所有观察者对象。
目的:将观察者和被观察者代码解耦,使得一个对象或者事件的变更,让不同观察者可以有不同的处理,非常灵活,扩展性很强,是事件驱动编程的基础。
观察者模式的组成部分
Subject(主题/被观察者): 状态发生变化时,通知所有注册的观察者
Observer(观察者): 接收来自主题的更新通知,并进行相应的操作。
ConcreteSubject(具体主题): 实现具体的主题对象,保存需要被观察的状态。
ConcreteObserver(具体观察者): 实现具体的观察者对象,更新自己以与主题的状态同步
我们以一个新闻发布系统来理解下观察者模式。
1)定义接口
新闻发布者(Subject)可以发布新闻,观察者(Observer)是订阅者,希望获取新闻更新
2)实现具体类
具体新闻发布者(Concrete Subject)维护一个订阅者列表,并在发布新闻时通知他们
具体观察者(Concrete Observer)如报纸、新闻网站等,实现更新方法来展示新闻
3)订阅和退订
订阅者可以订阅(注册)或退订(注销)新闻发布者的新闻。
4)发布新闻
当新闻发布者有新新闻时,"观察者更新自己的新闻内容。它通知所有订阅的观察者
5)客户端代码
客户端代码创建新闻发布者和观察者对象,订阅者选择订新闻,并在接收到新闻时更新显示。
// 定义观察者接口 interface Observer { void update(String news); } // 定义主题接口 interface Subject { void attach(Observer o); void detach(Observer o); void notifyObservers(); } // 具体新闻发布者 class NewsPublisher implements Subject { private List<Observer> observers = new ArrayList<>(); public void attach(Observer o) { observers.add(o); } public void detach(Observer o) { observers.remove(o); } public void notifyObservers() { for (Observer observer : observers) { observer.update("新闻更新"); } } public void publishNews() { // 假设这里是新闻发布逻辑,会通知发布者里面集合的观察者 notifyObservers(); } } // 具体观察者 class NewsPaper implements Observer { public void update(String news) { System.out.println("新报 " + news); } } // 客户端代码 public class Client { public static void main(String[] args) { NewsPublisher publisher = new NewsPublisher(); //发布者 Observer newsPaper = new NewsPaper(); //观察者 publisher.attach(newsPaper); //导入观察者 publisher.publishNews(); //发布新闻 publisher.detach(newsPaper); //移除观察者 publisher.publishNews(); // 此时报纸订阅者不会接收到新闻 } }
例如消息队列、 Spring 内的监听器机制等都是其应用场景
什么是迭代器模式?一般用在什么场景?
迭代器模式,它提供一种方法顺序访问一个集合对象中的各个元素,而又不暴露该对象(可能是数组、链表、树等等)的内部实现。
将遍历逻辑与集合对象的实现分离,提供一致的遍历方式,使得代码统一化,在不改变遍历代码的情况下就能替换底层集合实现。
像 Java 的java.util.Iterator
接口和Iterable
接口是迭代器模式的直接应用。所有集合类(如ArrayList、HashSet、LinkedList 等)都实现了 Iterable 接口,并提供了 iterator() 方法来获取迭代器,例如,遍历 ArrayList 中的元素:
List<String> list = new ArrayList<>(); list.add("one"); list.add("two"); list.add("three"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); }
再比如 JDBC 的ResultSet
接口遍历数据库査询结果,也是迭代器模式的实现。
迭代器模式的组成部分
lterator(迭代器接口): 定义访问和历元素的接口
Aggregate(聚合接口): 定义创建迭代器的接口
Concretelterator(具体迭代器): 实现迭代器接口,负责遍历聚合对象中的元素
ConcreteAggregate(具体聚合类): 实现聚合接口,返回一个具体的迭代器实例。
我们来简单实现一个迭代器
//定义迭代器接口 interface Iterator<T> { boolean hasNext(); T next(); } //定义聚合接口 interface Aggregate<T> { Iterator<T> createIterator(); } //定义具体迭代器 class ConcreteIterator<T> implements Iterator<T> { private List<T> items; private int position = 0; //当前遍历位置 public ConcreteIterator(List<T> items) { this.items = items; } @Override public boolean hasNext() { //如果进行集合遍历时位置<集合大小,则返回true return position < items.size(); } @Override public T next() { //获取当前遍历位置的下一个元素 return items.get(position++); } } //定义具体聚合类 class ConcreteAggregate<T> implements Aggregate<T> { private List<T> items = new ArrayList<>(); public void addItem(T item) { //添加元素方法 items.add(item); } @Override public Iterator<T> createIterator() { return new ConcreteIterator<>(items); } } //简单使用 public class Client { public static void main(String[] args) { ConcreteAggregate<String> aggregate = new ConcreteAggregate<>(); aggregate.addItem("Item 1"); //添加集合元素 aggregate.addItem("Item 2"); aggregate.addItem("Item 3"); Iterator<String> iterator = aggregate.createIterator(); //返回对应该集合的迭代器 while (iterator.hasNext()) { System.out.println(iterator.next()); } } }
什么是命令模式?一般用在什么场景?
命令模式(Command Pattern)是一种行为设计模式,它将请求封装成对象,使得请求参数化,便于对请求排队或记录请求日志,以及支持可撤销的操作。
大部分编程语言都不支持以函数作为参数传递,利用命令模式就可以实现这点。听起来稍微有点抽象,其实命令模式非常少见,被问的频率极低,稍作了解即可。
简单看下以开关灯为例实现命令模式的代码,更容易理解上面那段含义:
// 命令接口 interface Command { void execute(); } // 接收者 class Light { public void on() { //用于控制灯光的开关 System.out.println("The light is on."); } public void off() { System.out.println("The light is off."); } } // 具体命令 开灯 class LightOnCommand implements Command { private Light light; public LightOnCommand(Light light) { this.light = light; } @Override public void execute() { light.on(); } } // 具体命令 关灯 class LightOffCommand implements Command { private Light light; public LightOffCommand(Light light) { this.light = light; } @Override public void execute() { light.off(); } } // 调用者,传入了开关命令,因此可以开关灯 class RemoteControl { private Command command; public void setCommand(Command command) { this.command = command; } public void pressButton() { command.execute(); } } // 客户端代码 public class Client { public static void main(String[] args) { Light light = new Light(); Command lightOn = new LightOnCommand(light); //开灯 Command lightOff = new LightOffCommand(light); //关灯 RemoteControl remote = new RemoteControl(); remote.setCommand(lightOn); remote.pressButton(); //开灯 remote.setCommand(lightOff); remote.pressButton(); //关灯 } }
什么是状态模式?一般用在什么场景?
状态模式 允许对象在其内部状态发生改变时改变其行为,将状态的行为封装在独立的类中,并将这些状态对象组合在拥有状态的对象中,这样就可以在状态改变时切换状态对象,从而改变对象的行为
它主要是状态机的一种实现方式,状态机可分为: 状态、事件、动作三个部分。事件的触发就会导致状态的改变,并且可作出一定的动作(也可以没有动作,只有状态的改变)
// 定义状态接口 interface State { void handle(Context context); } // 上下文环境,状态模式的核心,它持有一个 State 对象,并提供了 setState 方法来改变当前状态 class Context { private State state; public Context(State state) { this.state = state; } public void setState(State state) { this.state = state; } public void request() { state.handle(this); } } // 具体实现类 //AB每个类在处理请求时都会打印一条消息,并且会改变上下文 Context 的状态为另一种状态 class ConcreteStateA implements State { @Override public void handle(Context context) { System.out.println("State A is handling the request."); context.setState(new ConcreteStateB()); } } class ConcreteStateB implements State { @Override public void handle(Context context) { System.out.println("State B is handling the request."); context.setState(new ConcreteStateA()); } } public class Client { public static void main(String[] args) { Context context = new Context(new ConcreteStateA()); //新建A状态环境 context.request(); // State A is handling the request. //请求A状态,然后设置当前状态为B context.request(); // State B is handling the request. context.request(); // State A is handling the request. context.request(); // State B is handling the request. } }
什么是责任链模式?一般用在什么场景?
责任链模式允许将多个对象连接成一条链,并且沿着这条链传递请求,让多个对象都有机会处理这个请求,请求会顺着链传递,直到某个对象处理它为止。
它主要避免了请求发送者和接受者之间的耦合,增强了系统的灵活性和可扩展性。
在很多场景都能看到责任链模式,比如日志的处理,不同级别不同输出。再比如 Spring 过滤器的 Chain 也是责任链模式。
下面是一个简单的日志处理示例代码。
日志处理器有三种类型: 控制台日志处理器(ConsoleLogger)、文件日志处理器(FileLogger)、错误日志处理器(ErrorLogger)。
日志处理器按照优先级进行处理,如果当前处理器不能处理,则将请求传递给下一个处理器。
// 责任链模式的抽象日志类 abstract class Logger { public static int INFO = 1; public static int DEBUG = 2; public static int ERROR = 3; protected int level; protected Logger nextLogger; //日志中可以存下一个日志 public void setNextLogger(Logger nextLogger) { this.nextLogger = nextLogger; } public void logMessage(int level, String message) { if (this.level <= level) { write(message); } if (nextLogger != null) { nextLogger.logMessage(level, message); } } protected abstract void write(String message); } // 具体处理器类:控制台日志处理器 class ConsoleLogger extends Logger { public ConsoleLogger(int level) { this.level = level; } @Override protected void write(String message) { System.out.println("Standard Console::Logger: " + message); } } // 具体处理器类:文件日志处理器 class FileLogger extends Logger { public FileLogger(int level) { this.level = level; } @Override protected void write(String message) { System.out.println("File::Logger: " + message); } } // 具体处理器类:错误日志处理器 class ErrorLogger extends Logger { public ErrorLogger(int level) { this.level = level; } @Override protected void write(String message) { System.out.println("Error Console::Logger: " + message); } } // 客户端代码 public class ChainPatternDemo { private static Logger getChainOfLoggers() { //获取日志责任链 Logger errorLogger = new ErrorLogger(Logger.ERROR); Logger fileLogger = new FileLogger(Logger.DEBUG); Logger consoleLogger = new ConsoleLogger(Logger.INFO); errorLogger.setNextLogger(fileLogger); //存下一个日志 fileLogger.setNextLogger(consoleLogger); //存再下一个日志 return errorLogger; //最后返回 } public static void main(String[] args) { Logger loggerChain = getChainOfLoggers(); loggerChain.logMessage(Logger.INFO, "mianshiya.com"); loggerChain.logMessage(Logger.DEBUG, "小程序:面试鸭"); loggerChain.logMessage(Logger.ERROR, "网页端:mianshiya.com"); } }
什么是中介者模式?一般用在什么场景?
中介模式通过引入了一个中介对象,来封装一组对象之间的交互,来避免对象之间的直接交互。通过引入一个中介者对象,使对象之间的关系变得简单且易于维护。
听起来不就是和现实生活中的中介一样嘛。
引入中介模式的原因:多对象交互可能会使得关系图很混乱,代码也不清晰,让多对象都和中介交互就能避免这点
聊天室实现其实就是运用了中介模式。信息的传递都由服务器这个中介来做,所有用户都把消息发给服务器,不然如果点对点传输大家可以想象下有多复杂。我们可以简单的看下聊天室的实现:
interface ChatMediator { //中介者接口 void sendMessage(String message, User user); void addUser(User user); } class ChatMediatorImpl implements ChatMediator { //具体中介者 private List<User> users; //存储加入的用户 public ChatMediatorImpl() { this.users = new ArrayList<>(); } @Override public void addUser(User user) { this.users.add(user); } @Override public void sendMessage(String message, User user) { for (User u : this.users) { if (u != user) { u.receive(message); } } } } abstract class User { //抽象用户 protected ChatMediator mediator; protected String name; public User(ChatMediator mediator, String name) { this.mediator = mediator; this.name = name; } public abstract void send(String message); public abstract void receive(String message); } class UserImpl extends User { //用户 public UserImpl(ChatMediator mediator, String name) { super(mediator, name); } @Override public void send(String message) { System.out.println(this.name + " sends: " + message); mediator.sendMessage(message, this); } @Override public void receive(String message) { System.out.println(this.name + " receives: " + message); } } public class ChatClient { //客户端 public static void main(String[] args) { ChatMediator mediator = new ChatMediatorImpl(); User user1 = new UserImpl(mediator, "Alice"); User user2 = new UserImpl(mediator, "Bob"); User user3 = new UserImpl(mediator, "Charlie"); mediator.addUser(user1); //用户加入中介 mediator.addUser(user2); mediator.addUser(user3); user1.send("Hello, everyone!"); } }
什么是访问者模式?一般用在什么场景?
访问者模式它将数据结构与操作分离,使得你可以在不改变数据结构的前提下定义新的操作。访问者模式通过将操作封装到独立的访问者对象中,使得新的操作可以很容易地添加到系统中。
例如对象序列化场景,比如需要将对象转成 JSON 或者 XML 等格式,利用访问者模式将对象的结构和序列化操作分离,这样就能很方便的扩展新的序列化格式。先看下示例代码:
//访问者接口(类比序列化接口) interface Visitor { void visit(ElementA element); void visit(ElementB element); } //具体访问者类(类比具体序列化实现) class ConcreteVisitor implements Visitor { @Override public void visit(ElementA element) { System.out.println("Processing ElementA: " + element.getName()); } @Override public void visit(ElementB element) { System.out.println("Processing ElementB: " + element.getName()); } } //需要对序列化的元素接口 interface Element { void accept(Visitor visitor); } // 具体被序列化的元素A class ElementA implements Element { private String name; public ElementA(String name) { this.name = name; } public String getName() { return name; } @Override public void accept(Visitor visitor) { visitor.visit(this); } } // 具体被序列化的元素B class ElementB implements Element { private String name; public ElementB(String name) { this.name = name; } public String getName() { return name; } @Override public void accept(Visitor visitor) { visitor.visit(this); } } //对象结构,包含很多元素 class ObjectStructure { private List<Element> elements = new ArrayList<>(); public void addElement(Element element) { elements.add(element); } public void accept(Visitor visitor) { for (Element element : elements) { element.accept(visitor); } } } // 客户端代码 public class Client { public static void main(String[] args) { // 组装对象 ObjectStructure objectStructure = new ObjectStructure(); objectStructure.addElement(new ElementA("Element A1")); objectStructure.addElement(new ElementB("Element B1")); objectStructure.addElement(new ElementA("Element A2")); //访问者 (如果是序列化场景,ConcreteVisitor 可以当做 JSON 序列化) Visitor visitor = new ConcreteVisitor(); objectStructure.accept(visitor); // 假设后面要替换 xml 序列化,仅需新建 xml 的访问者,然后传入到对象内部即可 Visitor visitorXML = new XMLVisitor(); objectStructure.accept(visitorXML); } }
可以看到,将数据结构和操作分离开之后,如果要替换具体的操作,仅需新增一个操作即可,不需要修改任何数据结构,这就符合开闭原则,也保证了类的职责单一。
什么是备忘录模式,一般用在什么场景?
备忘录模式指的是在不违背封装原则的前提下,捕获对象内部的状态,将其保存在外部,便于后面对象恢复之前的状态,使得系统更具灵活性和可维护性。
听来像不像保留个快照作为备份,后面基于这个快照进行恢复? 所以备忘录模式其实也叫快照模式。主要用于撤销、恢复等场景。
来看下示例代码,我先大致解释一下几个类的含义:
Memento,存储状态的备忘录
Originator,需要备份状态的对象类
Caretaker,管理者,仅保存备忘录
// 备忘录类 class Memento { private String state; public Memento(String state) { this.state = state; } public String getState() { return state; } } // 需要备份状态的对象类 class Originator { private String state; public void setState(String state) { this.state = state; System.out.println("State set to: " + state); } public String getState() { return state; } public Memento createMemento() { return new Memento(state); } public void restoreMemento(Memento memento) { this.state = memento.getState(); System.out.println("State restored to: " + state); } } //管理存储类 class Caretaker { private Memento memento; public Memento getMemento() { return memento; } public void setMemento(Memento memento) { this.memento = memento; } } //客户端类 public class Client { public static void main(String[] args) { Originator originator = new Originator(); //对象类 Caretaker caretaker = new Caretaker(); //存储类 originator.setState("State1"); caretaker.setMemento(originator.createMemento()); //保存快照 originator.setState("State2"); System.out.println("Current State: " + originator.getState()); originator.restoreMemento(caretaker.getMemento()); //恢复快照 System.out.println("Restored State: " + originator.getState()); } }
如果直接利用 set 方法修改内部状态,就违反了封装的原则,因为如果你暴露了 set 方法,则对象内部的状态很可能被别的业务调用修改了。而 restoreMemento 是一个很清晰的方法定义,即恢复之前的状态,不会被乱用。这也是备忘录模式的前提,不违反封装原则。