设计模式基础知识补充

为什么私有化构造函数后可以防止外部通过new singleton来创建新实例

1、使用new关键字时,就是在执行构造方法创建对象

new关键字的工作流程

  1. 内存分配
    JVM 先在堆内存中为对象分配空间,并初始化所有实例变量为默认值(如 int 默认为 0,引用类型默认为 null)。
    如果用无参构造方法,对象的变量也会有默认值

  2. 执行构造方法
    调用类的构造方法(如 Singleton())来初始化对象的状态(如果有初始化代码)。

    • 如果构造方法是 private 的,外部代码无法通过 new 调用它,从而阻止直接创建对象。
  3. 返回对象引用
    最终返回新创建对象的引用(内存地址)。


单例模式中的关键点
在单例模式中:

private Singleton() {}  // 私有构造方法

外部代码尝试 new Singleton() 时:
编译器会直接报错,因为私有构造方法对外不可见,无法调用。
(此时连第一步的内存分配都不会发生,因为语法检查阶段就失败了)

静态变量是只加载一次吗,什么时候才会加载静态变量

静态变量在以下情况下会被加载:

  1. 类首次主动使用时(主动引用):

    • 创建类的实例(new
    • 访问类的静态变量或静态方法
    • 使用反射(Class.forName()
    • 初始化子类时(父类会先初始化)
    • 作为程序入口的主类(包含main()的类)
  2. 不会触发加载的情况(被动引用):

    • 通过子类引用父类的静态变量(不会初始化子类)
    • 定义类的数组(如MyClass[] arr
    • 引用类的常量(final static常量,在编译期已优化)

所以利用静态变量只会加载一次的特性

单例模式中的应用(单例模式的其中一种--饿汉式(静态常量))

在单例模式中利用静态变量的特性:

class Singleton {
    // 静态变量在类加载时初始化,且只执行一次
    private static final Singleton instance = new Singleton();
    
    private Singleton() {}  // 私有构造
    
    public static Singleton getInstance() {
        return instance;  // 总是返回同一实例
    }
}

然后在psvm中执行
这样
Singleton.getInstance就能得到singleton对象,然后由于类的static变量只会执行一次,后期继续Singleton.getInstance,得到的对象只是同一个
这个类的静态变量加载时候就是上面的访问类的静态变量或静态方法生成的

这样可以保证:

  1. 线程安全(由JVM类加载机制保证)
  2. 全局唯一性
  3. 延迟加载(直到首次调用getInstance()时才加载类)

原型模式中为什么要继承clone接口再重写clone的方法,直接写一个clone方法不是更快吗?

非常好的问题!这触及了设计模式的核心思想之一:在接口和契约下编程,而不是在实现下编程。

直接写一个 clone() 方法确实更快,但从设计角度来说,实现 Cloneable 接口并重写 Object.clone() 是更优秀、更标准、更安全的选择。原因如下:

  1. 建立明确的契约(最重要的原因)

· “我是一个可克隆的对象”:实现 Cloneable 接口的首要目的,是给类的使用者(其他程序员或未来的你)一个明确的信号:“这个类设计时考虑了克隆功能,并且支持克隆”。这是一个标识,是一种约定。
· 没有契约的后果:如果只是随意地自己写一个 myClone() 方法,使用者如何知道你的类有这个功能?他们需要阅读源代码或文档才能发现。而实现标准的 Cloneable 接口和 clone() 方法是 Java 生态中众所周知的约定,使用者一看便知。

  1. 与Java内置机制和第三方库兼容

· Java标准库的支持:许多 Java 内置的工具、框架或第三方库(如通过反射操作对象、序列化库、Bean工具等)可能会识别 Cloneable 接口,并调用标准的 clone() 方法来创建对象副本。如果你使用自定义方法,这些机制将完全失效。
· 多态性:你可以编写接收 Cloneable 接口参数的方法,然后对所有实现了该接口的不同类的对象调用 clone()。如果每个类都有自己的克隆方法名(如 cloneA(), copyB(), duplicateC()),你就无法编写通用的克隆代码。

// 基于接口和多态的通用克隆方法
public void processAndClone(Cloneable original) {
    try {
        Object copy = original.clone();
        // ... 对copy进行一些操作
    } catch (CloneNotSupportedException e) {
        // 处理异常
    }
}

// 如果每个人都有自己的方法名,这就无法实现
public void processAndClone(MyObject original) {
    MyObject copy = original.myClone(); // 只能针对MyObject类
}
  1. 规范方法签名和行为

Object.clone() 方法已经定义了一个标准的方法签名:

protected native Object clone() throws CloneNotSupportedException;

重写它时,我们通常会将其改为 public,并返回具体的类型(协变返回类型,这是Java 5后的特性),这提供了更好的类型安全。

@Override
public MyPrototype clone() {
    try {
        return (MyPrototype) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(); // 因为我们已经实现了Cloneable,所以不会发生
    }
}

这样做保证了所有类的克隆方法都具有相同的行为:返回一个当前对象的副本。自定义方法名无法强制这种一致性。

  1. “保护伞”功能:Object.clone() 的本地实现

虽然 Object.clone() 方法有很多问题(如浅拷贝、笨拙的异常处理),但它提供了一个高效的、 native 级别的内存二进制拷贝作为基础实现。我们通常调用 super.clone() 来利用这个基础功能,然后再根据需要重写它来实现深拷贝。

public class ComplexObject implements Cloneable {
    private int id;
    private String name;
    private int[] data; // 引用类型

    @Override
    public ComplexObject clone() {
        try {
            ComplexObject clone = (ComplexObject) super.clone(); // 1. 调用Object.clone()进行浅拷贝
            clone.data = data.clone(); // 2. 手动对引用类型进行深拷贝
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

直接自己写一个方法,你就失去了这个可以快速完成所有字段浅拷贝的“起点”。

直接写一个Clone方法的缺点总结:

  1. 无契约:使用者无法通过类型系统得知该对象可克隆。
  2. 不兼容:无法与期望标准 clone() 方法的工具、框架或通用代码协作。
  3. 不一致:方法名称和行为在不同类之间可能不统一,降低代码可读性和可维护性。
  4. 重复造轮子:放弃了 Object.clone() 提供的现成的、高效的浅拷贝基础实现。

结论

实现 Cloneable 并重写 Object.clone() 是一种“遵守契约”的设计思想。它牺牲了一点初期的“方便”,换来了代码的清晰性、一致性、兼容性和可维护性。这正是设计模式所倡导的:通过遵循良好的约定和模式,来构建更健壮、更灵活的软件结构。

所以,虽然直接写一个方法“更快”,但从软件工程的角度来看,遵循标准接口是“更好”的选择。

posted @ 2025-08-14 20:02  乘加法  阅读(8)  评论(0)    收藏  举报