- 参考资料
- 一、工厂方法设计模式
- 二、抽象工厂模式
- 三、建造者模式
- 四、单例模式
- 4.1 饿汉式
- 4.2 懒汉式
- 4.3 懒汉式的多线程定义
- 4.3.1 synchronized 同步代码块
- 4.3.2 synchronized 同步创建实例
- 4.3.3【最优策略】静态内部类实现懒汉式
- 五、原型模式
- 5.1 对象的浅拷贝和深拷贝
- 5.2 内部属性的深拷贝
视频资料:查看视频
在使用设计模式前,我们先用常见的方法来实现封装水果类,以及水果的继承类苹果和橘子。
- 定义一个水果抽象类
public abstract class Fruit { // 水果抽象类
private final String name;
public Fruit(String name){
this.name = name;
}
@Override
public String toString() {
return name + "@" + hashCode(); // 打印当前水果名称和 哈希值
}
}
- 定义实现的子类
// 苹果
class Apple extends Fruit{
public Apple(){
super("苹果");
}
}
// 橘子
class Origin extends Fruit{
public Origin(){
super("橘子");
}
}
- 运行测试
public class Demo {
public static void main(String[] args) {
System.out.println(new Apple());
System.out.println(new Origin());
}
}
// 输出结果
// 苹果@685325104
// 橘子@460141958
接下来我们通过一些设计模式来改写上面的例子,以便体会不同设计模式的特点。
一、工厂方法设计模式简单的工厂模式
基于之前的例子,我们创建一个工厂类,依此来获取创建实例
这里需注意获取实例的方法是static静态方法
public class FruitFactory {
public static Fruit getFruit(String type){
switch (type){
case "苹果":
return new Apple();
case "橘子":
return new Origin();
default:
return null;
}
}
}
通过简单工厂创建对象:
public class Demo {
public static void main(String[] args) {
Fruit apple = FruitFactory.getFruit("苹果");
System.out.println(apple);
}
}
// 输出结果
// 苹果@685325104
这种简单的工厂模式带来的弊端是:
-
不符合面向对象的开闭原则 OCP,即对扩展开发,对修改关闭。当有新的Fruit子类时,需要改写getFruit的方法
-
getFruit需要进行字符串的判断,比较鸡肋
优化: 使用抽象类+泛型,将水果工厂类进一步划分成具体的水果工厂类
public abstract class FruitFactory{ public abstract T getFruit(); } class AppleFactory extends FruitFactory { @Override public Apple getFruit() { return new Apple(); } } class OriginFactory extends FruitFactory { @Override public Origin getFruit() { return new Origin(); } }
public class Demo {
public static void main(String[] args) {
System.out.println(new AppleFactory().getFruit());
System.out.println(new OriginFactory().getFruit());
}
}
// 输出结果
// 苹果@685325104
// 橘子@460141958
二、抽象工厂模式
之前介绍的工厂方法模式,通过定义顶层抽象工厂类,以及继承的方式,针对每一种水果产品都提供一个工厂类进行创建,这里没有涉及更复杂的概念,比如工厂不仅生产水果,而且还生产饮料之类的。
这里我们可以定义水果、饮品类,然后抽象到一个工厂里,按品牌分类。
- 定义表示水果的类,和之前一样
public abstract class Fruit { // 水果抽象类
private final String name;
public Fruit(String name){
this.name = name;
}
@Override
public String toString() {
return name + "@" + hashCode(); // 打印当前水果名称和 哈希值
}
}
// 苹果
class Apple extends Fruit{
public Apple(){
super("苹果");
}
}
// 橘子
class Origin extends Fruit{
public Origin(){
super("橘子");
}
}
- 定义表示饮品的类,和之前表示水果类似
abstract class Drink { // 饮品抽象类
private final String name;
public Drink(String name){
this.name = name;
}
@Override
public String toString() {
return name + "@" + hashCode(); // 打印当前水果名称和 哈希值
}
}
// 苹果汁
class AppleDrink extends Drink{
public AppleDrink(){
super("苹果汁");
}
}
// 橘子汁
class OriginDrink extends Drink{
public OriginDrink(){
super("橘子汁");
}
}
- 定义抽象工厂类
abstract class AbstractFactory{
public abstract Fruit getFruit();
public abstract Drink getDrink();
}
class FactoryA extends AbstractFactory{
@Override
public Fruit getFruit() {
return new Apple();
}
@Override
public Drink getDrink() {
return new AppleDrink();
}
}
class FactoryB extends AbstractFactory{
@Override
public Fruit getFruit() {
return new Origin();
}
@Override
public Drink getDrink() {
return new OriginDrink();
}
}
- 测试
public class Demo {
public static void main(String[] args) {
FactoryA factoryA = new FactoryA();
FactoryB factoryB = new FactoryB();
System.out.println("品牌A-> 水果: "+ factoryA.getFruit() +
", 饮品: " + factoryA.getDrink());
System.out.println("品牌B-> 水果: "+ factoryB.getFruit() +
", 饮品: " + factoryB.getDrink());
}
}
// 运行结果
// 品牌A-> 水果: 苹果@685325104, 饮品: 苹果汁@460141958
// 品牌B-> 水果: 橘子@1163157884, 饮品: 橘子汁@1956725890
优点: 比工厂方法模式更清晰,能处理更加复杂的情况
缺点: 不得不为每一个产品组的工厂都去添加新产品的生产方法,违背了开闭原则。
三、建造者模式许多框架里用到的一种设计模式,JDK8本身有用的一种设计模式,比如 StringBuilder
这里我们先创建一个StringBuilder对象,然后就可以链式的添加元素。
public class Demo {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder();
builder.append("Hello, ").append("World!");
System.out.println(builder);
}
}
接下来先定义一个学生实体类,然后通过之前的方法来创建学生实例。
import java.util.List;
public class Student {
private String name;
private int age;
private List hobbies;
public Student(){}
public Student(String name, int age, List hobbies) {
this.name = name;
this.age = age;
this.hobbies = hobbies;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public List getHobbies() {
return hobbies;
}
public void setHobbies(List hobbies) {
this.hobbies = hobbies;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + ''' +
", age=" + age +
", hobbies=" + hobbies +
'}';
}
}
在创建学生实例的时候,主要有两种情况:
- 创建具有所有属性的学生对象
- 创建只有部分属性的学生对象
import java.util.ArrayList;
import java.util.Arrays;
public class Demo {
public static void main(String[] args) {
// 创建所有属性
Student a = new Student("uni", 22, Arrays.asList("吃饭", "睡觉"));
// 创建部分属性
Student b = new Student();
b.setName("uni");
b.setAge(22);
System.out.println("a: " + a);
System.out.println("b: " + b);
}
}
// 运行结果
// a: Student{name='uni', age=22, hobbies=[吃饭, 睡觉]}
// b: Student{name='uni', age=22, hobbies=null}
可以看到无论是所有属性还是部分属性,都不太方便。
- 所有属性构造,需要通过构造函数,填入对应的参数,参数位置不能改变。
- 部分属性,需要先构造空的对象,然后逐一的setXXX,这样使得代码看起来十分不美观,最好的策略就是在创建对象的时候,就直接能将我们需要填入的参数赋值给这个对象。接下来我们通过建造者模式来解决这个问题。
这里由于 getter、setter方法比较多,有些冗余,所以使用 lombok 的注解来简化代码。
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@Setter
@ToString
public class Student {
private String name;
private int age;
private List hobbies;
public Student(){}
private Student(String name, int age, List hobbies) {
this.name = name;
this.age = age;
this.hobbies = hobbies;
}
public static StudentBuilder builder(){
return new StudentBuilder();
}
@Getter
public static class StudentBuilder{
private String name;
private int age;
private List hobbies;
public StudentBuilder name(String name) {
this.name = name;
return this;
}
public StudentBuilder age(int age) {
this.age = age;
return this;
}
public StudentBuilder hobbies(List hobbies) {
this.hobbies = hobbies;
return this;
}
public Student build(){
return new Student(name, age, hobbies);
}
}
}
通过上述代码可以看到,Student的全参构造设置为了 private 私有级别,这使得在创建Student实例时只能通过其内部的静态类 StudentBuilder,该内部类存储了 Student对象的所有属性,方便在构造的时候进行赋值。
静态内部类 StudentBuilder提供返回实例对象的set方法,以及 build 方法返回一个Student对象,这里的build方法则固定了参数的位置,所以在调用的时候无需关系参数的顺序。
import java.util.Arrays;
public class Demo {
public static void main(String[] args) {
// 创建所有属性
Student a = new Student.StudentBuilder()
.name("uni")
.age(22)
.hobbies(Arrays.asList("吃饭", "睡觉"))
.build();
// 创建部分属性
Student b = new Student.StudentBuilder()
.name("uni")
.age(22)
.build();
System.out.println("a: " + a);
System.out.println("b: " + b);
}
}
// 运行结果
// a: Student(name=uni, age=22, hobbies=[吃饭, 睡觉])
// b: Student(name=uni, age=22, hobbies=null)
四、单例模式
单例模式是指只有一个实例对象,同一个类始终只会有一个对象来进行操作,比如数据库连接类,这种情况下只需要创建一个对象或者直接使用静态方法,不需要创建多个对象。
单例模式通常分为两种,饿汉式和懒汉式。
public class Singleton {
// 饿汉式: 定义静态对象,并创建实例
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
创建饿汉式的实例:
class Demo{
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
}
}
// 运行结果
// Singleton@4554617c
4.2 懒汉式
public class Singleton {
// 懒汉式: 先定义静态对象,但不创建实例
private static Singleton instance;
private Singleton(){}; // 构造方法私有化
public static Singleton getInstance(){
if(instance == null)
instance = new Singleton();
return instance;
}
}
4.3 懒汉式的多线程定义
之前的定义是对于单线程情景的,如果我们是使用多线程来创建实例,那么就需要考虑并发带来的问题。假如同时有两个线程判断了 if(instance == null) ,那么 instance就将被这两个线程实例化,这样依赖就实例化了两次,在高并发场景,有多个线程,那么instance可能会被实例化很多次。
比如:我们使用三个线程来创建实例,如果对三个线程分别设置 1s 、2s 、3s 的延迟,达到延迟后就创建懒汉式的实例,这种情况下,Singleton被实例化了一次。
class Demo{
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(1000);
Singleton singleton = Singleton.getInstance();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread thread2 = new Thread(() -> {
try{
Thread.sleep(2000);
Singleton singleton = Singleton.getInstance();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread thread3 = new Thread(() -> {
try{
Thread.sleep(3000);
Singleton singleton = Singleton.getInstance();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread1.start();
thread2.start();
thread3.start();
}
}
但我们将线程的延迟时间都调整到100s,这时候Singleton就被每一个线程实例化了一次,即共实例化了三次。
Thread.sleep(100);
所以我们需要用锁来预防这种情况,防止对象被多次实例化,浪费资源。
4.3.1 synchronized 同步代码块通过添加 synchronized 关键字同步创建实例
public class Singleton {
private static Singleton instance;
private Singleton(){}; // 构造方法私有化
public static synchronized Singleton getInstance(){
if(instance == null) {
System.out.println("Singleton被实例化了.");
instance = new Singleton();
}
return instance;
}
}
给getInstance()方法添加synchronized关键字之后,多个线程就无法并发的执行这个方法了,这样就有效地避免了多次创建实例的情况,这里可以继续使用之前的多线程代码进行测试,可以将3个线程sleep的时间设置为相同的100,然后同时启动3个线程,结果肯定是只实例化了一次。
这种方式是最简单粗暴的,直接锁整个方法,但是也有一个缺点那就是效率比较低,因为在其他线程执行到这个方法的时候,会等待锁释放,然后才执行方法,这意味着getInstance() 方法还是被其他的线程执行了,只是它们没有创建实例而已。
4.3.2 synchronized 同步创建实例根据之前的问题,synchronized 同步整个方法,会导致其他线程等待锁的释放,同时还会执行这个方法,比较浪费资源,这里将synchronized 定义到判断实例为空的内部,相当于只锁住创建实例的部分,因为执行时间更短,故性能会稍微高一些。
public class Singleton {
private static Singleton instance;
private Singleton(){}; // 构造方法私有化
public static Singleton getInstance(){
if(instance == null) {
synchronized(Singleton.class){
instance = new Singleton();
System.out.println("Singleton被实例化了.");
}
}
return instance;
}
}
不过这里依然有之前的问题,多个线程在判断 instance == null 成立后,有可能同时进入同步代码块,这就跟之前一样,出现了创建多次实例的情况,这里的优化思路是在同步代码块里再进行一次判断,这样避免重复创建实例。
public class Singleton {
private static Singleton instance;
private Singleton(){}; // 构造方法私有化
public static Singleton getInstance(){
if(instance == null) {
synchronized(Singleton.class){
if(instance == null) {
instance = new Singleton();
System.out.println("Singleton被实例化了.");
}
}
}
return instance;
}
}
最后这里还可以进行优化,那就是将 instance 加一个关键字 volatile,这个关键字可保证 instance 在多个线程之间可见,这样当其他线程进入同步代码块后,会拿其他线程更新的最新值去判断(这里理解不是很深入,大概先这么认为)
private static volatile Singleton instance;4.3.3【最优策略】静态内部类实现懒汉式
这里是根据Java类的加载特性,静态内部类在调用之前是不会进行初始化的,这样我们只要在静态内部类里先初始化一个静态实例,之后在获取的时候直接运行然后返回,这样就保证获取的实例是同一个而且仅有一个了。
public class Singleton {
private Singleton(){} // 私有化无参构造
private static class Holder{ // 静态内部类
// 根据类加载特性, 仅使用Singleton类时,不会对静态内部类进行初始化
private final static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return Holder.INSTANCE; // 直接获取内部类的静态实例
}
}
接下来我们通过一个简单的多线程Demo来验证这种懒汉式的正确性:
class Demo{
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(100);
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread thread2 = new Thread(() -> {
try{
Thread.sleep(100);
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread1.start();
thread2.start();
}
}
// 运行结果
// Singleton@2b139240
// Singleton@2b139240
在多线程中获取实例,然后输出其哈希的部分值,这样可以看到输出的哈希结果是一样的,所以指向的就是同一个对象实例。
而且没有任何加锁的操作,同时也保证了线程的安全。
原型模式实际上与对象拷贝息息相关,原型模式使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。
即,原型对象作为模板,通过克隆操作,来产生更多的对象,就像细胞复制。
- 浅拷贝
- 对于类中的基本数据类型,赋值时会直接复制值给拷贝的对象;
- 对于引用类型,只会复制对象的地址,而实际上还是原来的那个对象,相当于变脸术。
public class Copy {
public static void main(String[] args) {
int a = 10;
int b = a; // 基本类型 浅拷贝
System.out.println(a == b); // 结果为: true
Object obja = new Object();
Object objb = obja; // 引用类型 浅拷贝引用
System.out.println(obja == objb); // 结果为: true
}
}
- 深拷贝
- 无论是基本类型还是引用类型,深拷贝会将引用类型的所有内容,全部拷贝为一个新的对象,包括对象内部的所有成员变量,也进行拷贝。
在 Java 中,我们可以通过 Cloneable 接口提供的拷贝机制,来实现原型模式:
public class Student implements Cloneable{ // 必须实现 Cloneable 接口
@Override
public Student clone() {
try {
return (Student) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
测试看看克隆的对象是否为原来的对象:
class Demo{
public static void main(String[] args) {
Student a = new Student();
Student b = a.clone();
System.out.println(a);
System.out.println(b);
}
}
// 运行结果
// Student@4554617c
// Student@74a14482
可以看到,输出的哈希值不同,所以是不同的对象,克隆成功,但不能确认这就是深拷贝,因为深拷贝要保证其内部的引用类型都各不相同。5.2 内部属性的深拷贝
基于上一个例子,我们在 Student 类里定义引用类型 String,然后通过克隆创建不同的Student 实例,对它们的 Name 进行比较,看看是否为同一个 String。
public class Student implements Cloneable{
private String name;
public Student(String name){
this.name = name;
}
public String getName(){
return name;
}
@Override
public Student clone() {
try {
return (Student) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
查看内部属性是否克隆成功
class Demo{
public static void main(String[] args) {
Student a = new Student("a");
Student b = a.clone();
System.out.println(a.getName() == b.getName()); // 结果为: true
}
}
由于返回的结果为true,所以内部属性Name克隆失败了,引用类型还是之前的克隆对象。
所以Java提供的clone方法实际上是一种浅拷贝,其内部的引用对象并不会进行拷贝。
要实现真正的深拷贝,在clone()方法里,我们需要拷贝每一个引用类型,这里需要拷贝 name。
@Override
public Student clone() {
try {
Student student = (Student) super.clone();
student.name = new String(name);
return student;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}



