23种设计模式之创建型模式
一.创建型模式关注点是如何创建对象,其核心思想是要把对象的创建和使用相分离,这样使得两者能相对独立地变换。
二.创建型模式包括:
- 工厂方法:Factory Method
- 抽象工厂:Abstract Factory
- 建造者:Builder
- 原型:Prototype
- 单例:Singleton
1.工厂方法
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。
工厂方法的目的是使得创建对象和使用对象是分离的,并且客户端总是引用抽象工厂和抽象产品:
参照具体的例子来说:假设我们希望实现一个解析字符串到Number的Factory,可以定义如下:
public interface NumberFactory {
Number parse(String s);
}
有了工厂接口,再编写一个工厂的实现类:
public class NumberFactoryImpl implements NumberFactory {
public Number parse(String s) {
return new BigDecimal(s);
}
}
而产品接口是Number,NumberFactoryImpl返回的实际产品是BigDecimal。
那么客户端如何创建NumberFactoryImpl呢?通常我们会在接口Factory中定义一个静态方法getFactory()来返回真正的子类:
public interface NumberFactory {
// 创建方法:
Number parse(String s);
// 获取工厂实例:
static NumberFactory getFactory() {
return impl;
}
static NumberFactory impl = new NumberFactoryImpl();
}
在客户端中,我们只需要和工厂接口NumberFactory以及抽象产品Number打交道:
NumberFactory factory = NumberFactory.getFactory();
Number result = factory.parse("123.456");
调用方可以完全忽略真正的工厂NumberFactoryImpl和实际的产品BigDecimal,这样做的好处是允许创建产品的代码独立地变换,而不会影响到调用方。
有的人会问:一个简单的parse()需要写这么复杂的工厂吗?实际上大多数情况下我们并不需要抽象工厂,而是通过静态方法直接返回产品,即:
public class NumberFactory {
public static Number parse(String s) {
return new BigDecimal(s);
}
}
这种简化的使用静态方法创建产品的方式称为静态工厂方法(Static Factory Method)。静态工厂方法广泛地应用在Java标准库中.
例子:
Integer n = Integer.valueOf(100);
Integer既是产品又是静态工厂。它提供了静态方法valueOf()来创建Integer。那么这种方式和直接写new Integer(100)有何区别呢?我们观察valueOf()方法:
public final class Integer {
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
...
}
它的好处在于,valueOf()内部可能会使用new创建一个新的Integer实例,但也可能直接返回一个缓存的Integer实例。对于调用方来说,没必要知道Integer创建的细节。
工厂方法可以隐藏创建产品的细节,且不一定每次都会真正创建产品,完全可以返回缓存的产品,从而提升速度并减少内存消耗。
如果调用方直接使用Integer n = new Integer(100),那么就失去了使用缓存优化的可能性。
我们经常使用的另一个静态工厂方法是List.of():
List<String> list = List.of("A", "B", "C");
这个静态工厂方法接收可变参数,然后返回List接口。需要注意的是,调用方获取的产品总是List接口,而且并不关心它的实际类型。即使调用方知道List产品的实际类型是java.util.ImmutableCollections$ListN,也不要去强制转型为子类,因为静态工厂方法List.of()保证返回List,但也完全可以修改为返回java.util.ArrayList。这就是里氏替换原则:返回实现接口的任意子类都可以满足该方法的要求,且不影响调用方。
总是引用接口而非实现类,能允许变换子类而不影响调用方,即尽可能面向抽象编程。
小结
工厂方法是指定义工厂接口和产品接口,但如何创建实际工厂和实际产品被推迟到子类实现,从而使调用方只和抽象工厂与抽象产品打交道。
实际更常用的是更简单的静态工厂方法,它允许工厂内部对创建产品进行优化。
调用方尽量持有接口或抽象类,避免持有具体类型的子类,以便工厂方法能随时切换不同的子类返回,却不影响调用方代码。
2.抽象工厂:
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
抽象工厂模式和工厂方法不太一样,它要解决的问题比较复杂,不但工厂是抽象的,产品是抽象的,而且有多个产品需要创建,因此,这个抽象工厂会对应到多个实际工厂,每个实际工厂负责创建多个实际产品:
这种模式有点类似于多个供应商负责提供一系列类型的产品。我们举个例子:
假设希望为商家提供一个生产手机和PC的服务,定义接口如下:
public interface AbstractFactory {
//创建手机
phone createPhone();
//创建pc
pc createPc();
}
注意到上面的抽象工厂仅仅是一个接口,没有任何代码。同样的,因为phone和pc都比较复杂,现在我们并不知道如何实现它们,所以只有接口:
interface phone {
String phone();
}
interface pc{
String pc();
}
这样,我们就定义好了抽象工厂(AbstractFactory)以及两个抽象产品(phone和pc)。因为实现它们比较困难,我们决定让供应商来完成。
现在市场上有两家供应商:小米和苹果两个品牌,我们决定同时使用这两家供应商的产品.
我们先看看apple的产品是如何实现的。首先,apple必须要有实际的产品,即ApplePhone和ApplePc:
public class ApplePhone implements phone{
@Override
public String phone() {
return "生产了一个iphone";
}
}
class ApplePc implements pc{
@Override
public String pc() {
return "生产了一个Mac";
}
}
然后,apple必须提供一个实际的工厂来生产这两种产品,即AppleFactory:
public class AppleFactory implements AbstractFactory{
@Override
public phone createPhone() {
return new ApplePhone();
}
@Override
public pc createPc() {
return new ApplePc();
}
}
这样,我们就可以使用apple的服务了。客户端编写代码如下:
public class test {
public static void main(String[] args) {
//创建AbstractFactory ,实际是AppleFactory类型
AbstractFactory appleFactory = new AppleFactory();
phone iphone = appleFactory.createPhone();
System.out.println(iphone.phone());
pc pc = appleFactory.createPc();
System.out.println(pc.pc());
}
}
运行结果:
如果我们要同时使用Mi的服务怎么办?因为用了抽象工厂模式,Mi只需要根据我们定义的抽象工厂和抽象产品接口,实现自己的实际工厂和实际产品即可:
public class MiFactory implements AbstractFactory {
@Override
public phone createPhone() {
return new MiPhone();
}
@Override
public pc createPc() {
return new MiPc();
}
}
public class MiPhone implements phone{
@Override
public String phone() {
return "生产了一个MiPhone";
}
}
class MiPc implements pc{
@Override
public String pc() {
return "生产了一个MiPC";
}
}
客户端要使用mi的服务,只需要把原来的new AppleFactory()切换为new MiFactory()即可。
public class test {
public static void main(String[] args) {
AbstractFactory miFactory = new MiFactory();
phone Miphone = miFactory.createPhone();
System.out.println(Miphone.phone());
pc Mipc = miFactory.createPc();
System.out.println(Mipc.pc());
}
}
结果:
注意到客户端代码除了通过new创建了AppleFactory或MiFactory外,其余代码只引用了产品接口,并未引用任何实际产品(例如,ApplePhone ),如果把创建工厂的代码放到AbstractFactory中,就可以连实际工厂也屏蔽了:
public interface AbstractFactory {
//创建手机
phone createPhone();
//创建pc
pc createPc();
public static AbstractFactory createAbstractFactory(String name){
if (name.equalsIgnoreCase("apple")){
return new AppleFactory();
}else if (name.equalsIgnoreCase("mi")){
return new MiFactory();
}else{
throw new IllegalArgumentException("Invalid factory name");
}
}
}
客户端:
AbstractFactory apple = AbstractFactory.createAbstractFactory("apple");
phone applePhone = apple.createPhone();
pc applePc = apple.createPc();
System.out.println(applePhone.phone());
System.out.println(applePc.pc());
小结:抽象工厂模式是为了让创建工厂和一组产品与使用相分离,并可以随时切换到另一个工厂以及另一组产品;
抽象工厂模式实现的关键点是定义工厂接口和产品接口,但如何实现工厂与产品本身需要留给具体的子类实现,客户端只和抽象工厂与抽象产品打交道。
3.建造者:
定义
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可创建不同的表示。
使用场景
相同的方法,不同的执行顺序。
要初始化的对象十分复杂,如参数多且都具有默认值。
例子:
- 组装电脑的配置,组装电脑的配置
- 肯德基的套餐配置
- Java中的StringBuilder
- ps组合使用:工厂模式建造零件,建造者模式创建复杂对象)
优缺点
优点:
良好的封装性,可使调用者不必知道产品内部组成的细节。(控制细节风险)
建造者独立,容易扩展。
缺点:
会产生多余的Builder对象以及Director对象,消耗内存。
电脑组装例子:
首先创建实体
import lombok.Data;
@Data
public class Computer{
private String buildA;//内存
private String buildB;//显示屏
private String buildC;//CPU
private String buildD;//键鼠
@Override
public String toString() {
return "Computer{" +
"buildA='" + buildA + '\'' +
", buildB='" + buildB + '\'' +
", buildC='" + buildC + '\'' +
", buildD='" + buildD + '\'' +
'}';
}
}
创建动作父类(abstract抽象父类)
public abstract class Builder {
//无参构造,均设置好默认值
abstract void buildA();//内存
abstract void buildB();//显示屏
abstract void buildC();//CPU
abstract void buildD();//键鼠
//有参构造,DIY
abstract void buildA(String str);
abstract void buildB(String str);
abstract Computer getComputer();
}
写多个子类,重写父类方法:
public class Worker_Normal extends Builder{
private Computer computer;
public Worker_Normal() {
computer = new Computer();
}
@Override
void buildB() {
computer.setBuildB("1080p显示屏");
System.out.print("1080p显示屏==>");
}
@Override
void buildA() {
computer.setBuildA("1T内存");
System.out.print("1T内存==>");
}
@Override
void buildC() {
computer.setBuildC("i5");
System.out.print("i5==>");
}
@Override
void buildD() {
computer.setBuildD("联想键鼠");
System.out.print("联想键鼠==>");
}
@Override
void buildA(String str) {
computer.setBuildA(str);
}
@Override
void buildB(String str) {
computer.setBuildB(str);
}
@Override
Computer getComputer() {
return computer;
}
}
public class Worker_Good extends Builder{
private Computer computer;
public Worker_Good() {
computer = new Computer();
}
@Override
void buildA() {
computer.setBuildA("2T内存");
}
@Override
void buildB() {
computer.setBuildB("2K显示屏");
}
@Override
void buildC() {
computer.setBuildC("i9");
}
@Override
void buildD() {
computer.setBuildD("罗技键鼠");
}
@Override
void buildA(String str) {
computer.setBuildA(str);
}
@Override
void buildB(String str) {
computer.setBuildB(str);
}
@Override
Computer getComputer() {
return computer;
}
}
写Director,决定顺序和组件个数:
public class Director {
public Computer build(Builder builder){//director的作用很重要,决定具体顺序
builder.buildA();
builder.buildB();
builder.buildC();
builder.buildD();
return builder.getComputer();
}
}
public class DirectorPRO {
public Computer build(Builder builder){
builder.buildA();
builder.buildA();
builder.buildD();
builder.buildC();
builder.buildB();
return builder.getComputer();
}
}
最后写测试类
public class Test {
public static void main(String[] args) {
System.out.println("====标配高级电脑====");
Director director = new Director();
Worker_Good worker_good = new Worker_Good();
Computer buildGood = director.build(worker_good); //Alt+Enter自动补全
System.out.println(buildGood.toString() );
System.out.println("====DIY高级电脑====");//设置一些有参构造,可以DIY属性; 无参构造只能是默认属性
Computer buildGoodDIY = director.build(worker_good);
worker_good.buildA("DIY:4T内存");
worker_good.buildB("DIY:4K显示屏");
System.out.println(buildGoodDIY.toString() );
System.out.println("====普通电脑====");
for (int i=0;i<2;i++){
Computer buildNormal = director.build(new Worker_Normal());
System.out.println(buildNormal.toString());
}
System.out.println("====普通电脑PRO====");//这里用了不同的director可使构造的顺序不同(director决定顺序,还有组件数量)
DirectorPRO directorPRO = new DirectorPRO();
for(int i=0;i<1;i++){
Computer buildNormalPro = directorPRO.build(new Worker_Normal());
System.out.println(buildNormalPro);//这里应该是帮助自动toString
System.out.println(buildNormalPro.toString());
}
}
}
运行结果:
类图:
小结
Builder模式是为了创建一个复杂的对象,需要多个步骤完成创建,或者需要多个零件组装的场景,且创建过程中可以灵活调用不同的步骤或组件。
4.原型:
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。->(对象的克隆)
优点:
- 克隆对象会将对象已经设置的属性带出来,而不用在new之后去一个个重新设置。
- Java 自带的原型模式基于内存二进制流的复制,在性能上比直接 new 一个对象更加优良。
- 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
例子:
import lombok.*;
import java.util.Date;
//@Data // Lombok的@Data注解有侵入性,会重写hashcode,导致clone对象hash值一致
@AllArgsConstructor
@Setter
@Getter
public class Video implements Cloneable{ //implements Cloneable
private String name;
private Date createTime;
@Override
public String toString() { //手动增加打印hashcode
return "hashCode = " + super.hashCode() +
" Video{" +
" name='" + name + '\'' +
", createTime=" + createTime +
'}';
}
//Alt+Insert => Override =>clone()
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();//父类克隆
}
}
测试:
import java.util.Date;
public class Test {
public static void main(String[] args) throws CloneNotSupportedException, InterruptedException {
Date date = new Date();
Video video1 = new Video("【流浪地球】", date);
System.out.println("video1 = " + video1);
Video video2 = (Video) video1.clone(); //类型强制转换
System.out.println("video2 = " + video2);
video2.setName("【学习视频】");
System.out.println("video1 = " + video1);
System.out.println("video2 = " + video2);
}
}
结果:
分析:
clone出来的对象的hashCode和原对象是不同的;
修改属性后,两个对象就不同了,说明不是引用,而是真实复制了副本;
加入自定义类:
import lombok.*;
@AllArgsConstructor
@Getter
@Setter
public class ClonePeople implements Cloneable{
private String name;
private Integer height;
private Integer Intelligent;
private BaseInfo baseInfo;//自定义类
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "hashCode" + super.hashCode() +
" ClonePeople{" +
"name='" + name + '\'' +
", height=" + height +
", Intelligent=" + Intelligent +
", baseInfo=" + baseInfo +
'}';
}
}
写一个自定义类:
import lombok.*;
@Setter
@Getter
@AllArgsConstructor
@ToString
public class BaseInfo {
private String address;
private String parents;
}
测试:
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
ClonePeople clonePeople = new ClonePeople("双胞胎哥哥",195,155, new BaseInfo("地球","超人"));
//有参构造中,对象的值格式(value1 , value2) =>括号逗号
System.out.println("OriginPeople = " + clonePeople);//这里clonePeople等价于clonePeople.toString()
// System.out.println("clonePeople = " + clonePeople.toString());
ClonePeople cloneLittleBrother = (ClonePeople) clonePeople.clone();//纯克隆,只有hashCode改变
System.out.println("clone = " + cloneLittleBrother);
//设置新属性值
cloneLittleBrother.setName("双胞胎弟弟");
cloneLittleBrother.setIntelligent(175);
cloneLittleBrother.setBaseInfo(new BaseInfo("海王星","超人"));
System.out.println("======最终结果======");
System.out.println("cloneLittleBrother = " + cloneLittleBrother);
System.out.println("OriginPeople = " + clonePeople.toString());
}
}
结果:
5.单例:
单例模式:一个类只能构造一个实例对象("构造器私有")
场景:
-
Windows任务管理器、回收站
-
项目中,配置文件的类,一般只有一个对象
-
网站的计数器、时钟
-
数据库连接池
-
Servlet
-
Spring中的Bean(缓存中取bean很快,减少jvm垃圾回收)(当有请求来的时候会先从缓存(map)里查看有没有,有的话直接使用这个对象,没有的话才实例化一个新的对象)
a.饿汉式单例->上来直接new对象,所有类实例化。坏处是:大量浪费不必要的资源(因为很多类 不需要实例化)
public class Hungry {
private Hungry(){} //构造函数
private final static Hungry HUNGRY = new Hungry();//直接实例化,new出对象
public static Hungry getInstance(){
return HUNGRY;
}
}
b.懒汉式单例->懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。
线程不安全:多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题(Synchronized 同步锁来修饰 getInstance 方法)
public class Singleton {
private Singleton() {} //单例=》私有构造器
private static Singleton instance = null; //单例对象
public static Singleton getInstance() { //调用getInstance方法才会开始构造对象
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
但是这种方法不符合线程安全:
public class Singleton {
private Singleton() {
System.out.println(Thread.currentThread().getName()); //构造时候打印线程名
} //私有构造函数
private static Singleton instance = null; //单例对象
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public static void main(String[] args) {
for(int i =0;i<10;i++){
new Thread( ()->{Singleton.getInstance();} ).start(); //1.启动新线程 2.lambda表达式
}
}
}
可能会出现构造了很多个的情形
方法一:双重锁检测 DCL->==》首先将类加同步锁(syn),但是new语句不是原子操作,所以对了类的实例加volatile锁(可见性)
第一把锁:synchronized锁
public static Singleton getInstance() {
if (instance == null) { //外层判断
synchronized (Singleton.class) { //将Singleton类 加锁
if (instance == null) { //原有判断
instance = new Singleton(); // 上面这行 不是原子操作: // 1.分配内存空间 2.执行构造方法,初始化对象 3.把这个对象指向这个空间
}
}
}
return instance;
}//加上两层:一层if、一层锁
//这样就能保证单例在多线程下的唯一性
//双重检查模式:DCL (Double Check)
第二把锁:volatile锁
private volatile static Singleton instance = null; //由于instance = new Singleton()的非原子性(new对象3步),所以需要volatile保证强制同步一致
volatile关键字不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值。
但是可以用反射来破坏DCL:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Singleton instance = Singleton.getInstance();
Constructor<Singleton> declaredConstructor =Singleton.class.getDeclaredConstructor(null); //通过反射获取declaredConstructor这个构造器
declaredConstructor.setAccessible(true);
Singleton instance2 = declaredConstructor.newInstance();
System.out.println(instance.hashCode() );
System.out.println(instance2.hashCode() );
}
实现单例模式,三种方法对比:
方法二:用静态内部类
public class Holder {
private Holder(){}
public static Holder getInstance() {return InnerClass.HOLDER;}
private static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}
这个方法也能被反射破坏单例的唯一性
方法三:枚举->直接把类名前的class替换成enum就好了,因为枚举无法反射
public enum EnumSingleton { //这里是enum而不是class
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
测试:
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingleton instance1 = EnumSingleton.INSTANCE.getInstance();
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingleton instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
结果:
异常:枚举无法创造反射.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~