采访问题:关于Java序列化和单例

在采访中,面试官问我以下问题:是否可以序列化单个对象? 我说是的,但在哪种情况下我们应该序列化一个单身人士?

是否可以设计一个无法序列化对象的类?

这个问题应该更好地表达为“是否有可能以不破坏单例模式的方式使用单例模式类C进行序列化和反序列化?”

答案基本上是肯定的:

import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectStreamException; import java.io.Serializable; public class AppState implements Serializable { private static AppState s_instance = null; public static synchronized AppState getInstance() { if (s_instance == null) { s_instance = new AppState(); } return s_instance; } private AppState() { // initialize } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); synchronized (AppState.class) { if (s_instance == null) { // re-initialize if needed s_instance = this; // only if everything succeeds } } } // this function must not be called other than by the deserialization runtime private Object readResolve() throws ObjectStreamException { assert(s_instance != null); return s_instance; } public static void main(String[] args) throws Throwable { assert(getInstance() == getInstance()); java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(baos); oos.writeObject(getInstance()); oos.close(); java.io.InputStream is = new java.io.ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(is); AppState s = (AppState)ois.readObject(); assert(s == getInstance()); } } 

但请注意,使用此代码可以存在多个AppState实例。 但是,只引用了一个。 其他符合垃圾收集条件,仅由反序列化运行时创建,因此它们不存在用于实际目的。

对于其他两个问题的答案(在哪种情况下我们应该序列化一个单例?是否可以设计一个其对象无法序列化的类?),请参阅@Michael Borgwardt的答案 。

在哪种情况下我们应该序列化单例。

想象一下,你有一个长期运行的应用程序,并希望能够关闭它,然后继续关闭它(例如,为了进行硬件维护)。 如果应用程序使用有状态的单例,则必须能够保存和恢复sigleton的状态,这最容易通过序列化来完成。

是否可以设计一个无法序列化对象的类。

确实非常简单:只是不实现Serializable并使类final

是否可以序列化单例对象?

这取决于单身人士的实施方式。 如果您的单例实现为具有一个元素的枚举类型,则默认情况下:

 // Enum singleton - the preferred approach public enum Elvis { INSTANCE; public void leaveTheBuilding() { ... } } 

如果您的单例不是使用单元素枚举类型实现的,而是使用静态工厂方法(变体是使用公共静态最终字段):

 // 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() { ... } } 

然后添加implements Serializable以使其可序列化是不够的, 您必须声明所有实例字段是瞬态的 (以防止序列化攻击) 并提供readResolve方法

要保持单例保证,必须声明所有实例字段为瞬态并提供readResolve方法(第77项)。 否则,每次反序列化序列化实例时,都会创建一个新实例,在我们的示例中,将导致虚假的猫王目击。 要防止这种情况,请将此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; } 

这在Effective Java中有很多讨论(它也显示了序列化攻击):

  • 第3项:使用私有构造函数或枚举类型强制执行单例属性
  • 第77项:例如控制,首选枚举类型为readResolve

我们应该在哪种情况下序列化单例

例如,用于临时,短期存储或通过网络传输对象(例如,使用RMI)。

是否可以设计一个无法序列化对象的类。

正如其他人所说,不要实现Serializable 。 即使一个对象或其中一个超类实现了Serializable ,您仍然可以通过从writeObject()抛出NotSerializableException来阻止它被序列化。

我说是

不是默认的。 在实现java.io.Serializable您需要覆盖readObject()writeObject() readResolve()方法,因为您无法序列化静态字段。 单例将其实例保存在静态字段中。

但在哪种情况下我们应该序列化一个单身人士。

实际上没有想到有用的真实场景。 单身人员在其整个生命周期中通常不会改变状态,也不包含您想要保存/恢复的任何状态。 如果确实如此,那么将其作为单身人士已经是错误的。

Java SE API中两个单例模式的真实示例是java.lang.Runtime#getRuntime()java.awt.Desktop#getDesktop() 。 它们都没有实现可序列化。 它也没有任何意义,因为它们在每次调用时都返回正确/期望/预期的实例。 如果序列化和反序列化,最终可能会出现多个实例。 如果同时从环境切换,则实例可能根本不起作用。

是否可以设计一个无法序列化对象的类。

是。 只是不要让类实现java.io.Serializable接口。

序列化单例的问题在于您最终得到两个,原始副本和反序列化副本。

防止序列化的最明显方法是简单地不实现可序列化。 但是,有时您会希望您的单例实现可序列化,以便可以在序列化对象中引用它而不会出现问题。

如果这是一个问题,你有几个选择。 如果可能的话,最好是使单例成为单个成员枚举。 这样底层的Java实现就可以处理所有细节。

如果这不可能,那么您需要实现适当的readObject和writeObject方法,以确保序列化不会生成单独的副本。

可序列化的类可以通过反序列化来实例化,允许多个实例,使其不是单例。

第二个问题,来自Java doc for java.io.Serializable

实现java.io.Serializable接口的类启用了类的可序列化。

因此,要实现一个不可序列化的类,请不要实现Serializable。