序列化的意义
1.永久存储某个jvm中运行时的对象。
2.对象可以网络传输
3.rmi调用都是以序列化的方式传输参数
基本知识
1.在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。
2.通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化
3.虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)
4.序列化并不保存静态变量。
5.要想将父类对象也序列化,就需要让父类也实现Serializable 接口。
6.Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
7.服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。(自定义实现readObject和writeObject)
一些杂事
1.将统一个对象连续多次写入一个文件,Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得清单 3 中的 t1 和 t2 指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("result.obj"));
Test test = new Test();
//试图将对象两次写入文件
out.writeObject(test);
out.flush();
System.out.println(new File("result.obj").length());
out.writeObject(test);
out.close();
System.out.println(new File("result.obj").length());
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
"result.obj"));
//从文件依次读出两个文件
Test t1 = (Test) oin.readObject();
Test t2 = (Test) oin.readObject();
oin.close();
//判断两个引用是否指向同一个对象
System.out.println(t1 == t2);
2.arraylist源码,elementData为什么是transient的
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
transient Object[] elementData; // non-private to simplify nested class access
private int size;
}
ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient。
3.自定义序列化方式writeObject,readObject
以arraylist的readObject为例
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject(); //如果不自己定义readObject方法,默认就是调用ObjectInputStream的defaultReadObject方法。
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
Serializable的几个自定义神器方法
一. 解决反序列化异常的神器——Serializable接口的readObjectNoData方法
- 方法定义:private void readObjectNoData() throws ObjectStreamException
- 这个方法可能会让你摸不着头脑,测试的时候怎么都不会被执行?,哈哈哈!这里是真相:如果有一个类A正在被反序列化,S是它现在的父类,readObjectNoData方法也应该存在与S类中。当发生以下条件时,将触发这个readObjectNoData方法被执行:
1.S类直接或间接实现了Serializable接口
2.S类必须定义实现readObjectNoData方法
3.当前待被反序列化的stream在被序列化时A类还没有进程S类。
The readObjectNoData() method is analogous to the class-defined readObject() method, except that (if defined) it is called in cases where the class descriptor for a superclass of the object being deserialized (and hence the object data described by that class descriptor) is not present in the serialization stream.
Note that readObjectNoData() is never invoked in cases where a class-defined readObject() method could be called, though serializable class implementors can call readObjectNoData() from within readObject() as a means of consolidating initialization code.
二. writeReplace,如果实现了writeReplace方法后,那么在序列化时会先调用writeReplace方法将当前对象替换成另一个对象(该方法会返回替换后的对象)并将其写入流中,例如:
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return name + "(" + age + ")";
}
private Object writeReplace() throws ObjectStreamException {
ArrayList<Object> list = new ArrayList<>();
list.add(name);
list.add(age);
return list;
}
}
在这里就将该对象直接替换成了一个list保存;
- 实现writeReplace就不要实现writeObject了,实现了也不会被调用;
- writeReplace的返回值(对象)必须是可序列话的,如果是Java自己的基础类或者类型那就不用说了;
- writeReplace替换后在反序列化时就无法被恢复了,即对象被彻底替换了!也就是说使用ObjectInputStream读取的对象只能是被替换后的对象,只能在读取后自己手动恢复了,接着上例的演示:
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return name + "(" + age + ")";
}
private Object writeReplace() throws ObjectStreamException {
ArrayList<Object> list = new ArrayList<>();
list.add(name);
list.add(age);
return list;
}
}
public class Test {
public static void print(String s) {
System.out.println(s);
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.buf"))) {
Person p = new Person("lala", 33);
oos.writeObject(p);
oos.close();
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.buf"))) {
print(((ArrayList)ois.readObject()).toString());
}
}
}
- 使用writeReplace替换写入后也不能通过实现readObject来实现自动恢复了,即下面的代码是错误的!因为默认已经被彻底替换了,就不存在自定义反序列化的问题了,直接自动反序列化成ArrayList了,该方法即使实现了也不会调用!
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return name + "(" + age + ")";
}
private Object writeReplace() throws ObjectStreamException {
ArrayList<Object> list = new ArrayList<>();
list.add(name);
list.add(age);
return list;
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { // 错!写了也没用
ArrayList<Object> list = (ArrayList)in.readObject();
this.name = (String)list.get(0);
this.age = (int)list.get(1);
}
}
所以,writeObject只和readObject配合使用,一旦实现了writeReplace在写入时进行替换就不再需要writeObject和readObject了!因为替换就已经是彻底的自定义了,比writeObject/readObject更彻底!
三. 保护性恢复对象(同时也可以替换对象)——readResolve:
- readResolve会在readObject调用之后自动调用,它最主要的目的就是让恢复的对象变个样,比如readObject已经反序列化好了一个Person对象,那么就可以在readResolve里再对该对象进行一定的修改,而最终修改后的结果将作为ObjectInputStream的readObject的返回结果;
- 可以返回一个非原类型的对象,也就是说可以彻底替换对象
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return name + "(" + age + ")";
}
private Object readResolve() throws ObjectStreamException { // 直接替换成一个int的1返回
return 1;
}
}
- 其最重要的应用就是保护性恢复单例、枚举类型的对象!
枚举问题示例:
class Brand implements Serializable {
private int val;
private Brand(int val) {
this.val = val;
}
// 两个枚举值
public static final Brand NIKE = new Brand(0);
public static final Brand ADDIDAS = new Brand(1);
}
public class Test {
public static void print(String s) {
System.out.println(s);
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.buf"))) {
oos.writeObject(Brand.NIKE);
oos.close();
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.buf"))) {
Brand b = (Brand)ois.readObject();
print("" + (b == Brand.NIKE)); // 答案显然是false !!!!!!!!!因为Brand.NIKE是程序中创建的对象,而b是从磁盘中读取并恢复过来的对象,两者明显来源不同,因此必然内存空间是不同的,引用(地址)显然也是不同的
}
}
}
使用readResolve解决这个问题:
class Brand implements Serializable {
private int val;
private Brand(int val) {
this.val = val;
}
// 两个枚举值
public static final Brand NIKE = new Brand(0);
public static final Brand ADDIDAS = new Brand(1);
//这样就能保证反序列化回来的枚举能相等。
private Object readResolve() throws ObjectStreamException {
if (val == 0) {
return NIKE;
}
if (val == 1) {
return ADDIDAS;
}
return null;
}
}
四. 强制自己实现串行化和反串行化——Externalizable接口
- 不像Serializable接口只是一个标记接口,里面的接口方法都是可选的(可实现可不实现,如果不实现则启用其自动序列化功能),而Externalizable接口不是一个标记接口,它强制你自己动手实现串行化和反串行化算法:
//可以看到该接口直接继承了Serializable接口,其两个方法其实就对应了Serializable的writeObject和readObject方法,实现方式也是一模一样;
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException; // 串行化
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; // 反串行化
}
- ObjectOutput和ObjectInput的使用方法和ObjectOutputStream和ObjectInputStream一模一样(readObject、readInt之类的,完全一模一样),因此Externalizable就是强制实现版的Serializable罢了;
class Person implements Externalizable {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// TODO Auto-generated method stub
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// TODO Auto-generated method stub
this.name = ((StringBuffer)in.readObject()).reverse().toString();
this.age = in.readInt();
}
}
- 和Serializable的主要区别:
1.效率比Serializable高
2.但Serializable更加灵活,其最重要的特色就是可以自动序列化,这也是其应用更广泛的原因
3.使用Externalizable进行序列化时,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。因此,必须提供一个无参构造器,访问权限为public;否则会抛出java.io.InvalidClassException 异常;Serializable序列化时不会调用默认的构造器