杰瑞科技汇

为何Java构造函数要设为private?

核心概念

当一个类的构造函数被声明为 private 时,意味着这个类只能在它自己的内部被实例化,任何其他类都无法通过 new 关键字来创建这个类的实例。

为何Java构造函数要设为private?-图1
(图片来源网络,侵删)

这听起来可能有些违反直觉,因为我们通常创建类就是为了在别处使用它。private 构造函数是一种非常重要的设计模式,主要用于实现单例模式静态工具类


主要用途

实现单例模式

这是 private 构造函数最经典和最常见的用途,单例模式确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。

为什么需要 private 构造函数?

  • 防止外部实例化:如果构造函数是 public 的,任何人都可以通过 new Singleton() 创建新的实例,这样就破坏了“只有一个实例”的规则。
  • 防止继承private 构造函数也使得该类不能被继承,因为子类在实例化时需要调用父类的构造函数,而父类的构造函数是 private 的,子类无法访问,从而阻止了继承。

实现方式:

为何Java构造函数要设为private?-图2
(图片来源网络,侵删)

有两种主流的单例模式实现:饿汉式懒汉式

a. 饿汉式

这种方式在类加载时就创建好了实例,所以是“饿”的,一上来就准备好。

public class Singleton {
    // 1. 在类内部创建一个唯一的、私有的静态实例
    // 注意:这个实例在类加载时就初始化了
    private static final Singleton INSTANCE = new Singleton();
    // 2. 将构造函数私有化,防止外部通过 new 创建实例
    private Singleton() {
        // 可以做一些初始化操作
    }
    // 3. 提供一个公共的静态方法,让外部可以获取到这个唯一的实例
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

优点

  • 实现简单。
  • 线程安全,因为 JVM 在类加载时就保证了实例的唯一性。

缺点

为何Java构造函数要设为private?-图3
(图片来源网络,侵删)
  • 如果这个实例从未被使用过,会造成内存的浪费。

b. 懒汉式

这种方式只有在第一次调用 getInstance() 方法时才创建实例,所以是“懒”的,按需创建。

线程不安全的实现(不推荐用于多线程环境)

public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) { // 多个线程可能同时在这里判断为 true
            instance = new Singleton(); // 导致创建多个实例
        }
        return instance;
    }
}

线程安全的实现(推荐使用)

可以使用 synchronized 关键字来保证线程安全。

public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    // 在方法上添加 synchronized 关键字,确保同一时间只有一个线程能进入此方法
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

更优化的线程安全实现(双重检查锁定 - DCL)

synchronized 会影响性能,我们可以使用双重检查锁定来优化。

public class Singleton {
    // 使用 volatile 关键字,确保 instance 变量在多线程环境下的可见性和有序性
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查(无锁,快速判断)
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查(在同步块内,确保只有一个线程能创建实例)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

创建静态工具类

当一个类只包含静态方法和静态字段时,我们不希望它被实例化。java.lang.Mathjava.util.Collections

为什么需要 private 构造函数?

  • 防止程序员不小心创建该类的实例,new Math(),由于所有方法都是静态的,创建实例没有任何意义,反而会浪费内存。

实现方式:

public final class MathUtils { // 加上 final 确保不能被继承
    // 私有构造函数,防止外部实例化
    private MathUtils() {
        // 抛出异常,防止通过反射调用私有构造函数
        throw new AssertionError("Cannot instantiate this utility class");
    }
    public static int add(int a, int b) {
        return a + b;
    }
    public static double power(double base, int exponent) {
        // ... 计算幂 ...
        return Math.pow(base, exponent);
    }
}

注意:在私有构造函数中抛出 AssertionError 是一个很好的实践,这可以防止有人通过反射(setAccessible(true))来绕过访问限制并创建实例,如果真的发生了,程序会直接报错,而不是静默地创建一个无用的实例。


如何绕过 private 构造函数?(以及如何防范)

了解如何绕过可以帮助我们更好地理解其原理和防范措施。

反射

可以通过反射机制调用 private 构造函数。

// 假设 Singleton 类是上面饿汉式的单例
Singleton singleton1 = Singleton.getInstance();
// 使用反射尝试创建第二个实例
try {
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true); // 解除私有访问限制
    Singleton singleton2 = constructor.newInstance();
    System.out.println(singleton1 == singleton2); // 输出 false,破坏了单例!
} (Exception e) {
    e.printStackTrace();
}

如何防范? 可以在单例类的私有构造函数中加入一个“标志位”,在第一次创建实例后将其设为 true,反射再次创建时检查这个标志位,如果为 true 就抛出异常。

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    // 添加一个标志位
    private static boolean isCreated = false;
    private Singleton() {
        if (isCreated) {
            throw new IllegalStateException("Singleton instance already created. Use getInstance() method.");
        }
        isCreated = true;
    }
    // ... 其他代码 ...
}

序列化与反序列化

如果单例类实现了 Serializable 接口,通过序列化和反序列化可以创建出新的实例,从而破坏单例。

// 假设 Singleton 实现了 Serializable
Singleton instance1 = Singleton.getInstance();
// 将 instance1 序列化到文件
// ... (序列化代码)
// 从文件反序列化
Singleton instance2 = (Singleton) new ObjectInputStream(new FileInputStream("singleton.ser")).readObject();
System.out.println(instance1 == instance2); // 输出 false,破坏了单例!

如何防范? 可以实现 readResolve() 方法,在反序列化时,JVM 会检查这个方法,如果存在,它会调用这个方法并返回其结果,而不是创建新的实例。

import java.io.Serializable;
public class Singleton implements Serializable {
    // ... 其他代码 ...
    // 防止反序列化破坏单例
    protected Object readResolve() {
        return getInstance();
    }
}

特性/用途 描述
核心作用 限制类的实例化,只能在类内部创建对象。
主要用途 实现单例模式:确保全局只有一个实例。
创建静态工具类:防止无意义的实例化。
如何实现 将构造函数声明为 private,并提供一个 public static 方法来获取预创建的实例(单例)或完全不提供实例化方法(工具类)。
如何绕过 反射:使用 setAccessible(true) 调用私有构造器。
序列化/反序列化:可以创建出与原实例无关的新对象。
如何防范 反射:在构造函数中检查实例是否已存在,存在则抛出异常。
序列化:实现 readResolve() 方法,返回已存在的单例实例。
其他优点 - 防止继承private 构造函数隐式地阻止了类的继承。
- API 清晰性:对于工具类,明确的私有构造函数向其他开发者传达了“这是一个工具类,请勿实例化”的意图。

private 构造函数是 Java 中一个强大而基础的语言特性,是实现特定设计模式(尤其是单例模式)和编写健壮的工具类的关键工具,理解它的工作原理以及如何绕过和防范它,对于编写高质量的 Java 代码至关重要。

分享:
扫描分享到社交APP
上一篇
下一篇