// 基本类
class Shape {
public void draw() { /* ... */ }
}
// 扩展新形状而不修改Shape类
class Circle extends Shape {
public void draw() { /* 绘制圆形 */ }
}
class Bird {
public void fly() { /* ... */ }
}
class Sparrow extends Bird { /* 保持fly方法行为一致 */ }
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}
// Penguin类违反了LSP原则,因为它改变了fly方法的行为。
// 抽象接口
interface Repository {
void save(Object data);
}
// 高层模块
class Service {
private Repository repository;
public Service(Repository repository) {
this.repository = repository;
}
public void process(Object data) {
repository.save(data);
}
}
// 低层模块实现接口
class SQLRepository implements Repository {
public void save(Object data) { /* SQL存储逻辑 */ }
}
// 不好的设计:一个庞大的接口
interface Worker {
void work();
void eat();
}
// 好的设计:接口拆分
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Human implements Workable, Eatable {
public void work() { /* ... */ }
public void eat() { /* ... */ }
}
// 不好的设计:两个职责
class User {
public void manageUserInfo() { /* ... */ }
public void trackUserActions() { /* ... */ }
}
// 好的设计:拆分职责
class UserInfoManager {
public void manageUserInfo() { /* ... */ }
}
class UserActionTracker {
public void trackUserActions() { /* ... */ }
}
// 不好的设计:通过继承实现复用
class Stack extends ArrayList { /* ... */ }
// 好的设计:通过合成实现复用
class Stack {
private ArrayList list = new ArrayList();
public void push(Object item) { list.add(item); }
// 其他方法...
}
// 不好的设计:过长的调用链
class A {
B getB() { /* ... */ }
}
class B {
C getC() { /* ... */ }
}
class C {
void doSomething() { /* ... */ }
}
// 在外部调用
new A().getB().getC().doSomething();
// 好的设计:减少调用链
class A {
B b;
void doSomething() {
b.doSomething(); // A 只与直接朋友B通信
}
}
class B {
C c;
void doSomething() {
c.doSomething(); // B 只与直接朋友C通信
}
}
定义
简单工厂模式(又称为静态工厂模式)不是一种正式的设计模式,而是一种编程习惯。在这种模式中,一个工厂类负责创建其他类的实例,这些类通常都有一个共同的父类或接口。也就是我们可以使用这个工厂去创建各种类型的实例(如果你愿意的话)
优点
缺点
使用了什么原则
违背了什么原则
简单工厂模式解决的问题
简单工厂模式主要解决了客户端与产品类的紧密耦合问题,使客户端从直接创建对象转变为通过工厂类来创建,从而简化了客户端的代码和逻辑。
简单工厂模式通过一个中心化的工厂类来决定创建哪种类型的对象,客户端通过传递参数来指定所需对象的类型。这种模式适用于产品种类不多且产品创建逻辑不复杂的情况。它的优点在于减少了客户端和具体产品类之间的耦合,简化了客户端的使用。然而,这种模式的缺点是当新产品被添加到系统中时,需要修改工厂类,违背了开闭原则,并使得工厂类职责过重。
假设我们有一个Product接口和几个实现类ProductA、ProductB等。工厂类根据传入的类名来创建相应的对象。
interface Product {
void use();
}
class ProductA implements Product {
public void use() { System.out.println("Using Product A"); }
}
class ProductB implements Product {
public void use() { System.out.println("Using Product B"); }
}
class SimpleFactory {
public static Product createProduct(String type) {
if ("A".equals(type)) {
return new ProductA();
} else if ("B".equals(type)) {
return new ProductB();
}
throw new IllegalArgumentException("Unknown product type");
}
}
这种方式可以减少工厂类中的if-else语句,使得代码更加简洁。
class ReflectiveSimpleFactory {
public static Product createProduct(String className) {
try {
Class> clazz = Class.forName(className);
return (Product) clazz.newInstance();
} catch (Exception e) {
throw new IllegalArgumentException("Unknown class name");
}
}
}
在使用时,可以这样调用:
Product productA = ReflectiveSimpleFactory.createProduct("ProductA");
productA.use();
Product productB = ReflectiveSimpleFactory.createProduct("ProductB");
productB.use();
// Product接口及其实现
interface Product {
void use();
}
class ProductA implements Product {
public void use() { System.out.println("Using Product A"); }
}
class ProductB implements Product {
public void use() { System.out.println("Using Product B"); }
}
// 基于Class对象的工厂类
class ClassBasedSimpleFactory {
public static Product createProduct(Class extends Product> productClass) {
try {
return productClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Creation of product failed", e);
}
}
}
Product productA = ClassBasedSimpleFactory.createProduct(ProductA.class);
productA.use();
Product productB = ClassBasedSimpleFactory.createProduct(ProductB.class);
productB.use();
定义
工厂方法模式是一种创建型设计模式,它定义了一个用于创建对象的接口,但让子类决定要实例化哪个类。工厂方法使一个类的实例化延迟到其子类。
优点
缺点
使用了什么原则
违背了什么原则
工厂方法模式解决了简单工厂模式的什么问题
工厂方法模式中,每种产品有一个与之对应的工厂类。这种模式允许系统在不直接依赖具体产品类的情况下,通过调用对应的工厂来创建产品对象。它适用于产品种类较多且每种产品的创建逻辑相对独立的情况。其优点是良好地遵循了开闭原则,易于扩展新产品。缺点是随着产品种类的增加,需要增加越来越多的工厂类,增加了系统的复杂度。
假设我们有一个Product接口,多个产品实现这个接口,每个产品有对应的工厂类。
// Product接口
interface Product {
void use();
}
// 具体产品类
class ConcreteProductA implements Product {
public void use() { System.out.println("Using Product A"); }
}
class ConcreteProductB implements Product {
public void use() { System.out.println("Using Product B"); }
}
// 抽象工厂接口
interface ProductFactory {
Product createProduct();
}
// 具体工厂类
class ConcreteFactoryA implements ProductFactory {
public Product createProduct() {
return new ConcreteProductA();
}
}
class ConcreteFactoryB implements ProductFactory {
public Product createProduct() {
return new ConcreteProductB();
}
}
使用工厂方法模式:
public class FactoryMethodDemo {
public static void main(String[] args) {
ProductFactory factoryA = new ConcreteFactoryA();
Product productA = factoryA.createProduct();
productA.use();
ProductFactory factoryB = new ConcreteFactoryB();
Product productB = factoryB.createProduct();
productB.use();
}
}
定义
抽象工厂模式是一种创建型设计模式,它提供了一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。抽象工厂模式通常涉及多个工厂方法,用于创建一系列的产品。
优点
缺点
使用了什么原则
违背了什么原则
抽象工厂模式解决了什么问题
抽象工厂模式提供了一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。适用于产品族的概念,即一组产品需要一起使用,并且可能需要增加新的产品族。其优点是能够保证客户端始终使用同一产品族中的对象。缺点是难以支持新种类的(单一的)产品,因为抽象工厂接口确定了可以创建的产品集合,增加新种类的产品需要修改接口以及所有实现了该接口的类。
假设我们有一个产品族,包含两种产品:ProductA和ProductB。每种产品有不同的变体。
这里的产品族的意思是,一系列相关的产品,整合到一起使得他们有关联性。
// Product接口及其变体
interface ProductA {
void useA();
}
interface ProductB {
void useB();
}
class ProductA1 implements ProductA {
public void useA() { System.out.println("Using Product A1"); }
}
class ProductA2 implements ProductA {
public void useA() { System.out.println("Using Product A2"); }
}
class ProductB1 implements ProductB {
public void useB() { System.out.println("Using Product B1"); }
}
class ProductB2 implements ProductB {
public void useB() { System.out.println("Using Product B2"); }
}
// 抽象工厂接口
interface AbstractFactory {
ProductA createProductA();
ProductB createProductB();
}
// 具体工厂实现
class ConcreteFactory1 implements AbstractFactory {
public ProductA createProductA() {
return new ProductA1();
}
public ProductB createProductB() {
return new ProductB1();
}
}
class ConcreteFactory2 implements AbstractFactory {
public ProductA createProductA() {
return new ProductA2();
}
public ProductB createProductB() {
return new ProductB2();
}
}
使用抽象工厂模式:
public class AbstractFactoryDemo {
public static void main(String[] args) {
AbstractFactory factory1 = new ConcreteFactory1();
ProductA productA1 = factory1.createProductA();
ProductB productB1 = factory1.createProductB();
productA1.useA();
productB1.useB();
AbstractFactory factory2 = new ConcreteFactory2();
ProductA productA2 = factory2.createProductA();
ProductB productB2 = factory2.createProductB();
productA2.useA();
productB2.useB();
}
}
在选择这些模式时,需要考虑实际的应用场景、产品的复杂性以及系统未来可能的扩展性。简单工厂模式适用于产品种类相对较少且不经常变化的情况,工厂方法模式适用于产品种类较多且可能经常变化的情况,而抽象工厂模式适用于需要处理产品族并且产品族可能扩展的复杂场景。
建造者模式是一种创建型设计模式,它用于构造一个复杂对象的分步骤接口。在这种模式中,同一个构建过程可以创建不同的表示。建造者模式将对象的构造过程与其表示分离,使得同样的构建过程可以创建不同的表示。
目的和作用
结构
// 产品类
class Car {
private String engine;
private String wheels;
private String body;
// 省略构造器和属性的setter和getter
@Override
public String toString() {
return "Car{" +
"engine='" + engine + '\'' +
", wheels='" + wheels + '\'' +
", body='" + body + '\'' +
'}';
}
}
// 抽象建造者
abstract class CarBuilder {
protected Car car;
public Car getCar() {
return car;
}
public void createNewCar() {
car = new Car();
}
public abstract void buildEngine();
public abstract void buildWheels();
public abstract void buildBody();
}
// 具体建造者
class SportsCarBuilder extends CarBuilder {
@Override
public void buildEngine() {
car.setEngine("Sports Engine");
}
@Override
public void buildWheels() {
car.setWheels("Sports Wheels");
}
@Override
public void buildBody() {
car.setBody("Sports Body");
}
}
// 导演类
class Director {
private CarBuilder carBuilder;
public Director(CarBuilder carBuilder) {
this.carBuilder = carBuilder;
}
public Car construct() {
//可以发现每次都得操作一个builder对象
//所以是不是可以在这里进行优化呢
carBuilder.createNewCar();
carBuilder.buildEngine();
carBuilder.buildWheels();
carBuilder.buildBody();
return carBuilder.getCar();
}
}
// 使用示例
public class BuilderDemo {
public static void main(String[] args) {
CarBuilder builder = new SportsCarBuilder();
Director director = new Director(builder);
Car car = director.construct();
System.out.println(car);
}
}
在这个例子中,Car是产品类,CarBuilder是抽象建造者,提供了创建产品各个部件的接口。SportsCarBuilder是具体的建造者,实现了这些接口以构造和装配SportsCar的各个部件。Director类负责管理构造过程。
适用场景
建造者模式提供了一种更灵活构造复杂对象的方式,允许用户只通过指定复杂对象的类型和内容就可以构建它们,同时将对象的构建过程和细节隐藏起来。
可以发现,上面的构造过程中,其实非常繁琐,每次都要重复使用builder对象,所以我们可以进行优化。
使得它的使用更加方便和直观。优化的常见方法是使用所谓的“流式接口”(Fluent Interface),它通过返回当前对象的引用(通常是通过返回this),允许链式调用设置方法,从而使代码更加简洁易读。
以下是对之前建造者模式示例的优化:
class Car {
private String engine;
private String wheels;
private String body;
// ... 省略其他部分 ...
// 使用流式接口设置属性
public Car setEngine(String engine) {
this.engine = engine;
return this;
}
public Car setWheels(String wheels) {
this.wheels = wheels;
return this;
}
public Car setBody(String body) {
this.body = body;
return this;
}
// ... 省略其他部分 ...
}
class CarBuilder {
private String engine = "Default Engine";
private String wheels = "Default Wheels";
private String body = "Default Body";
public CarBuilder setEngine(String engine) {
this.engine = engine;
return this;
}
public CarBuilder setWheels(String wheels) {
this.wheels = wheels;
return this;
}
public CarBuilder setBody(String body) {
this.body = body;
return this;
}
public Car build() {
return new Car().setEngine(engine).setWheels(wheels).setBody(body);
}
}
// 使用示例
public class BuilderDemo {
public static void main(String[] args) {
Car car = new CarBuilder()
.setEngine("Sports Engine")
.setWheels("Sports Wheels")
.setBody("Sports Body")
.build();
System.out.println(car);
}
}
在优化后的版本中,Car类和CarBuilder类都提供了设置各个属性的方法,并且这些方法都返回当前对象的引用。这允许在构建Car对象时可以进行链式调用,使代码更加清晰和简洁。
这种方式的优点是使得客户端代码更加直观和易于理解,同时保持了灵活性和可读性。这种风格的建造者模式在许多现代Java库中非常常见,尤其是在创建配置密集型对象时。
这种通过链式编程的方式来实现建造者方式,使得代码更加方便,我们使用Lombok注解提供的@Builder也就是通过这种方式实现的,非常的方便。
建造者模式的最直接的用法就是我们的StringBuffer了,它的append方法。
优点
缺点
使用场景
建造者模式和工厂模式都是为了创建对象,但是他们也有一些区别。
建造者模式
工厂模式
区别总结
单例模式是一种常用的软件设计模式,其核心思想是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。单例模式的实现通常涉及以下几个关键点:
单例模式的使用场景包括,但不限于,数据库连接、日志记录、配置管理等,这些场景中通常只需要一个共享的实例,或者因为创建实例的代价较高而需要限制实例的数量。
单例模式有几个变体,例如懒汉式(延迟初始化)、饿汉式(预先初始化)、双重检查锁定(Double-Check Locking)、注册式单例、ThreadLocal单例等,每种变体都有其特定的使用场景和优缺点。
对于ApplicationContext、ServletContext、DBPool等核心框架和系统组件被设计为单例,主要是出于以下几个原因:
例如,ApplicationContext在Spring框架中用于管理Bean的生命周期和依赖注入,如果有多个实例,每个实例可能会有不同的配置和状态,这会导致数据不一致和逻辑错误。同理,ServletContext用于表示整个web应用的环境,多实例将导致无法有效共享应用级信息。数据库连接池(DBPool)作为单例则能有效管理数据库连接,提高资源利用率,避免频繁创建和销毁连接带来的性能开销。
因此,将这些组件设计为单例是为了保持应用程序的一致性、减少资源消耗,并简化配置和管理的复杂性。
饿汉式单例模式是在类加载时就创建了单例对象,确保了单例的唯一性。以下是一个饿汉式单例的实现示例:
public class Singleton {
// 在类加载时就初始化
private static final Singleton INSTANCE = new Singleton();
//使用static赋值 和上面的那种方式二选一即可
static{
INSTANCE = new Singleton();
}
// 私有构造方法,防止外部实例化
private Singleton() {}
// 提供一个全局访问点
public static Singleton getInstance() {
return INSTANCE;
}
// 示例方法
public void doSomething() {
// ...
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.doSomething();
}
}
优点和设计理由
缺点
由于饿汉式单例的缺点是提前加载,因此在spring中就不可能使用饿汉式了,当然你也可以通过配置的方式来设定为某些类使用饿汉式单例,因为如果所有类都是用饿汉式单例加载,那么spring项目的启动速度将会大幅度降低。
懒汉式单例模式是一种单例实现方式,它在类加载时不会创建单例实例,而是在第一次被客户端调用时才创建这个唯一的实例。这种方式实现了延迟加载,有助于减少资源的浪费。
public class LazySingleton {
private static LazySingleton instance;
// 私有构造函数,防止外部实例化
private LazySingleton() {}
// 提供一个全局访问点
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
// 示例方法
public void doSomething() {
// ...
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
LazySingleton singleton = LazySingleton.getInstance();
singleton.doSomething();
}
}
优点
缺点
具体为什么会导致线程不安全,可以看下面我debug的方式。
我们当线程0先进入到断点,并且我们让其走进if,然后此时我们切换到线程1。
也就是此时两个线程下一步要执行的都是创建操作。
可以发现,这里就很明显的已经创建了两个的INSTANCE实例了,单例就被破坏。
而如果要解决这个问题,我们可以考虑加锁,将我们的方法设定为synchronized修饰。
那么此时这两个线程只能二选一的去执行创建,也就解决了线程安全问题。但是我们都知道加锁会导致线程阻塞。而这也是我们需要去避免的问题。具体的解决方法我们可以看下一节。
双重检查锁定单例是一种结合了懒汉式单例和同步锁的设计模式。它在单例的实例创建过程中进行了两次检查:第一次是为了避免不必要的同步,第二次是为了在单例实例尚未创建时创建实例。
public class DoubleCheckedLockingSingleton {
// 使用volatile关键字保证了instance变量的可见性
private static volatile DoubleCheckedLockingSingleton instance;
// 私有构造函数,防止外部实例化
private DoubleCheckedLockingSingleton() {}
// 提供一个全局访问点
public static DoubleCheckedLockingSingleton getInstance() {
if (instance == null) { // 第一重检查
synchronized (DoubleCheckedLockingSingleton.class) { // 同步锁
if (instance == null) { // 第二重检查
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
// 示例方法
public void doSomething() {
// ...
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
DoubleCheckedLockingSingleton singleton = DoubleCheckedLockingSingleton.getInstance();
singleton.doSomething();
}
}
优点
缺点
双重检查锁定单例模式的设计是为了解决懒汉式单例在多线程环境下的性能问题,同时保持延迟加载的优势。使用volatile关键字防止了JVM指令重排,保证了在多线程环境中对单例实例的安全发布。
静态内部类单例模式是利用Java语言特性实现单例的一种方式。它通过在单例类内部创建一个静态内部类,在这个内部类中持有单例对象的实例。由于静态内部类只会在第一次被加载时初始化一次,这种方法自然地实现了延迟加载和线程安全。
public class Singleton {
// 私有构造函数,防止外部实例化
private Singleton() {}
// 静态内部类
private static class SingletonHolder {
// 在内部类中持有单例的实例,并且可被直接初始化
private static final Singleton INSTANCE = new Singleton();
}
// 提供一个全局访问点
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
// 示例方法
public void doSomething() {
// ...
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.doSomething();
}
}
优点
缺点
其实我们知道,反射是可以破坏掉所有常规单例的,因为我们可以直接通过获取到构造方法的方式来创建对象实例。这里使用反射破坏单例比较简单,我写一套代码来反射反射的破坏,这里就基于静态内部类单例的方式来解决反射的破坏。
根据上面我们的静态内部类的特性我们知道,反射破坏单例的方法其实就是通过直接获取到外部类的构造方法然后newInstance的方式来获取到实例对象,那么我们可以在构造方法这里进行判断,如果内部类还没有被创建,你就像调用我构造方法,我直接一个报错。
当然,就是写起来不那么优雅。
package blossom.project.designmode.singleton;
import java.lang.reflect.Constructor;
public class Singleton {
// 私有构造方法
private Singleton() {
// 防御反射破坏单例
if (SingletonHolder.INSTANCE != null) {
throw new IllegalStateException("Instance already exists!");
}
}
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 获取实例的公共静态方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
public void show() {
System.out.println("Singleton using static inner class");
}
}
class ReflectionSingletonTest {
public static void main(String[] args) {
try {
// 获取Singleton类的Class对象
Class clazz = Singleton.class;
// 获取私有构造方法
Constructor constructor = clazz.getDeclaredConstructor();
// 设置私有构造方法的可访问性
constructor.setAccessible(true);
// 创建Singleton的实例
Singleton instance1 = constructor.newInstance();
// 获取正常途径获得的实例
Singleton instance2 = Singleton.getInstance();
// 输出两个实例的哈希码,检查是否相同
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}
枚举式单例模式是利用Java的枚举类型来实现单例模式。由于Java保证任何枚举值只实例化一次,在整个程序中不会被再次实例化,这自然形成了单例模式。并且由于枚举类型不能使用反射创建,所以保证了安全。
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("Doing something...");
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.doSomething();
}
}
优点
缺点
枚举式单例能够防止反射攻击主要是由于Java枚举类型的语言特性。在Java中,枚举类型有以下特点:
枚举式单例模式是实现单例的最佳方法之一,特别是在简单的单例实现中,或者在需要确保单例实例绝对防止多次实例化的场景下。由于其简洁性和提供的安全保证,它在许多情况下是实现单例的首选方法。
容器式单例模式(也称为注册式单例模式)使用一个容器来维护单例对象的引用,通常通过一个键(如字符串标识)来访问容器中的单例对象。这种模式允许在运行时动态地管理单例对象,使得管理多种单例更为灵活。
spring就是类似于按照这种方式来实现单例,但是肯定更加复杂和完整。
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SingletonManager {
private static final Map INSTANCE_MAP = new ConcurrentHashMap<>();
private SingletonManager() {}
@SuppressWarnings("unchecked")
public static T getInstance(String className) {
return (T) INSTANCE_MAP.computeIfAbsent(className, key -> {
try {
Class> clazz = Class.forName(key);
return clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
MySingleton singleton = SingletonManager.getInstance(MySingleton.class.getName());
singleton.doSomething();
}
}
class MySingleton {
public void doSomething() {
System.out.println("Doing something...");
}
}
优点
缺点
容器式单例模式适用于需要管理多种单例对象的场景,特别是在对象种类和数量在编译时无法完全确定,或者需要延迟加载的情况。这种模式通过统一的接口来管理和访问多个单例,使得单例对象的管理更加集中和统一。
注册式单例本身,在使用ConcurrentHashMap作为注册容器时,确实是线程安全的。ConcurrentHashMap通过提供线程安全的putIfAbsent、computeIfAbsent等方法,可以确保在多线程环境下对单个键值对的操作是安全的。
然而,线程安全问题可能出现在以下几个方面:
序列化和反序列化机制在某些情况下可以破坏单例模式。这是因为反序列化每次都会创建一个新的实例,不管该类是否实现了单例模式。下面是一个演示如何使用序列化和反序列化来破坏单例模式的例子:
首先,我们创建一个实现了Serializable接口的单例类:
import java.io.Serializable;
public class Singleton implements Serializable {
private static final long serialVersionUID = -7604766932017737115L;
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
if (INSTANCE != null) {
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
public static Singleton getInstance() {
return INSTANCE;
}
// 此方法用于防止反序列化破坏单例
protected Object readResolve() {
return getInstance();
}
}
接下来,我将这个单例实例序列化到一个文件中,然后再从该文件中反序列化两次,从而尝试创建两个实例:
import java.io.*;
public class SingletonSerializationTest {
public static void main(String[] args) throws Exception {
Singleton instanceOne = Singleton.getInstance();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
out.writeObject(instanceOne);
out.close();
// 反序列化第一次
ObjectInput in = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton instanceTwo = (Singleton) in.readObject();
in.close();
// 反序列化第二次
in = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton instanceThree = (Singleton) in.readObject();
in.close();
// 检查是否相同
System.out.println("instanceOne hashCode=" + instanceOne.hashCode());
System.out.println("instanceTwo hashCode=" + instanceTwo.hashCode());
System.out.println("instanceThree hashCode=" + instanceThree.hashCode());
}
}
在这个例子中,即使Singleton类是单例的,序列化和反序列化机制仍然可以创建它的多个实例。为了防止这种情况,我们在Singleton类中添加了readResolve方法。这个方法在反序列化过程中被调用,用于返回单例实例,从而保护单例状态不被破坏。
当运行上述代码时,你会发现所有实例的哈希码都是相同的,这表明反序列化过程中没有创建新的实例,单例状态得到了维护。这正是readResolve方法的功效。
使用ThreadLocal实现单例模式是一种确保在每个线程中单例对象唯一的方法。这种实现方式适用于需要在多线程环境下为每个线程维护一个单独的单例实例的场景。
public class ThreadLocalSingleton {
private static final ThreadLocal INSTANCE = ThreadLocal.withInitial(ThreadLocalSingleton::new);
private ThreadLocalSingleton() {}
public static ThreadLocalSingleton getInstance() {
return INSTANCE.get();
}
public void doSomething() {
// 方法实现
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();
singleton.doSomething();
}
}
在这个实现中,ThreadLocal.withInitial(ThreadLocalSingleton::new) 确保了每个访问该单例的线程都有一个独立的ThreadLocalSingleton实例。当调用getInstance()方法时,它首先检查当前线程的ThreadLocal映射中是否有ThreadLocalSingleton的实例。如果没有,它将调用提供的Supplier接口(这里是通过方法引用ThreadLocalSingleton::new实现的)来创建一个新的实例并保存在当前线程的ThreadLocal映射中。
特点和适用场景
需要注意的是,ThreadLocal单例模式并不是传统意义上的全局单例,因为它为每个线程创建了一个单独的实例。在使用这种单例模式时,应该清楚地了解其线程隔离的特性以及适用的场景。
代理模式是一种结构型设计模式,它为其他对象提供了一种代理(或称为替身),以控制对这个对象的访问。在代理模式中,代理对象插入到实际对象和访问者之间,作为中介,执行某些操作(如访问控制、延迟初始化、日志记录等),然后将调用传递给实际对象。代理模式的主要作用如下:
静态代理
静态代理是指在编译期间就创建好代理类的一种代理模式。在这种模式下,代理类和目标对象实现相同的接口或继承相同的类,代理类持有目标对象的引用,并在调用目标对象的方法前后可以执行一些附加操作。
以一个简单的例子来说明,假设有一个接口和一个实现了这个接口的类,我们将创建一个代理类来增强这个实现类的功能:
javaCopy code
// 接口
interface Service {
void doSomething();
}
// 实现类
class RealService implements Service {
public void doSomething() {
System.out.println("Doing something in RealService");
}
}
// 静态代理类
class StaticProxy implements Service {
private Service realService;
public StaticProxy(Service realService) {
this.realService = realService;
}
public void doSomething() {
System.out.println("Before RealService doSomething");
realService.doSomething();
System.out.println("After RealService doSomething");
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
Service service = new StaticProxy(new RealService());
service.doSomething();
}
}
优点
适用场景
缺点
静态代理模式在应用较为简单且目标对象稳定的情况下是非常有用的,但在需要大量动态代理的方法或目标对象经常变化的情况下,可能会导致代码的冗余和维护难度增加。在这种情况下,可以考虑使用动态代理模式。
JDK动态代理是Java提供的一种动态生成代理对象的机制,它允许开发者在运行时创建代理对象,而无需为每个类编写具体的代理实现。JDK动态代理主要通过java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口来实现。
实现原理
JDK动态代理工作原理是利用InvocationHandler来关联代理对象和实际对象,当通过代理对象调用方法时,这个调用会被转发到InvocationHandler的invoke方法。在invoke方法内,开发者可以在调用实际对象的方法前后添加自定义逻辑。
下面是一个使用JDK动态代理的例子:
package blossom.project.designmode.proxy;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import sun.misc.ProxyGenerator;
// 接口
interface Service {
void doSomething();
}
// 实现类
class RealService implements Service {
public void doSomething() {
System.out.println("Doing something in RealService");
}
}
// 调用处理器
class ServiceInvocationHandler implements InvocationHandler {
//被代理的实际对象
private Object target;
public ServiceInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//代理前置处理
System.out.println("Before method " + method.getName());
//被代理对象的方法被调用
Object result = method.invoke(target, args);
//代理后置处理
System.out.println("After method " + method.getName());
return result;
}
}
// 使用示例
public class JdkProxy {
public static void main(String[] args) {
RealService realService = new RealService();
//创建代理对象 然后调用代理对象的方法即可
Service proxyService = (Service) Proxy.newProxyInstance(
RealService.class.getClassLoader(),
new Class>[] {Service.class},
new ServiceInvocationHandler(realService)
);
proxyService.doSomething();
//生成字节码文件
byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{Service.class});
// 保存到文件系统
try (FileOutputStream out = new FileOutputStream("D://desktop//" + "$Proxy.class")) {
out.write(classFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个例子中,Service是一个接口,RealService是它的一个实现。ServiceInvocationHandler是一个InvocationHandler,在invoke方法中添加了在执行方法前后的逻辑。我们使用Proxy.newProxyInstance方法创建了RealService的代理对象。
优点
缺点
适用场景
JDK动态代理适用于需要代理的对象实现了一个或多个接口的场景。它在AOP(面向切面编程)、事务代理、日志记录等场景中非常有用。
面试的时候有被问到JDK动态代理的底层实现源码,所以这里我也简单的介绍一下它的源码实现。
运行我们上面的代码并且进入debug状态,可以看到如下状态。
首先是我们的proxyService的内部的target就是我们一开始new出来的那个需要被代理的对象。
但是最后的这个proxyService可以发现它的类型是
P
r
o
x
y
,我们知道,使用
Proxy,我们知道,使用
Proxy,我们知道,使用代表的是他是一个子类或者内部类的意思。而这里用
P
r
o
x
y
开头代表的是他是一个代理类,在外面看不到只能在内存中看到。而这里的
0
代表的是
J
d
k
自增的一个序号。这里我们知道,动态代理的底层其实就是为我们再运行的时候生成了一个类,那么我们只需要看看这个类的代码,我们就能大概知道他是如何实现的了。接下来我们来获取动态代理生成的类的字节码文件,运行上面的代码:这里特别注意需要用
J
D
K
8
哦。此时我们会得到一个
c
l
a
s
s
文件,不用打开看了,你啥也看不懂的。
!
[
i
m
a
g
e
.
p
n
g
]
(
h
t
t
p
s
:
/
/
i
m
g
−
b
l
o
g
.
c
s
d
n
i
m
g
.
c
n
/
i
m
g
c
o
n
v
e
r
t
/
19
e
c
2
d
3
b
b
048
f
7
c
0
e
1746490239248
b
3.
p
n
g
)
然后我们使用
J
A
D
来进行反编译,如果没有下载
J
A
D
的可以下载一个,链接如下:
[
J
A
D
反编译工具
]
(
h
t
t
p
s
:
/
/
v
a
r
a
n
e
c
k
a
s
.
c
o
m
/
j
a
d
/
)
!
[
i
m
a
g
e
.
p
n
g
]
(
h
t
t
p
s
:
/
/
i
m
g
−
b
l
o
g
.
c
s
d
n
i
m
g
.
c
n
/
i
m
g
c
o
n
v
e
r
t
/
a
b
93
a
53
f
7
e
2
f
082049
e
a
263
a
2883
d
b
a
e
.
p
n
g
)
之后就得到了反编译以后的文件了。上面的代码编译后如下:其中可以看到我们的被代理的方法,调用的是
s
u
p
e
r
.
h
.
i
n
v
o
k
e
方法,那么我们就得了解一下这个
h
到底是什么了。我们进入到
P
r
o
x
y
类的源码进行查看。因为这里
Proxy开头代表的是他是一个代理类,在外面看不到只能在内存中看到。而这里的0代表的是Jdk自增的一个序号。 这里我们知道,动态代理的底层其实就是为我们再运行的时候生成了一个类,那么我们只需要看看这个类的代码,我们就能大概知道他是如何实现的了。 接下来我们来获取动态代理生成的类的字节码文件,运行上面的代码: 这里特别注意需要用JDK8哦。 此时我们会得到一个class文件,不用打开看了,你啥也看不懂的。 ![image.png](https://img-blog.csdnimg.cn/img_convert/19ec2d3bb048f7c0e1746490239248b3.png) 然后我们使用JAD来进行反编译,如果没有下载JAD的可以下载一个,链接如下: [JAD反编译工具](https://varaneckas.com/jad/) ![image.png](https://img-blog.csdnimg.cn/img_convert/ab93a53f7e2f082049ea263a2883dbae.png) 之后就得到了反编译以后的文件了。 上面的代码编译后如下: 其中可以看到我们的被代理的方法,调用的是super.h.invoke方法,那么我们就得了解一下这个h到底是什么了。我们进入到Proxy类的源码进行查看。 因为这里
Proxy开头代表的是他是一个代理类,在外面看不到只能在内存中看到。而这里的0代表的是Jdk自增的一个序号。这里我们知道,动态代理的底层其实就是为我们再运行的时候生成了一个类,那么我们只需要看看这个类的代码,我们就能大概知道他是如何实现的了。接下来我们来获取动态代理生成的类的字节码文件,运行上面的代码:这里特别注意需要用JDK8哦。此时我们会得到一个class文件,不用打开看了,你啥也看不懂的。![image.png](https://img−blog.csdnimg.cn/imgconvert/19ec2d3bb048f7c0e1746490239248b3.png)然后我们使用JAD来进行反编译,如果没有下载JAD的可以下载一个,链接如下:[JAD反编译工具](https://varaneckas.com/jad/)![image.png](https://img−blog.csdnimg.cn/imgconvert/ab93a53f7e2f082049ea263a2883dbae.png)之后就得到了反编译以后的文件了。上面的代码编译后如下:其中可以看到我们的被代理的方法,调用的是super.h.invoke方法,那么我们就得了解一下这个h到底是什么了。我们进入到Proxy类的源码进行查看。因为这里Proxy0是继承了Proxy的,所以进入Proxy类。
可以发现,这里的h应该就是我们传入的InvocationHadnler的对象了。也就是我们的代理类。
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
import blossom.project.designmode.proxy.Service;
import java.lang.reflect.*;
public final class $Proxy0 extends Proxy
implements Service
{
public $Proxy0(InvocationHandler invocationhandler)
{
super(invocationhandler);
}
public final boolean equals(Object obj)
{
try
{
return ((Boolean)super.h.invoke(this, m1, new Object[] {
obj
})).booleanValue();
}
catch(Error _ex) { }
catch(Throwable throwable)
{
throw new UndeclaredThrowableException(throwable);
}
}
public final void doSomething()
{
try
{
super.h.invoke(this, m3, null);
return;
}
catch(Error _ex) { }
catch(Throwable throwable)
{
throw new UndeclaredThrowableException(throwable);
}
}
public final String toString()
{
try
{
return (String)super.h.invoke(this, m2, null);
}
catch(Error _ex) { }
catch(Throwable throwable)
{
throw new UndeclaredThrowableException(throwable);
}
}
public final int hashCode()
{
try
{
return ((Integer)super.h.invoke(this, m0, null)).intValue();
}
catch(Error _ex) { }
catch(Throwable throwable)
{
throw new UndeclaredThrowableException(throwable);
}
}
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
static
{
try
{
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {
Class.forName("java.lang.Object")
});
m3 = Class.forName("blossom.project.designmode.proxy.Service").getMethod("doSomething", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
}
catch(NoSuchMethodException nosuchmethodexception)
{
throw new NoSuchMethodError(nosuchmethodexception.getMessage());
}
catch(ClassNotFoundException classnotfoundexception)
{
throw new NoClassDefFoundError(classnotfoundexception.getMessage());
}
}
}
那么到此为止我们就大概知道JDK动态代理的实现了。
我们现在来手写一个JDK动态代理。
CGLIB(Code Generation Library)是一个功能强大的高性能代码生成库,它允许在运行时动态地扩展Java类和实现Java接口。在代理模式中,CGLIB常被用于实现动态代理,特别是当被代理的对象没有实现任何接口时。
实现原理
CGLIB动态代理通过继承要代理的类并在运行时生成子类来实现。CGLIB通过拦截所有对代理对象的方法调用,将这些调用转发到一个方法拦截器(Method Interceptor)中,从而实现代理逻辑。
要使用CGLIB,需要引入CGLIB库。如果你使用Maven,可以添加以下依赖:
cglib
cglib
3.3.0
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
// 被代理的类(不需要实现接口)
class RealService {
public void doSomething() {
System.out.println("Doing something in RealService");
}
}
// 方法拦截器
class ServiceMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before method " + method.getName());
Object result = proxy.invokeSuper(obj, args);
System.out.println("After method " + method.getName());
return result;
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(RealService.class);
enhancer.setCallback(new ServiceMethodInterceptor());
RealService proxyService = (RealService) enhancer.create();
proxyService.doSomething();
}
}
优点
缺点
适用场景
CGLIB代理非常适合以下场景:
在Spring框架中,当AOP需要代理没有实现接口的Bean时,通常会使用CGLIB代理。
我们知道项目开发的时候一般会用到多数据源,而多数据源的实现也是依赖于我们的代理模式。
对于多数据源的切换,我们得考虑如下几点:
按照如上的几点,我们可以写出如下的代码:
首先创建一个动态数据源类,其中使用ThreadLocal来实现线程隔离切换数据源。
public class DynamicDataSource {
public final static String DEFAULE_DATA_SOURCE = "DB_2022";
private final static ThreadLocal local = new ThreadLocal();
private DynamicDataSource(){}
public static String get(){return local.get();}
public static void reset(){
local.set(DEFAULE_DATA_SOURCE);
}
public static void set(String source){local.set(source);}
public static void set(int year){local.set("DB_" + year);}
}
然后我们实现InvocationHandler接口。
要做的就是在执行具体的被代理增强的方法之前切换一下数据源即可。
public class UserServiceDynamicProxy implements MyInvocationHandler {
private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
Object proxyObj;
public Object getInstance(Object proxyObj) {
this.proxyObj = proxyObj;
Class> clazz = proxyObj.getClass();
return MyProxy.newProxyInstance(new MyClassLoader(),clazz.getInterfaces(),this);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before(args[0]);
Object object = method.invoke(proxyObj,args);
after();
return object;
}
private void after() {
System.out.println("Proxy after method");
//还原成默认的数据源
DynamicDataSource.reset();
}
//target 应该是订单对象Order
private void before(Object target) {
try {
//进行数据源的切换
System.out.println("Proxy before method");
//约定优于配置
Long time = (Long) target.getClass().getMethod("getCreateTime").invoke(target);
Integer dbRouter = Integer.valueOf(yearFormat.format(new Date(time)));
System.out.println("静态代理类自动分配到【DB_" + dbRouter + "】数据源处理数据");
DynamicDataSource.set(dbRouter);
}catch (Exception e){
e.printStackTrace();
}
}
}
public class DbRouteProxyTest {
public static void main(String[] args) {
try {
User user = new User();
// user.setCreateTime(new Date().getTime());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
Date date = sdf.parse("2023/03/01");
user.setCreateTime(date.getTime());
IUserService orderService = (IUserService)new UserServiceDynamicProxy().getInstance(new UserService());
// IOrderService orderService = (IOrderService)new UserServiceStaticProxy(new UserService());
orderService.createUser(user);
}catch (Exception e){
e.printStackTrace();
}
}
}
JDK动态代理和CGLIB动态代理是Java中实现动态代理的两种主要方法,它们各有特点和适用场景。下面详细比较这两种动态代理方式:
JDK动态代理
底层实现
优点
缺点
CGLIB动态代理
底层实现
优点
缺点
总结
在实际的软件开发中,具体选择哪种代理方式取决于具体的应用场景和需求。在Spring框架中,当AOP代理需要被应用时,如果目标对象实现了接口,默认会使用JDK动态代理;如果目标对象没有实现接口,或者显式配置使用CGLIB代理,则会使用CGLIB。
原型模式是一种创建型设计模式,它使得一个对象能够通过复制现有的实例来创建新的实例,而无需知道相应类的具体实现。在原型模式中,被复制的实例就是所谓的“原型”,这种模式通常涉及到实现一个克隆(Clone)操作。
作用
在Java中,原型模式通常是通过实现Cloneable接口和重写Object类中的**clone()**方法来实现的。
// 实现Cloneable接口
public class Prototype implements Cloneable {
private String value;
public Prototype(String value) {
this.value = value;
}
public String getValue() {
return value;
}
// 重写clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class PrototypeDemo {
public static void main(String[] args) {
try {
Prototype prototype = new Prototype("Original");
Prototype clone = (Prototype) prototype.clone();
System.out.println(prototype.getValue()); // 输出: Original
System.out.println(clone.getValue()); // 输出: Original
// 修改克隆的值
clone.setValue("Cloned");
System.out.println(prototype.getValue()); // 输出: Original
System.out.println(clone.getValue()); // 输出: Cloned
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
在这个例子中,Prototype类实现了Cloneable接口,并重写了clone()方法。这允许创建Prototype对象的副本。PrototypeDemo类展示了如何使用原型模式来创建对象的副本。
注意事项
下面是一个演示浅克隆问题的Java示例。在这个示例中,我将创建一个包含引用类型字段的类,并展示在浅克隆过程中这个引用类型字段如何被处理,从而导致克隆对象和原始对象共享相同的引用类型字段。
class RefType {
private int data;
public RefType(int data) {
this.data = data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
}
class ShallowCloneExample implements Cloneable {
private RefType refType;
public ShallowCloneExample(RefType refType) {
this.refType = refType;
}
// 实现浅克隆
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public RefType getRefType() {
return refType;
}
public static void main(String[] args) {
try {
RefType refType = new RefType(100);
ShallowCloneExample original = new ShallowCloneExample(refType);
ShallowCloneExample cloned = (ShallowCloneExample) original.clone();
System.out.println("Original object refType data: " + original.getRefType().getData()); // 输出: 100
System.out.println("Cloned object refType data: " + cloned.getRefType().getData()); // 输出: 100
// 修改原始对象的引用类型字段
refType.setData(200);
// 因为是浅克隆,所以克隆对象的refType字段也被改变了
System.out.println("Original object refType data (after modification): " + original.getRefType().getData()); // 输出: 200
System.out.println("Cloned object refType data (after modification): " + cloned.getRefType().getData()); // 输出: 200
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
在这个例子中,ShallowCloneExample类包含一个名为refType的引用类型字段。这个类实现了Cloneable接口,并重写了**clone()**方法来提供浅克隆功能。
这正是浅克隆的问题所在:如果对象包含引用其他对象的字段,这些字段的内容不会被克隆,而是两个对象(原始对象和克隆对象)将共享同一个引用。这可能导致意外的行为,尤其是在涉及可变对象时。
要解决浅克隆带来的问题,我们可以实现深克隆(Deep Clone)。在深克隆过程中,不仅克隆对象本身,还会克隆它所引用的所有对象。这样,克隆出的对象与原始对象在引用类型上不再共享同一个实例。
下面是实现深克隆的示例代码,包括对引用类型成员的深度复制:
import java.io.*;
class RefType implements Serializable {
private int data;
public RefType(int data) {
this.data = data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
}
class DeepCloneExample implements Serializable {
private RefType refType;
public DeepCloneExample(RefType refType) {
this.refType = refType;
}
// 实现深克隆
public DeepCloneExample deepClone() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (DeepCloneExample) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public RefType getRefType() {
return refType;
}
public static void main(String[] args) {
RefType refType = new RefType(100);
DeepCloneExample original = new DeepCloneExample(refType);
DeepCloneExample cloned = original.deepClone();
System.out.println("Original object refType data: " + original.getRefType().getData()); // 输出: 100
System.out.println("Cloned object refType data: " + cloned.getRefType().getData()); // 输出: 100
// 修改原始对象的引用类型字段
refType.setData(200);
// 检查克隆对象的refType字段是否受到影响
System.out.println("Original object refType data (after modification): " + original.getRefType().getData()); // 输出: 200
System.out.println("Cloned object refType data (after modification): " + cloned.getRefType().getData()); // 输出: 100
}
}
在这个例子中,我们使用了序列化和反序列化来实现深克隆。这种方法要求被克隆的对象以及它引用的所有对象必须实现Serializable接口。
注意事项
特别注意:我们知道在spring中可以选择类的类型,也就是选择单例/原型。
因此,这两个模式本身就是冲突的。只能二选一,这也就是为什么spring中分别提供了两种模式,而不是提供一个:单例原型类型。
原型模式有可能破坏单例模式。当一个单例类实现了克隆(Cloneable)接口并重写了clone方法时,克隆操作可能会创建单例对象的多个副本,从而破坏单例的唯一性。
下面是一个展示原型模式如何破坏单例的示例代码:
javaCopy code
class Singleton implements Cloneable {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
@Override
protected Object clone() throws CloneNotSupportedException {
// 调用Object类的clone方法
return super.clone();
}
}
public class SingletonCloneTest {
public static void main(String[] args) {
try {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = (Singleton) singleton1.clone();
System.out.println(singleton1 == singleton2); // 输出:false
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
在这个例子中,我们有一个Singleton类,它是一个标准的单例实现。但是,这个类同时实现了Cloneable接口并重写了clone方法,这使得我们可以通过克隆方法创建它的副本。在SingletonCloneTest类的main方法中,我们首先获取了单例实例singleton1,然后通过克隆创建了第二个实例singleton2。由于克隆操作创建了一个新的实例,因此singleton1和singleton2不是同一个对象,这就破坏了单例的原则。
要防止单例被克隆破坏,可以在重写的clone方法中抛出异常,或者直接返回单例实例:
javaCopy code
@Override
protected Object clone() throws CloneNotSupportedException {
// 抛出异常,阻止克隆
throw new CloneNotSupportedException("Cannot clone a singleton.");
// 或者返回单例实例
// return instance;
}
这种方式可以确保即使单例类实现了Cloneable接口,也不会通过克隆创建出多个实例,从而维护单例的唯一性。
在JDK中,很多类都是用了原型模式。
首先是ArrayList中的clone方法。这就是一个深拷贝。
再来看看HashMap。
一句话概括深浅拷贝的区别的话:浅拷贝就是拷贝引用,深拷贝拷贝的就是值。
使用场景
缺点
优点: