java平台允许我们在内存中创建可复用的内存对象,这些对象在JVM运行时才存在,JVM停止时就消失。现实应用中可能需要将这些内存对象持久化,以便后续JVM启动后再次读取。
序列化:将内存对象转换成另外一种可存储(持久化)或传输(网络通讯)的形式(json、xml)的过程。
反序列化:序列化的逆向,将存储后的文件或网络过来的字节数组转化成对象的过程。
简单通过socket写一个网络通讯的例子,如下:
网络传输对象User
@Getter
@ToString
public class User implements Serializable {
private static final long serialVersionUID = 1607879477192902264L;
private String name;
private int age;
private String sex;
public User setName(String name) {
this.name = name;
return this;
}
public User setAge(int age) {
this.age = age;
return this;
}
public User setSex(String sex) {
this.sex = sex;
return this;
}
}
socket服务端
public class ServerSocketDemo {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
ObjectInputStream oi = null;
try {
serverSocket = new ServerSocket(8080);
socket = serverSocket.accept();
oi = new ObjectInputStream(socket.getInputStream());
User user = (User)oi.readObject();
System.out.println(user);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally{
if (oi!=null){
try {
oi.close();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
socket客户端
public class SocketClientDemo {
public static void main(String[] args) {
Socket socket = null;
ObjectOutputStream os = null;
try {
socket = new Socket("127.0.0.1",8080);
os = new ObjectOutputStream(socket.getOutputStream());
User user = new User().setName("zhangsan").setAge(18).setSex("man");
os.writeObject(user);
os.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (os!=null){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
分别启动ServerSocketDemo与ServerClientDemo的main方法,执行结果如下:
java.io.NotSerializableException: com.learn.serialize.User
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.learn.serialize.SocketClientDemo.main(SocketClientDemo.java:20)
这说明User对象未实现序列化,不能在网络中传输。
大家都知道要实现网络传输,必须要实现Serializable接口,更改代码,让User implements Serializable ,看到如下结果确实是完成了网络传输,socket服务端输出了网络传输的对象反序列化后的结果。
那么究竟是如何实现序列化和反序列化的?为什么进行网络传输的对象实现了Serializable接口就可以进行网络传输的呢?接下来我们分析一下原理。
相信大家看到过很多实现了Serializable类会定义一个serialVersionUID变量(private static final long serialVersionUID = 1607879477192902264L;),serialVersionUID字面意义是序列化版本号,而变量值是ide根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段,也就意味着如果序列化的类的未做任何改变,这个变量值不会变化,但如果序列化后将这个类内容改变,则序列化前后两对象的版本号不一致,之前序列化好的内容就不能成功的反序列化。
是否是这个serialVersionUID变量决定了序列化呢?那我们通过下面例子,将对象序列化后存储在文件中,然后更改类内容,查看之前序列化的内容是否可以成功反序列化。如果可以,说明serialVersionUID与序列化无关;但如果不能成功序列化,说明就是serialVersionUID与序列化有关。示例如下:
User.java
@Getter
@ToString
public class User implements Serializable {
private static final long serialVersionUID = 1607879477192902264L;
private String name;
private int age;
private String sex;
public User setName(String name) {
this.name = name;
return this;
}
public User setAge(int age) {
this.age = age;
return this;
}
public User setSex(String sex) {
this.sex = sex;
return this;
}
}
JavaSerialize.java
public class JavaSerialize {
public byte[] serializeToFile(T obj) {
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("user"));;
oos.writeObject(obj);
oos.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (oos!=null){
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return new byte[0];
}
public T deserializeFromFile() {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(new File("user")));
return (T)ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally {
if (ois!=null){
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
测试类
public class Demo {
public static void main(String[] args) {
JavaSerialize javaSerialize = new JavaSerialize();
User user = new User().setName("zhangsan").setAge(18).setSex("man");
javaSerialize.serializeToFile(user);
System.out.println((User)javaSerialize.deserializeFromFile());
}
}
此时修改User.java类,类内容发生变化,UID需要重新生成。
@Getter
@ToString
public class User implements Serializable {
private static final long serialVersionUID = 8695436283467511144L;
private String name;
private int age;
private String sex;
private Integer id;
public void setId(Integer id) {
this.id = id;
}
public User setName(String name) {
this.name = name;
return this;
}
public User setAge(int age) {
this.age = age;
return this;
}
public User setSex(String sex) {
this.sex = sex;
return this;
}
}
将之前序列化到文件中user对象读到内存中:
public class Demo {
public static void main(String[] args) {
JavaSerialize javaSerialize = new JavaSerialize();
// User user = new User().setName("zhangsan").setAge(18).setSex("man");
// javaSerialize.serializeToFile(user);
System.out.println((User)javaSerialize.deserializeFromFile());
}
}
结果如下
null
java.io.InvalidClassException: com.learn.serialize.User; local class incompatible: stream classdesc serialVersionUID = 1607879477192902264, local class serialVersionUID = 8695436283467511144
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1883)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1749)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2040)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1571)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.learn.serialize.JavaSerialize.deserializeFromFile(JavaSerialize.java:45)
at com.learn.serialize.Demo.main(Demo.java:12)
Disconnected from the target VM, address: '127.0.0.1:6748', transport: 'socket'
明显可以看出因为serialVersionUID不一致,导致无法正确反序列化。正如上面所猜想的serialVersionUID变量与着序列化有关。
serialVersionUID两种生成方式:
显示指定:
1)指定为1L,类内容发生变化不会报错,只是部分反序列化的值缺失。(不建议使用)
2)ide自动生成,idea里面需要勾选Settings->Editior->Inspections->搜索UID->Serializable class withOut ‘serialVersionUID’。当实现了序列化的接口未定义serialVersionUID时,idea会提醒,在提醒的地方自动导入即可。
隐式指定:
如果没有为指定的class设置serialVersionUID,那么java编译器会自动给这个class进行一个摘要算法,当这个文件有任何改动,UID就会改变。
transient关键字修饰序列化类中的变量时,可以在序列化时,阻止该值序列化到文件中。
User.java类给age加上transient关键字修饰,将User对象(name=zhangsan, age=18, sex=man)序列化持久化到文件中,然后在反序列化得到的结果如下:
Connected to the target VM, address: '127.0.0.1:9919', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:9919', transport: 'socket'
User(name=zhangsan, age=0, sex=man)
Process finished with exit code 0
但User.java重写了writeObject方法、readObject方法,被transient修饰的字段经反序列化后得到的结果age仍然为18,结果如下所示:
User.java
@Getter
@ToString
public class User implements Serializable {
private static final long serialVersionUID = 1607879477192902264L;
private String name;
private transient int age;
private String sex;
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(age);
}
private void readObject(ObjectInputStream oi) throws IOException, ClassNotFoundException {
oi.defaultReadObject();
age=oi.readInt();
}
public User setName(String name) {
this.name = name;
return this;
}
public User setAge(int age) {
this.age = age;
return this;
}
public User setSex(String sex) {
this.sex = sex;
return this;
}
}
测试类
public class Demo {
public static void main(String[] args) {
JavaSerialize javaSerialize = new JavaSerialize();
User user = new User().setName("zhangsan").setAge(18).setSex("man");
javaSerialize.serializeToFile(user);
System.out.println((User)javaSerialize.deserializeFromFile());
}
}
结果
Disconnected from the target VM, address: '127.0.0.1:10034', transport: 'socket'
User(name=zhangsan, age=18, sex=man)
Process finished with exit code 0
为什么加上writeObject方法和readObject方法后就能改变transient的作用呢?跟踪源码发现序列化调用的是ObjectOutputStream的writeObject方法(将内存中的对象转换成另外一种格式存储或传输),反序列化调用的是ObjectInputStream的readObject方法(从持久化或网络上获取数据,反序列化成对象)。
以序列化为例,跟踪ObjectOutputStream的writeObject方法,发现底层有一层判断,当原序列化类没有重写writeObject方法时,调用defaultWriteFields(obj, slotDesc);重写了 writeObject方法时,会调用slotDesc.invokeWriteObject(obj, this)方法;
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slotDesc.hasWriteObjectMethod()) {
...
slotDesc.invokeWriteObject(obj, this);
...
} else {
defaultWriteFields(obj, slotDesc);
}
}
}
继续跟踪invokeWriteObject代码发现最终是利用反射调用的原对象中的writeObjectMethod定义的方法,writeObjectMethod变量定义的方法为下面第二张图所示私有的writeObject方法,且参数类型为ObjectOutPutStream,返回值类型为void的方法(所以在重写的时候要严格按照这里的定义重写writeObject方法,否则就表示未重写writeObject方法,这里就调用不到了)。因此可以利用重写该方法,实现序列化特殊处理。
ArrayList中也重写了这个方法,如下所示:
java序列化只针对对象的状态,不关注对象的方法
当父类实现了序列化,子类自动实现序列化
当字段被transient修饰时,序列化会自动的忽略这个字段
重写writeObject和readObject方法可以实现序列化特殊处理