共计 7256 个字符,预计需要花费 19 分钟才能阅读完成。
当程序创立的对象,在程序终止后仍要存在,并在程序下次运行时重建对象,且领有与程序上次运行时所领有的信息雷同。序列化能够将对象写入字节流,反序列化就是将字节流复原为对象,如下图所示。
Java 中提供了一种通用序列化机制,实现将对象写出到输入流中,并在之后将其读回。
对象序列化
定义一个 Student
类,如下所示。
public class Student implements Serializable {
private static final long serialVersionUID = -4496225960550340595L;
private String name;
private Integer age;
private Double score;
...getter 与 setter...
@Override
public String toString() {return new StringJoiner(",", Student.class.getSimpleName() + "[", "]")
.add("name='" + name + "'")
.add("age=" + age)
.add("score=" + score)
.toString();}
}
在程序中应用 Student
类能够创立实例来示意某一学生。
Student s = new Student();
s.setName("小赵");
s.setAge(24);
s.setScore(98.5);
然而,在程序完结后,该实例会被销毁,如果想在下次运行时领有与上次运行时雷同的信息,应用 ObjectOutputStream
实例将 Student
实例序列化保留到文件中。
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxx"));
out.writeObject(s);
out.close();
对象序列化才应用
writeObject/readObject
办法,根本类型须要应用writeInt/readInt
或writeDouble/readDouble
这样的办法,对象流类都实现了DataInput/DataOutput
接口。
保留到文件中后,想要应用的话就能够通过创立 ObjectInputStream
实例并调用 readObject()
办法来获取。
ObjectInputStream in = new ObjectInputStream(new FileInputStream("xxx"));
Student saved = (Student) in.readObject();
in.close();
Serializable
Java 想要使一个类能被序列化,就须要像 Student
一样,实现 Serializable
接口。
public interface Serializable {}
该接口没有任何办法须要实现,只是作为类能被序列化的标记。然而没有该接口的话,序列化会报 java.io.NotSerializableException
异样。源码中序列化如下所示。
if (obj instanceof String) {writeString((String) obj, unshared);
} else if (cl.isArray()) {writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) { // 判断是否实现了 Serializable 接口
writeOrdinaryObject(obj, desc, unshared);
} else {if (extendedDebugInfo) {
throw new NotSerializableException(cl.getName() + "\n" + debugInfoStack.toString());
} else {throw new NotSerializableException(cl.getName());
}
}
当然,实现 Serializable
接口的可序列的类,在序列化时,会默认将类中所有信息序列化,如果想要管制序列化对象,能够应用 transient
关键字标识须要敞开序列化的字段,如下所示。
public class Student implements Serializable {
private static final long serialVersionUID = -4496225960550340595L;
private String name;
private transient Integer age;
private Double score;
···
}
// 序列化前:Student[name='小赵', age=24, score=98.5]
// 反序列化后:Student[name='小赵', age=null, score=98.5]
这样应用 transient
进行标记,就能够在对象序列化时跳过。
应用 Serializable
的默认序列化会升高扭转类实现的灵活性,减少 Bug
和安全漏洞的可抗性,测试也会减少累赘等,因而想应用 Serializable
实现序列化时要思考革除。
Externalizable
除了 Serializable
接口外,Java 还提供了继承 Serializable
接口的 Externalizable
接口,并且还要实现两个办法。
public class Student implements Externalizable {
...
@Override
public void writeExternal(ObjectOutput out) throws IOException { }
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {}}
Java 提供的 Externalizable
接口能够管制对象序列化时,不想被其序列化的信息,和反序列时,不被反序列化的信息。
@Override
public void writeExternal(ObjectOutput out) throws IOException {out.writeObject(name);
out.writeObject(score);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { // 反序列程序要与序列化时的程序统一,否则会报 java.lang.ClassCastException 异样
name = (String) in.readObject();
score = (Double) in.readObject();}
这样就不会将 Student
中的 age
序列化了,对序列化过程进行管制。
readObject
和writeObject
办法是公有的,并且只能被序列化机制调用。与此不同的是,readExternal
和writeExternal
办法是公共的。特地是,readExternal
还潜在地容许批改现有对象的状态。
serialVersionUID
在实现 Serializable
接口的可序列化的类里,都须要显示的减少上面这行代码。
private static final long serialVersionUID = -4496225960550340595L;
该静态数据域用于显示申明序列版本 UID
,并在序列化机制中,通过判断 serialVersionUID
来验证版本一致性。因而,反序列时,只有 serialVersionUID
与本地相应实体类的 serialVersionUID
统一,就能够反序列化,实现兼容,否则会报 java.io.InvalidClassException
异样。因而,只有 serialVersionUID
不变,序列化就能够读入这个类的对象的不同版本。
如果被序列化的对象具备在以后版本中所有没有的数据域,反序列化时会疏忽额定的数据;如果以后版本具备在被序列化的对象所没有的数据域,那么新增加的域将被设置成它们的默认值。
显式的申明 serialVersionUID
也会带来小小的性能益处。如果没有提供显式的序列版本 UID
,编译器会抉择一个摘要算法,并在运行时通过高开销的计算过程来产生一个序列版本 UID
,只有这个类有改变,失去的 UID
也就会变动,到时对象输出流将会回绝反序列具备不同序列版本 UID
。因而,在可序列化的类中声显著式的 serialVersionUID
。
束缚平安
当你确定默认的序列化模式就满足了以后环境,还必须提供一个 readObject
办法增加验证或其余行为以保障束缚关系和安全性。
private void readObject(ObjectInputStream in);
private void writeObject(ObjectOutputStream out);
之后,数据域就再也不会被主动序列化,取而代之的是调用这些办法。
如上所示,当反序列化时,如果学生分数不在 0~100
之间,就是谬误的数值,能够保护性地编写 readObject()
办法,保护其约束条件。
public class Student implements Serializable {
...
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {in.defaultReadObject(); // 调用默认反序列化办法
if (score < 0 || score > 100)
throw new IllegalArgumentException("The score is between 0 and 100."); // 判断如果学生问题有问题,抛出异样,终止操作。}
private void writeObject(ObjectOutputStream out) throws IOException {out.defaultWriteObject(); // 调用默认序列化办法
}
}
枚举序列化
当指标对象惟一时,可应用枚举实现序列化,如下所示。
public enum Week {
MONDAY, // 星期一
TUESDAY, // 星期二
WEDNESDAY, // 星期三
THURSDAY, // 星期四
FIRDAY, // 星期五
SATURDAY, // 星期六
SUNDAY; // 星期日
}
为了保障枚举类型及其定义的枚举变量在 VM
中是惟一的,Java 规定了枚举常量的序列化是通过ObjectOutputStream
将枚举的 name()
办法返回的值做序列化;反序列化时,通过 ObjectInputStream
从流中读取常量名称,而后调用 java.lang.Enum.valueOf()
办法取得反序列化常量,并将常量的枚举类型和收到的常量名称作为参数传递。
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException("No enum constant" + enumType.getCanonicalName() + "." + name);
}
编译器在序列化和反序列化时,会疏忽枚举类型定义的任何类特定的 writeObject
、readObject
、ReadObjectNodeData
、writeReplace
和 readResolve
办法。相似地,也会疏忽任何 serialPersistentFields
或 serialVersionUID
字段申明,所有枚举类型都具备规定的 serialVersionUID 0L
。记录枚举类型的可序列化字段和数据是不必要的,因为发送的数据类型没有变动。
在序列化和反序列化时,如果指标对象是惟一的,那么你必须加倍当心,这通常会在实现单例和类型平安的枚举时产生。
但在枚举之前,是通过应用 static final
来示意枚举类型,如下所示。
public class Week {public static final Week MONDAY = new Week(1);
public static final Week TUESDAY = new Week(2);
public static final Week WEDNESDAY = new Week(3);
public static final Week THURSDAY = new Week(4);
public static final Week FIRDAY = new Week(5);
public static final Week SATURDAY = new Week(6);
public static final Week SUNDAY = new Week(7);
private int value;
private Week(int v) {this.value = v;}
}
然而,为这个类实现 Serializable
接口变成可序列化的类后,默认序列化机制就不再实用,任何 readObject
办法都会返回一个新键的实例。
public class Week implements Serializable {public static final Week MONDAY = new Week(1);
···
}
Week w = FIRDAY;
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxx/ioweek.txt"));
out.writeObject(w);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("xxx/ioweek.txt"));
Week s = (Week) in.readObject();
in.close();
反序列化获取的 Week
对象与序列化的 Week
对象比拟 w == s
为 false
,新键的实例与该类初始化时创立的实例不同,阐明即便结构器公有,序列化机制也能够创立新实例。
readResolve
个性容许用 readObject
创立的实例代替另一个实例。因而,在 Week
类中定义 readResolve
办法,如下所示。
private Object readResolve() throws ObjectStreamException {if (value == 1) return Week.MONDAY;
if (value == 2) return Week.TUESDAY;
if (value == 3) return Week.WEDNESDAY;
if (value == 4) return Week.THURSDAY;
if (value == 5) return Week.FIRDAY;
if (value == 6) return Week.SATURDAY;
if (value == 7) return Week.SUNDAY;
throw new ObjectStreamException();}
定义 readResolve
办法后,反序列化时新键的对象会通过调用 readResolve
办法返回的值称为 readObject
的返回值,因而 w == s
返回的就是 true
。
序列化实现克隆
可序列化的类能够应用序列化机制实现对象克隆。如下所示,为可序列化的 Student
类提供克隆办法。
public class Student implements Cloneable, Serializable {
...
@Override
public Object clone() throws CloneNotSupportedException {ByteArrayOutputStream bout = new ByteArrayOutputStream();
try {
// save the object to a byte array
try (ObjectOutputStream out = new ObjectOutputStream(bout)
) {out.writeObject(this);
}
// read a clone of the object from the byte array
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()))
) {return in.readObject();
}
} catch (IOException | ClassNotFoundException e) {CloneNotSupportedException ex = new CloneNotSupportedException();
ex.initCause(e);
throw ex;
}
}
}
如上所述,类想实现 clone
办法,须要实现 Cloneable
接口,之后调用该 clone
办法即可。应用 ByteArrayOutputStream
将数据保留到字节数组中,而不用将对象写出到文件中。
Student s = new Student();
s.setName("小赵");
s.setAge(24);
s.setScore(98.5);
Student sc = (Student) s.clone();
但这种形式也会比复制或克隆数据域的克隆办法要慢得多。当然,Java 序列化也是有危险的,最好是防止在程序中应用。
欢送关注公众号「海人为记」,期待与你共同进步!