单例:是一个只实例化一次的类。单例通常用来表示无状态对象,比如函数或系统组件,它们在本质上是唯一的。
实现单例有两种常见的方法。两者都基于保证构造函数私有,然后暴露一个公共静态成员以提供对唯一实例的访问。
方式1:公共字段的方式
一种方式,便是用final修饰成员变量:
// Singleton with public final field
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
在这种方式中,私有构造函数只调用一次,用于初始化public static final
修饰的Elvis
类型字段INSTANCE
。不使用public
或protected
修饰的构造函数保证了全局唯一性:一旦初始化了Elvis
类,就会存在一个且只有一个Elvis
实例,不多也不少。客户端所做的任何事情都不能改变这一点,但有一点需要注意:拥有特殊权限的客户端可以借助AccessibleObject.setAccessible
方法利用反射调用私有构造函数。如果需要防范这种攻击,请修改构造函数,使其在请求创建第二个实例时抛出异常。
使用AccessibleObject.setAccessible
方法调用私有构造函数示例:
Constructor<?>[] constructors = Elvis.class.getDeclaredConstructors();
AccessibleObject.setAccessible(constructors, true);
Arrays.stream(constructors).forEach(name -> {
if (name.toString().contains("Elvis")) {
Elvis instance = (Elvis) name.newInstance();
instance.leaveTheBuilding();
}
});
方式2:公共静态方法的方式
实现单例的另一种方式是,提供一个公共的静态工厂方法:
// Singleton with static factory
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() { ... }
}
所有对getInstance()
方法的调用都返回相同的对象引用,并且不会创建其他Elvis
实例(注意前面提到的相同警告)。
公共字段方式和公共静态方法方式的比较:
公共字段方式主要优点是API
明确了类是单例的:public static
修饰的字段是final
的,因此它总是包含相同的对象引用。第二个优点是更简单。
静态工厂方法的一个优点是,它可以在不更改 API
的情况下决定类是否是单例。工厂方法返回唯一的实例,但是可以对其进行修改,为调用它的每个线程返回一个单独的实例。第二个优点是,如果应用程序需要的话,可以编写泛型的单例工厂。使用静态工厂的最后一个优点是方法引用能够作为一个提供者,例如 Elvis::getInstance
是 Supplier<Elvis>
的提供者。除非能够与这些优点沾边,否则使用 public 字段的方式更可取。
关于序列化: 上面的两种单例的实现方式,如果想让单例类实现可序列化,仅仅在声明中实现serializable
是不行的。想要保证单例,应声明所有实例字段为transient
,并提供readResolve
方法。否则,每次反序列化实例时,都会创建一个新实例,在我们的示例中,这会导致出现虚假的Elvis。为了防止这种情况发生,将这个readResolve
方法添加到 Elvis
类中:
// readResolve method to preserve singleton property
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
方式3:声明一个单元素枚举
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
这种方法类似于 public 字段方法,但是它更简洁,默认提供了序列化机制,提供了对多个实例化的严格保证,即使面对复杂的序列化或反射攻击也是如此。这种方法可能有点不自然,但是单元素枚举类型通常是实现单例的最佳方法。 注意,如果你的单例必须扩展一个超类而不是 Enum(尽管你可以声明一个 Enum 来实现接口),你就不能使用这种方法。
References
最新评论
我的是ipv4网络,如何使用直播源啊!
我今天试了,不想啊,我的是新疆昌吉移动的网络。
收不到验证码电报
现在充值29起了