1.什么是序列化和反序列化
java对象序列化的意思就是将对象的状态转化成字节流,以后可以通过这些值再生成相同状态的对象。对象序列化是对象持久化的一种实现方法,它是将对象的属性和方法转化为一种序列化的形式用于存储和传输。反序列化就是根据这些保存的信息重建对象的过程。
- 序列化:将java对象转化为字节序列的过程
- 反序列化:将字节序列转化为java对象的过程
2.序列化的作用
我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。
这里有个隐藏的问题,加入我们并不是两个java进程进行通信呢,而是java和js之间的通信呢?
3.序列化的几种方式
比较常见的作法有三种:
- 使用jdk的序列化和反序列化
(实现Serializalbe接口)
- 对象包装成JSON字符串
- Google的ProtoBuf
4.JDK的序列化和反序列化
序列化步骤
- 创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(“目标地址路径”));
- 通过对象输出流的writeObject()方法写对象:
out.writeObject("Hello");
out.writeObject(new Date());
反序列化步骤
- 创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:
ObjectInputStream in = new ObjectInputStream(new fileInputStream(“目标地址路径”));
- 通过对象输出流的readObject()方法读取对象:
String obj1 = (String)in.readObject();
Date obj2 = (Date)in.readObject();
说明:为了正确读取数据,完成反序列化,必须保证向对象输出流写对象的顺序与从对象输入流中读对象的顺序一致。
4.1 基本实现代码
public class TestStudent {
public static void main(String[] args) {
Student stu1 = new Student("a", "a1");
stu1.setAddress(new Address("ZZ"));
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
System.out.println(stu1);
try {
oos = new ObjectOutputStream(new FileOutputStream("D:/test/stu.txt"));
oos.writeObject(stu1);
oos.writeObject(stu1);
ois = new ObjectInputStream(new FileInputStream("D:/test/stu.txt"));
Student stu2 = (Student) ois.readObject();
Student stu3 = (Student) ois.readObject();
//Student stu4 = (Student) ois.readObject();
System.out.println(stu2);
System.out.println(stu3);
//System.out.println(stu4);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
if (oos != null) {
oos.close();
}
if(ois!= null){
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
如果是我们自定义的类,需要实现Serializalbe接口,才能进行序列化,否则会出现NotSerializableException异常。
4.2 java 序列化ID的作用
在以上的介绍中,我们在代码里会发现有这样一个变量:serialVersionUID,那么这个变量serialVersionUID到底具有什么作用呢?能不能去掉呢?
public class Student implements Serializable {
private static final long serialVersionUID = 6866904399011716299L;
private String stuId;
private transient String stuName;
private Address address;
//。。。。。。
}
序列化ID的作用:
其实,这个序列化ID起着关键的作用,它决定着是否能够成功反序列化!简单来说,java的序列化机制是通过在运行时判断类的serialVersionUID
来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID
与本地实体类中的serialVersionUID
进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。等会我们可以通过代码验证一下。
序列化ID如何产生:
当我们一个实体类中没有显示的定义一个名为“serialVersionUID”、类型为long的变量时,Java序列化机制会根据编译时的class自动生成一个serialVersionUID作为序列化版本比较,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID。譬如,当我们编写一个类时,随着时间的推移,我们因为需求改动,需要在本地类中添加其他的字段,这个时候再反序列化时便会出现serialVersionUID不一致,导致反序列化失败。那么如何解决呢?便是在本地类中添加一个“serialVersionUID”变量,值保持不变,便可以进行序列化和反序列化。
总结:
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)
。
4.3 如何使某些属性不被序列化进去
使用 transient 关键字
public class Student implements Serializable {
private static final long serialVersionUID = 6866904399011716299L;
private String stuId;
private transient String stuName;
private Address address;
//。。。。。。
}
Student [stuId=a, stuName=a1, address=Address [addr=ZZ]]
Student [stuId=a, stuName=null, address=Address [addr=ZZ]]
4.4 如何判断流中是否还有可读对象
oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(stu1);
oos.writeObject(stu1);
oos.writeObject(stu1);
oos.writeObject(stu1);
FileInputStream fis = new FileInputStream(file);
ois = new ObjectInputStream(fis);
while (fis.available()>0) {
System.out.println(ois.readObject());
}
4.5 覆盖写入对象
oos = new ObjectOutputStream(new FileOutputStream("D:/test/stu.txt", true));
4.6 java中序列化之子类继承父类序列化
父类实现了Serializable,子类不需要实现Serializable
相关注意事项
- 序列化时,只对对象的状态进行保存,而不管对象的方法;
- 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
- 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
- 并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如:
- 安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。transient标记的属性,也不会被实例化
- 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现
4.7 JDK序列化优缺点
优点
- java原生方法不依赖外部类库
缺点
- 字节数比较大,
不能跨语言
- 某些情况下对于对象属性的变化比较敏感
- 对象在进行序列化和反序列化的时候,必须实现Serializable接口,但并不强制声明唯一的serialVersionUID
5.JSON格式
public class UserVo{
private String name;
private int age;
private long phone;
private List<UserVo> friends;
// ...
}
UserVo src = new UserVo();
src.setName("Yaoming");
src.setAge(30);
src.setPhone(13789878978L);
UserVo f1 = new UserVo();
f1.setName("tmac");
f1.setAge(32);
f1.setPhone(138999898989L);
UserVo f2 = new UserVo();
f2.setName("liuwei");
f2.setAge(29);
f2.setPhone(138999899989L);
List<UserVo> friends = new ArrayList<UserVo>();
friends.add(f1);
friends.add(f2);
src.setFriends(friends);
采用Google的gson-2.2.2.jar
进行转义
Gson gson = new Gson();
String json = gson.toJson(src);
得到的字符串:
{"name":"Yaoming","age":30,"phone":13789878978,"friends":[{"name":"tmac","age":32,"phone":138999898989},{"name":"liuwei","age":29,"phone":138999899989}]}
字节数为153
优点
- 明文结构一目了然,可以跨语言,属性的增加减少对解析端影响较小
缺点
- 字节数过多,依赖于不同的第三方类库
6. Google ProtoBuf
protocol buffers 是google内部得一种传输协议,目前项目已经开源(http://code.google.com/p/protobuf/)
它定义了一种紧凑得可扩展得二进制协议格式,适合网络传输,并且针对多个语言有不同得版本可供选择。
- 参考文档: https://developers.google.com/protocol-buffers/docs/proto 语言指南
- Protobuf 是以message 的方式来管理数据的
- 支持跨平台、跨语言,即[客户端和服务器端可以是不同的语言编写的] (支持目前绝大多数语言,例如C++、C#、Java、python 等)
- 高性能,高可靠性
- 使用protobuf 编译器能自动生成代码,Protobuf 是将类的定义使用.proto 文件进行描述。说明,在idea 中编写.proto 文件时,会自动提示是否下载.ptotot 编写插件. 可以让语法高亮。
- 然后通过protoc.exe 编译器根据.proto 自动生成.java 文件
这篇文章仅仅介绍ProtoBuf,读者可自行查询学习其语法。