设计模式首先分成三类,这一篇主要讲创建型模式中的5种,创建型模式的作用就是为了产生实例对象,基于面向对象设计,将对象的创建与使用分离,降低系统的耦合度
单例模式
特点:
- 单例类只有一个实例对象,减少内存开销
- 该单例对象必须由单例类自行创建
- 单例类对外提供一个访问该单例的全局访问点
饿汉
public class HungrySingleton {
// 创建私有静态实例,意味着这个类第一次使用的时候就会进行创建
private static final HungrySingleton instance = new HungrySingleton();
// private 避免类在外部被实例化
private HungrySingleton() {
}
//饿汉直接返回该实例就行了
public static HungrySingleton getInstance() {
return instance;
}
// 瞎写一个静态方法。只要调用,就会生成 Singleton实例
public static Date getDate(String mode) {return new Date();}
}
缺点是只要使用该类中的其他静态方法,就已经自动实例化该类了,不过基本不会有人蠢到给单例类中塞个没啥用的静态方法
懒汉
public class LazySingleton {
// volatile保证其可见性
private static volatile LazySingleton instance = null;
// private 避免类在外部被实例化
private LazySingleton() {
}
// 懒汉会有线程不安全的问题,上锁是关键
public static LazySingleton getInstance() {
if (instance == null) {
// 加锁
synchronized (LazySingleton.class) {
// 这一次判断也是必须的,不然会有并发问题
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
// //不建议直接方法加锁
// public static synchronized LazySingleton getInstance() {
// //getInstance 方法前加同步
// if (instance == null) {
// instance = new LazySingleton();
// }
// return instance;
// }
}
缺点就是锁带来的性能问题,不过也可以忽略不计
最后说一下枚举,枚举很特殊,它在类加载的时候会初始化里面的所有的实例,而且 JVM 保证了它们不会再被实例化,所以它天生就是单例的。有些java源码中会用枚举来实现单例。
有文章分析说枚举实现单例是最佳方法,详细自行百度,这里只重点掌握饿汉和懒汉,还有很多其他的单例实现方法
建造者模式
class User {
// 下面是“一堆”的属性
private String name;
private String password;
private String nickName;
private int age;
// 构造方法私有化,不然客户端就会直接调用构造方法了
private User(String name, String password, String nickName, int age) {
this.name = name;
this.password = password;
this.nickName = nickName;
this.age = age;
}
// 静态方法,用于生成一个 Builder,这个不一定要有,不过写这个方法是一个很好的习惯,
// 有些代码要求别人写 new User.UserBuilder().a()...build() 看上去就没那么好
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder {
// 下面是和 User 一模一样的一堆属性
private String name;
private String password;
private String nickName;
private int age;
private UserBuilder() {
}
// 链式调用设置各个属性值,返回 this,即 UserBuilder
public UserBuilder name(String name) {
this.name = name;
return this;
}
public UserBuilder password(String password) {
this.password = password;
return this;
}
public UserBuilder nickName(String nickName) {
this.nickName = nickName;
return this;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
// build() 方法负责将 UserBuilder 中设置好的属性“复制”到 User 中。
// 当然,可以在 “复制” 之前做点检验
public User build() {
if (name == null || password == null) {
throw new RuntimeException("用户名和密码必填");
}
if (age <= 0 || age >= 150) {
throw new RuntimeException("年龄不合法");
}
// 还可以做赋予”默认值“的功能
if (nickName == null) {
nickName = name;
}
return new User(name, password, nickName, age);
}
}
}
其实就是在Builder类中将setter方法赋值后return this
不过可以看到做必填校验的时候用建造者是很不错的,这样就不用在外部判空了,缺点是要写很多Builder类和build方法,不过可以用lombok,简洁优雅
使用@Builder注解
使用建造者模式,特点是链式写法
原型模式
原型模式的特点是基于当前类实例产生新的实例,所以用到了克隆
Object 类中有一个 clone() 方法,我们用他来生成新的对象,同时我们的类必须先实现 Cloneable 接口
该接口无任何内容,只是作为标记,如果不实现,则会抛出CloneNotSupportedException
异常
protected Object clone() throws CloneNotSupportedException {
if (!(this instanceof Cloneable)) {
throw new CloneNotSupportedException("Class " + getClass().getName() +
" doesn't implement Cloneable");
}
return internalClone();
}
这里要说的是 java 的克隆是浅克隆,碰到对象引用的时候,克隆出来的对象和原对象中的引用将指向同一个对象。通常实现深克隆的方法是将对象进行序列化,然后再进行反序列化。
可以看我之前写的文章:
Java对象拷贝复习,深/浅拷贝及深拷贝的三种实现方式
java克隆/拷贝,类型转换
//具体原型类
@Data
public class Realizetype implements Cloneable {
private String name;
Realizetype() {
System.out.println("具体原型创建成功!");
}
public Object clone() throws CloneNotSupportedException {
System.out.println("具体原型复制成功!");
return super.clone();
}
}
//原型模式的测试类
public class PrototypeTest {
public static void main(String[] args) throws CloneNotSupportedException {
Realizetype obj1 = new Realizetype();
obj1.setName("111");
Realizetype obj2 = (Realizetype) obj1.clone();
obj2.setName("222");
System.out.println(obj1.getName());
System.out.println(obj2.getName());
System.out.println("obj1==obj2?" + (obj1 == obj2));
}
}
obj1不等于obj2
工厂方法
工厂模式是循序渐进的,需要按照简单工厂-工厂方法-抽象工厂的顺序去理解
先介绍下简单工厂
public class Client {
//抽象产品
public interface Product {
void show();
}
//具体产品:Product1
static class Product1 implements Product {
public void show() {
System.out.println("具体产品1显示...");
}
}
//具体产品:Product2
static class Product2 implements Product {
public void show() {
System.out.println("具体产品2显示...");
}
}
final class Const {
static final int PRODUCT_A = 0;
static final int PRODUCT_B = 1;
static final int PRODUCT_C = 2;
}
static class SimpleFactory {
public static Product makeProduct(int kind) {
switch (kind) {
case Const.PRODUCT_A:
return new Product1();
case Const.PRODUCT_B:
return new Product2();
}
return null;
}
}
}
其实就是一个工厂类 XxxFactory,里面有一个静态方法,根据我们不同的参数,返回不同的派生自同一个父类(或实现同一接口)的实例对象。
这里要强调职责单一原则,一个类只提供一种功能,即FoodFactory只产出FoodA,FoodB,ProductFactory只产出ProductA,ProductB
而工厂模式则需要使用两个或两个以上的工厂,因此我们创建一个工厂类的接口,来实现不同的工厂,单个工厂类就用简单工厂模式就好了
interface AbstractFactory {
public Product newProduct();
}
然后先创建两个工厂
class Factory1 implements AbstractFactory {
public Product newProduct() {
System.out.println("工厂1生成产品1");
return new Product1();
}
}
class Factory2 implements AbstractFactory {
public Product newProduct() {
System.out.println("工厂2生成产品2");
return new Product2();
}
}
main方法测试一下
public static void main(String[] args) {
try {
Product a;
AbstractFactory af;
af = (AbstractFactory) ReadXML1.getObject();
a = af.newProduct();
a.show();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
这里使用xml配置文件中写类名的方式来返回类
import javax.xml.parsers.*;
import org.w3c.dom.*;
import java.io.*;
class ReadXML1 {
//该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
public static Object getObject() {
try {
//创建文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("src/FactoryMethod/config1.xml"));
//获取包含类名的文本节点
NodeList nl = doc.getElementsByTagName("className");
Node classNode = nl.item(0).getFirstChild();
String cName = "类所在的当前包名." + classNode.getNodeValue();
//System.out.println("新类名:"+cName);
//通过类名生成实例对象并将其返回
Class<?> c = Class.forName(cName);
Object obj = c.newInstance();
return obj;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
xml如下,作为配置文件传参,传个Factroy1
总结:一个工厂接口,需要2个以上的工厂实现,每个工厂生产自己拥有的多个不同产品
抽象工厂
最后介绍抽象工厂,在没使用抽象工厂前,使用工厂方法实现如下
// 得到 Intel 的 CPU
CPUFactory intelCPUFactory = new IntelCPUFactory();
CPU cpu = intelCPUFactory.makeCPU();
// 得到 AMD 的主板
MainBoardFactory mainBoardFactory = new AmdMainBoardFactory();
MainBoard mainBoard = mainBoardFactory.makeMainBoard();
// 组装 CPU 和主板
Computer computer = new Computer(cpu, mainBoard);
上面是得到硬件去组装cpu的过程,但实际情况会存在硬件兼容问题,这时候需要引入产品族的概念
抽象工厂则是将cpu作为工厂类,硬件类在内部实例化
public static void main(String[] args) {
// 第一步就要选定一个“大厂”
ComputerFactory cf = new AmdFactory();
// 从这个大厂造 CPU
CPU cpu = cf.makeCPU();
// 从这个大厂造主板
MainBoard board = cf.makeMainBoard();
// 从这个大厂造硬盘
HardDisk hardDisk = cf.makeHardDisk();
// 将同一个厂子出来的 CPU、主板、硬盘组装在一起
Computer result = new Computer(cpu, board, hardDisk);
}
对比发现:
- 工厂方法是各生产各的,cpu/主板等硬件是由各自的工厂生产
- 抽象工厂是成套式组装的,cpu/主板等硬件全在一个工厂里完成
个人理解产品族概念:
- 其实可以理解成套装/套餐,假如一个套餐内包含主食+饮料,a套餐里包含三明治+可乐,b套餐包含汉堡+果汁
- 工厂方法是将主食,饮料作为工厂,生产好后再组合成套餐
- 抽象工厂是将a,b套餐作为工厂,每个套餐生产各自的主食和饮料
最后总结下工厂模式(简单工厂,工厂方法,抽象工厂)
简单工厂:
- 优点: 工厂和产品的职责区分明确;只需简单传参就能生产需要的产品;可以引入配置文件,在不修改代码的情况下更换和添加新的具体产品类
- 缺点:职责单一,只能提供简单的功能;方法使用static,工厂无法继承
- 使用场景:产品种类相对较少,一个工厂类就能解决的场景下
工厂方法:
- 优点:灵活性强,创建新产品,只需多写相应的工厂类;简单工厂的增强
- 缺点:相比简单工厂增加了复杂度;产品之间不兼容
- 使用场景:需要使用两个或以上工厂时
抽象工厂:
- 优点:管理产品族方便;增强了程序的可扩展性,对扩展开放
- 缺点:再加一个产品时需要修改大量代码,对修改关闭
- 使用场景:套餐,产品不兼容等情况下