1、基本概念
1.1 什么是序列化和反序列化
(1)Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程;
(2)序列化:对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。
(3)反序列化:客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
(4)本质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。
1.2 为什么需要序列化与反序列化
我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。
还有,当互联网电商项目并发访问很大的时候,数百万用户产生数百万个session对象,内存可能吃不消,同时用户登录后不一定需要时时刻刻用到该session对象,那么我们的web容器可以将当前并没有使用的session对象序列化到磁盘中,等到需要使用的时候再反序列化为对象!
那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的!如何做到呢?这就需要Java序列化与反序列化了!
换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。
好处一:实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里)。
好处二:利用序列化实现远程通信,即在网络上传送对象的字节序列。
总的来说可以归结为以下几点:
(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中;
(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;
(3)通过序列化在进程间传递对象。
1.3 序列化算法步骤
(1)将对象实例相关的类元数据输出。
(2)递归地输出类的超类描述直到不再有超类。
(3)类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
(4)从上至下递归输出实例的数据。
2、实现序列化和反序列化
对象的类型是String,或数组,或Enum,或Serializable,那么就可以对该对象进行序列化,否则将抛出NotSerializableException。
2.1 JDK类库API
2.1.1 实现方式
(1)java.io.ObjectOutputStream:表示对象输出流;
它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中;
(2)java.io.ObjectInputStream:表示对象输入流;
它的readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回;
PS:只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常!
@Testpublic void test1() throws IOException, ClassNotFoundException {//序列化到对象输出流中,文件类型可以为任何类型 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")); User user1 = new User(1, "Jack", "123456"); List list = new ArrayList(); list.add("abc"); list.add("cba"); user1.setOrders(list); Role role = new Role(); role.setRoleName("Administrator"); user1.setRole(role); oos.writeObject(user1); oos.flush(); oos.close(); //从文件中读取字节序列,反序列化为对象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt")); User user2 = (User) ois.readObject(); ois.close(); System.out.println(user2);}@Test public void test2() throws IOException, ClassNotFoundException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); //序列化到对象输出流中,文件类型可以为任何类型 ObjectOutputStream oos = new ObjectOutputStream(baos); User user1 = new User(1, "Jack", "123456"); oos.writeObject(user1); byte[] bytes = baos.toByteArray(); System.out.println(Arrays.toString(bytes)); /*[-84, -19, 0, 5, 115, 114, 0, 20, 99, 111, 109, 46, 100, 111, 110, 103, 122, 108, 46, 112, 111, 106, 111, 46, 85, 115, 101, 114, 108, 124, -59, -89, -28, 34, 123, -76, 3, 0, 5, 73, 0, 2, 105, 100, 76, 0, 6, 111, 114, 100, 101, 114, 115, 116, 0, 16, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 76, 105, 115, 116, 59, 76, 0, 8, 112, 97, 115, 115, 119, 111, 114, 100, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 4, 114, 111, 108, 101, 116, 0, 22, 76, 99, 111, 109, 47, 100, 111, 110, 103, 122, 108, 47, 112, 111, 106, 111, 47, 82, 111, 108, 101, 59, 76, 0, 8, 117, 115, 101, 114, 110, 97, 109, 101, 113, 0, 126, 0, 2, 120, 114, 0, 22, 99, 111, 109, 46, 100, 111, 110, 103, 122, 108, 46, 112, 111, 106, 111, 46, 80, 97, 114, 101, 110, 116, 8, -97, -83, 13, 38, 14, -89, 30, 2, 0, 1, 76, 0, 3, 115, 101, 120, 113, 0, 126, 0, 2, 120, 112, 112, 116, 0, 9, 116, 114, 97, 110, 115, 105, 101, 110, 116, 120] */ oos.flush(); oos.close(); //从文件中读取字节序列,反序列化为对象 ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); User user2 = (User) ois.readObject(); ois.close(); System.out.println(user2); }//这个地方先标注下打印出来的字节数组,等下跟protobuf的对比/*[-84, -19, 0, 5, 115, 114, 0, 20, 99, 111, 109, 46, 100, 111, 110, 103, 122, 108, 46,112, 111, 106, 111, 46, 85, 115, 101, 114, 108, 124, -59, -89, -28, 34, 123, -76, 3, 0, 5,73, 0, 2, 105, 100, 76, 0, 6, 111, 114, 100, 101, 114, 115, 116, 0, 16, 76, 106, 97, 118,97, 47, 117, 116, 105, 108, 47, 76, 105, 115, 116, 59, 76, 0, 8, 112, 97, 115, 115, 119, 111,114, 100, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110,103, 59, 76, 0, 4, 114, 111, 108, 101, 116, 0, 22, 76, 99, 111, 109, 47, 100, 111, 110, 103,122, 108, 47, 112, 111, 106, 111, 47, 82, 111, 108, 101, 59, 76, 0, 8, 117, 115, 101, 114, 110,97, 109, 101, 113, 0, 126, 0, 2, 120, 114, 0, 22, 99, 111, 109, 46, 100, 111, 110, 103, 122, 108,46, 112, 111, 106, 111, 46, 80, 97, 114, 101, 110, 116, 8, -97, -83, 13, 38, 14, -89, 30, 2, 0,1, 76, 0, 3, 115, 101, 120, 113, 0, 126, 0, 2, 120, 112, 112, 116, 0, 9, 116, 114, 97, 110, 115,105, 101, 110, 116, 120]*/2.1.2 相关注意事项
1、序列化时,只对对象的状态进行保存,而不管对象的方法;
2、当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
3、当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
4、并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如:如果仅仅让某个类实现Serializable接口,而没有其它任何处理的话,则就是使用默认序列化机制。
使用默认机制在序列化对象时,不仅会序列化当前对象,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。
所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。
5、声明为transient类型的成员数据不能被序列化。因为transient代表对象的临时数据。如果想让该字段再次可以被序列化(选择性序列化),在类中添加两个方法:writeObject()与readObject()
// writeObject()会先调用ObjectOutputStream中的defaultWriteObject()方法,该方法会执行默认的序列化机制,此时会忽略掉age字段。// 然后再调用writeInt()方法显示地将age字段写入到 ObjectOutputStream中。private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(VALUE);}// readObject()的作用则是针对对象的读取,其原理与writeObject()方法相同。private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
VALUE = (String) in.readObject();}必须注意的是,writeObject()与readObject()都是private方法,那么它们是如何被调用的呢?
毫无疑问,使用反射。详情可以看看ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。这两个方法会在序列化、反序列化的过程中被自动调用。且不能关闭流,否则会导致序列化操作失败。
6、Java有很多基础类已经实现了serializable接口,比如String,Vector等。但是也有一些没有实现serializable接口的;
Java提供了另一个序列化接口 Externalizable,Externalizable 继承于 Serializable,当使用该接口时,序列化的细节需要由程序员去完成,实现的writeExternal()与readExternal()方法对需要序列化的字段处理,如果都不处理,那么这个序列化相当于白干。
public class Order implements Externalizable {private static final long serialVersionUID = 8334617098942116608L; private int id; private String orderName; private double price; public Order(int id, String orderName, double price) {this.id = id; this.orderName = orderName; this.price = price; }@Override public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(id); out.writeObject(orderName); }@Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {id = in.readInt(); orderName = (String)in.readObject(); }public Order() {
}@Override public String toString() {return "Order{" +"id=" + id +", orderName='" + orderName + '\'' +", price=" + price +'}'; }public void sayHello(){
System.out.println("hello serializable world"); }
}7、序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:
- 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
- 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
@Testpublic void test2() throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")); Order order = new Order(1, "商品", 9999.99); oos.writeObject(order); oos.flush(); oos.close(); //从文件中读取字节序列,反序列化为对象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt")); Object obj = ois.readObject(); ois.close(); System.out.println(obj);}//执行一次后将该测试类序列化的代码去掉,只进行反序列化@Testpublic void test3() throws IOException, ClassNotFoundException {//从文件中读取字节序列,反序列化为对象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt")); Object obj = ois.readObject(); ois.close(); System.out.println(obj);}此时可以正常的序列化,现在将Order类的serialVersionUID改一下,再次执行反序列化,会报异常:
java.io.InvalidClassException: com.dongzl.pojo.Order; local class incompatible:
stream classdesc serialVersionUID = 8334617098942116608, local class
serialVersionUID = 8334617098942116609
8、如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因。
2.2 JSON方式
我们使用比较有代表性的Gson插件来进行序列化转换。
引入依赖jar包:
com.google.code.gson gson 2.8.0@Test public void testGson() throws IOException {
Gson gson = new Gson(); User user = new User(1, "Jack", "123456"); //序列化 String json = gson.toJson(user); byte[] bytes = json.getBytes(); System.out.println(Arrays.toString(bytes)); //反序列化 User decodeUser = gson.fromJson(new String(bytes), User.class); System.out.println(decodeUser); //生成序列化文件 OutputStream os = new FileOutputStream("object2.txt"); os.write(bytes); os.flush(); os.close(); }//打印的字节数组/*[123, 34, 105, 100, 34, 58, 49, 44, 34, 117, 115, 101, 114, 110, 97,109, 101, 34, 58, 34, 74, 97, 99, 107, 34, 44, 34, 112, 97, 115, 115, 119,111, 114, 100, 34, 58, 34, 49, 50, 51, 52, 53, 54, 34, 125]*/2.3 谷歌ProtoBuf协议组件
2.3.1 为什么要使用protobuf
使用protobuf的原因肯定是为了解决开发中的一些问题,那使用其他的序列化机制会出现什么问题呢?
(1)java默认序列化机制:效率极低,而且还能不能跨语言之间共享数据。
(2)XML常用于与其他项目之间数据传输或者是共享数据,但是编码和解码会造成很大的性能损失。
(3)json格式也是常见的一种,但是在json在解析的时候非常耗时,而且json结构非常占内存。
但是我们protobuf是一种灵活的、高效的、自动化的序列化机制,可以有效的解决上面的问题。现在应该清楚了吧,正是由于目前的机制存在了很多问题,所以才有了这个序列化框架。
并且是语言跨平台的(基于Shell命令执行)
2.3.2 ProtoBuf入门使用
2.3.2.1、下载安装并配置环境变量
下载地址:
https:///protocolbuffers/protobuf/releases
选择你喜欢的版本

配置Path环境变量,指定protoc.exe执行程序路径。然后,打开CMD,输入protoc --version,显示版本号即可。
2.3.2.2 手动命令实现(略)
语法:protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
PS:也可以生成C++文件,区别在于java_out改为cpp_out
2.3.2.3 Java代码实现
导入依赖jar包:
com.google.protobuf protobuf-java 3.7.1先根据proto2或者proto3的语法创建一个.proto文件,下面是.proto数据类型和Java类型的对照表

定义一个命令类,用来根据.proto文件生成对应的序列化类:
public class Cmd {/** * 使用java process执行shell命令 * * @param commandList */ public void execute(List commandList) {
ProcessBuilder pb = new ProcessBuilder(commandList); pb.redirectErrorStream(true); Process p; int i = 1; try {
p = pb.start(); try {//jdk实现process时,调用外部命令不是同步的调用,而是异步执行,需要等待执行完成 i = p.waitFor(); } catch (InterruptedException e) {
e.printStackTrace(); }
} catch (IOException e) {throw new RuntimeException(e); }int iResult = p.exitValue(); if (iResult == 0 && i == 0) {
System.out.println(" result = " + p.exitValue() + ", execute command success! Command = " + commandList); } else {
System.out.println(" result = " + p.exitValue() + ", execute command failure! Command = " + commandList); }
}
}// protoc的目录private static final String PROTOC_FILE = System.getProperty("user.dir") + "\\src\\main\\resources\\protoc.exe";// .proto文件所在项目根目录private static final String IMPOR_TPROTO = System.getProperty("user.dir");// 生成java类输出目录private static final String JAVA_OUT = System.getProperty("user.dir") + "\\src\\main\\java";// 指定proto文件private static final String PROTOS = System.getProperty("user.dir") + "\\src\\main\\java\\com\\dongzl\\protobuf\\user.proto";@Testpublic void test3() {
List lCommand = new ArrayList(); lCommand.add(PROTOC_FILE); lCommand.add("-I=" + IMPOR_TPROTO); lCommand.add("--java_out=" + JAVA_OUT); lCommand.add(PROTOS); Cmd cmd = new Cmd(); cmd.execute(lCommand);}成功生成:com.dongzl.template.UserSerializable
序列化和反序列化测试:
@Test public void test4() throws IOException {//序列化 UserSerializable.User.Builder builder = UserSerializable.User.newBuilder(); builder.setId(1)
.setUsername("Jack")
.setPassword("123456"); UserSerializable.User user = builder.build(); byte[] bytes = user.toByteArray(); System.out.println(Arrays.toString(bytes)); //反序列化 UserSerializable.User decodeUser = UserSerializable.User.parseFrom(bytes); System.out.println(decodeUser); //生成序列化文件 OutputStream os = new FileOutputStream("object3.txt"); os.write(bytes); os.flush(); os.close(); }//protobuf打印的结果/*[8, 1, 18, 4, 74, 97, 99, 107, 26, 6, 49, 50, 51, 52, 53, 54]*///Gson打印的结果/*[123, 34, 105, 100, 34, 58, 49, 44, 34, 117, 115, 101, 114, 110, 97,109, 101, 34, 58, 34, 74, 97, 99, 107, 34, 44, 34, 112, 97, 115, 115, 119,111, 114, 100, 34, 58, 34, 49, 50, 51, 52, 53, 54, 34, 125]*///jdk打印的结果/*[-84, -19, 0, 5, 115, 114, 0, 20, 99, 111, 109, 46, 100, 111, 110, 103, 122, 108, 46,112, 111, 106, 111, 46, 85, 115, 101, 114, 108, 124, -59, -89, -28, 34, 123, -76, 3, 0, 5,73, 0, 2, 105, 100, 76, 0, 6, 111, 114, 100, 101, 114, 115, 116, 0, 16, 76, 106, 97, 118,97, 47, 117, 116, 105, 108, 47, 76, 105, 115, 116, 59, 76, 0, 8, 112, 97, 115, 115, 119, 111,114, 100, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110,103, 59, 76, 0, 4, 114, 111, 108, 101, 116, 0, 22, 76, 99, 111, 109, 47, 100, 111, 110, 103,122, 108, 47, 112, 111, 106, 111, 47, 82, 111, 108, 101, 59, 76, 0, 8, 117, 115, 101, 114, 110,97, 109, 101, 113, 0, 126, 0, 2, 120, 114, 0, 22, 99, 111, 109, 46, 100, 111, 110, 103, 122, 108,46, 112, 111, 106, 111, 46, 80, 97, 114, 101, 110, 116, 8, -97, -83, 13, 38, 14, -89, 30, 2, 0,1, 76, 0, 3, 115, 101, 120, 113, 0, 126, 0, 2, 120, 112, 112, 116, 0, 9, 116, 114, 97, 110, 115,105, 101, 110, 116, 120]*/起码我们现在可以看到,同样的一个User对象,protobuf序列化的字节码数组明显小太多,那么如果大量这样的对象在网络中传输的话,对于带宽的消耗是否要低得多,是不是速度会更快!
当然,JSON序列化的方式比较合理,所以要注意,在对性能没有极限要求的情况下,用JSON进行序列化即可,毕竟使用protobuf需要的学习成本比较大。
2.3.3 ProtoBuf的实现机制
从上面我们看到,ProtoBuf在网络传输中的字节流数组非常小,这是为什么呢?
我们先来看看三种情况下生成的序列化文件:
JDK:

Gson:

ProtoBuf:

发现什么了没?
JDK的最复杂,0000000h-000000c0h表示行号;0-f表示列;行后面的文字表示对这行16进制的解释。
Gson次之,包括属性,括号等。
ProtoBuf只剩下顺序的属性值了!
@java.lang.Overridepublic void writeTo(com.google.protobuf.CodedOutputStream output)throws java.io.IOException {if (id_ != 0) {
output.writeInt32(1, id_);//指定位置1 }if (!getUsernameBytes().isEmpty()) {
com.google.protobuf.GeneratedMessageV3.writeString(output, 2, username_);//指定位置2 }if (!getPasswordBytes().isEmpty()) {
com.google.protobuf.GeneratedMessageV3.writeString(output, 3, password_);//指定位置3 }for (int i = 0; i < orders_.size(); i++) {
com.google.protobuf.GeneratedMessageV3.writeString(output, 4, orders_.getRaw(i));//指定位置4 }
unknownFields.writeTo(output);}另外,ProtoBuf还有最大的特性:数据动态伸缩
打个比方,对于int age = 35这个成员变量来说,如果是JSON或者JDK的情况下,不管怎么样,都是占四个字节,而ProtoBuf采用可伸缩的机制,根据你实际的值来确定所占字节(int(1-5)最多五个字节),那么对于问题很明朗了,世界上超过100岁的人不多吧,超过1000岁的那是神仙了,所以ProtoBuf在age这个成员变量上最多消耗两个字节位!空间就这么省出来了!
public final void writeUInt32NoTag(int value) throws IOException {if (CodedOutputStream.HAS_UNSAFE_ARRAY_OPERATIONS && this.spaceLeft() >= 10) {while((value & -128) != 0) {//010000000(128) //110000000(-128) ---->010000000 != 0 UnsafeUtil.putByte(this.buffer, (long)(this.position++), (byte)(value & 127 | 128)); value >>>= 7; //010000000右移七位 --->01 }
UnsafeUtil.putByte(this.buffer, (long)(this.position++), (byte)value); } else {try {while((value & -128) != 0) {this.buffer[this.position++] = (byte)(value & 127 | 128); value >>>= 7; }this.buffer[this.position++] = (byte)value; } catch (IndexOutOfBoundsException var3) {throw new CodedOutputStream.OutOfSpaceException(String.format("Pos: %d, limit: %d, len: %d", this.position, this.limit, 1), var3); }
}
}面试重点:
1、只会序列化对象状态(信息),而不是序列化我们的对象结构。
2、如果父类序列化了,子类自动实现序列化,不需要实现Serializable接口。
3、对象引用其他对象,那么会将引用的对象一起序列化。注意:引用的对象要实现Serializable接口
4、并非所有的对象都要进行序列化。比如Spring中类似于容器的这种类对象,那么不要去序列化ClassPathXmlApplicationContext。总结就是类对象越复杂,开销越大。解决思路是拆分。
5、transient类型的成员变量不能被序列化,如果想序列化,你可以通过重写。writeObject和readObject方法来实现指定字段序列化。
6、实体类可以通过实现Externalizable接口,重写write和read方法实现序列化和反序列化。
7、可以通过控制serialVersionUID 来达到允许兼容或者说不兼容的问题。
- 如果希望不同版本相互兼容,那么serialVersionUID 需要相同。
- 如果不希望不同版本相互兼容,那么serialVersionUID 需要不同。
8、引用对象的序列化其实就是深拷贝。
















