Непонятно, почему вы думаете, что enum
типы не были лениво инициализированы. Нет никакой разницы с другими типами классов:
public class InitializationExample {
public static void main(String[] args) {
System.out.println("demonstrating lazy initialization");
System.out.println("accessing non-enum singleton");
Object o = Singleton.INSTANCE;
System.out.println("accessing the enum singleton");
Object p = SingletonEnum.INSTANCE;
System.out.println("q.e.d.");
}
}
public enum SingletonEnum {
INSTANCE;
private SingletonEnum() {
System.out.println("SingletonEnum initialized");
}
}
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton() {
System.out.println("Singleton initialized");
}
}
demonstrating lazy initialization
accessing non-enum singleton
Singleton initialized
accessing the enum singleton
SingletonEnum initialized
q.e.d.
Поскольку лень уже есть в любом случае, нет причин использовать вложенный тип, как в вашем сериализуемом примере синглтона. Вы все еще можете использовать более простую форму
public class SerializableSingleton implements Serializable {
public static final SerializableSingleton INSTANCE = new SerializableSingleton();
private static final long serialVersionUID = 1L;
private SerializableSingleton() {
System.out.println("SerializableSingleton initialized");
}
protected Object readResolve() {
return INSTANCE;
}
}
. Отличие от enum
состоит в том, что поля действительно сериализуются, но в этом нет никакого смысла, так как после десериализации восстановленный объект заменяется текущим экземпляр времени выполнения. Вот для чего нужен метод readResolve()
.
Это проблема семантики c, так как может быть произвольное количество различных сериализованных версий, но только один фактический объект, так как в противном случае это не было бы Singleton больше.
Просто для полноты,
public class SerializableSingleton implements Serializable {
public static final SerializableSingleton INSTANCE = new SerializableSingleton();
private static final long serialVersionUID = 1L;
int value;
private SerializableSingleton() {
System.out.println("SerializableSingleton initialized");
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
protected Object readResolve() {
System.out.println("replacing "+this+" with "+INSTANCE);
return INSTANCE;
}
public String toString() {
return "SerializableSingleton{" + "value=" + value + '}';
}
}
SerializableSingleton single = SerializableSingleton.INSTANCE;
single.setValue(42);
byte[] data;
try(ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(single);
oos.flush();
data = baos.toByteArray();
}
single.setValue(100);
try(ByteArrayInputStream baos = new ByteArrayInputStream(data);
ObjectInputStream oos = new ObjectInputStream(baos)) {
Object deserialized = oos.readObject();
System.out.println(deserialized == single);
System.out.println(((SerializableSingleton)deserialized).getValue());
}
SerializableSingleton initialized
replacing SerializableSingleton{value=42} with SerializableSingleton{value=100}
true
100
Так что здесь нет никакого поведенческого преимущества в использовании обычного класса здесь. Хранение полей противоречит одноэлементной природе и в лучшем случае , эти значения не имеют никакого эффекта, и десериализованный объект заменяется фактическим объектом времени выполнения, точно так же, как константа enum
десериализуется в канонический объект в первое место.
Кроме того, нет никакой разницы в отношении отложенной инициализации. Таким образом, класс non-enum требует больше кода для написания, чтобы получить ничего лучше.
Тот факт, что механизм readResolve()
требует сначала десериализации объекта, прежде чем он может быть заменен фактическим объектом результата, не только неэффективно, он временно нарушает одноэлементный инвариант, и это нарушение не всегда корректно устраняется в конце процесса.
Это открывает возможность для взлома сериализации:
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class TestSer {
public static void main(String[] args) throws IOException, ClassNotFoundException {
SerializableSingleton singleton = SerializableSingleton.INSTANCE;
String data = "’\0\5sr\0\25SerializableSingleton\0\0\0\0\0\0\0\1\2\0\1L\0\1at\0\10"
+ "LSneaky;xpsr\0\6SneakyOÎæJ&r\234©\2\0\1L\0\1rt\0\27LSerializableSingleton;"
+ "xpq\0~\0\2";
try(ByteArrayInputStream baos=new ByteArrayInputStream(data.getBytes("iso-8859-1"));
ObjectInputStream oos = new ObjectInputStream(baos)) {
SerializableSingleton official = (SerializableSingleton)oos.readObject();
System.out.println(official+"\t"+(official == singleton));
Object inofficial = Sneaky.instance.r;
System.out.println(inofficial+"\t"+(inofficial == singleton));
}
}
}
class Sneaky implements Serializable {
static Sneaky instance;
SerializableSingleton r;
Sneaky(SerializableSingleton s) {
r = s;
}
private Object readResolve() {
return instance = this;
}
}
SerializableSingleton initialized
replacing SerializableSingleton@bebdb06 with SerializableSingleton@7a4f0f29
SerializableSingleton@7a4f0f29 true
SerializableSingleton@bebdb06 false
Также на Ideone
Как продемонстрировано, readObject()
возвращает канонический экземпляр, как и предполагалось, но наш класс Sneaky
предоставляет доступ ко второму экземпляру «singleton», который должен был иметь временную природу.
Причина, по которой это работает, заключается именно в том, что поля сериализуются и десериализуются. Специально сконструированные (скрытые) данные потока содержат поле, которого на самом деле не существует в синглтоне, но, поскольку serialVersionUID
совпадает, ObjectInputStream
примет данные, восстановит объект и затем отбросит его, потому что нет поля для сохранить его. Но в это время экземпляр Sneaky
уже получил в руки синглтон через ссылку cycli c и запоминает его.
Специальная обработка типов enum
делает их невосприимчивыми к таким атакам.