网站关键词密度,网站开发的策划方案,百度手机网站制作,南昌网站建设公司信息基本概念
Java 序列化和反序列化三连问#xff1a;
什么是 Java 序列化和反序列化#xff1f;为什么需要 Java 序列化和反序列化#xff1f;如何实现 Java 序列化和反序列化#xff1f;
是什么
一句话就能够说明白什么是 Java 序列化和反序列化#xff1f;Java 序列化…
基本概念
Java 序列化和反序列化三连问
什么是 Java 序列化和反序列化为什么需要 Java 序列化和反序列化如何实现 Java 序列化和反序列化
是什么
一句话就能够说明白什么是 Java 序列化和反序列化Java 序列化是将 Java 对象转换为字节序列的过程而 Java 反序列化则是将字节序列恢复为 Java 对象的过程。
序列化任何需要保存到磁盘或者在网络进行传输的 Java 对象都需要支持序列化序列化后的字节流保存了 Java 对象的状态及相关的描述信息反序列化能够根据这些信息“复刻”出一个一模一样的对象。序列化的核心作用就是对象状态的保存。反序列化反序列化就是根据磁盘中保存的或者网络上传输的字节流中所保存的对象状态和相关描述信息通过反序列化重建对象。
所以从本质上来说序列化就是将对象的状态和相关描述信息按照一定的格式写入到字节流中而反序列化则是从字节流中重建这个对象。
为什么
为什么需要 Java 序列化和反序列化呢有两个原因
持久化。即将该对象保存到磁盘中。一般来说我们是不需要持久化 Java 对象的但是如果遇到特殊情况我们需要将 Java 对象持久化到磁盘中以便于我们在重启 JVM 时可以重建这些 Java 对象。所以我们可以通过序列化的方式将 Java 对象转换成字节流然后将这些字节流保存到磁盘中实现持久化。在我们应用程序重启时可以读取这些字节流进行反序列化还原 Java 对象。网络传输我们都知道网络上传输的对象是二进制字节流我们是无法传输一个 Java 对象给一个应用的所以在传输前我们需要对 Java 对象进行序列化将其转换为字节流。而接收方则根据字节流中所包含的信息重建该 Java 对象。
怎么做
在 Java 中如果一个对象要想实现序列化它有两种方式
实现 Serializable 接口实现 Externalizable 接口
这两个接口是如何工作的呢又有什么区别呢下面我们分别介绍。
Java 如何实现序列化和反序列化
Serializable 接口
Serializable 接口只是一个标记接口不用实现任何方法。一个对象只要实现了该接口就意味着该对象是可序列化的。
序列化
Java 对象序列化的步骤如下
对象实现 Serializable 接口创建一个 ObjectOutputStream 输出流调用 ObjectOutputStream 对象的 writeObject() 输出可序列化对象
如下
Data
ToString
NoArgsConstructor
AllArgsConstructor
public class Person implements Serializable {private String name;private Integer age;private Float height;
}public class Serializable01 {public static void main(String[] args) throws Exception {ObjectOutputStream oos new ObjectOutputStream(new FileOutputStream(person01.txt));Person person01 new Person(张三,35,175.4F);oos.writeObject(person01);}
}
用 idea 打开 person01.txt 文件就可以得到如下内容 从这个文件中我们基本上可以看清楚 Person01 对象的字节流的轮廓。
反序列化
Java 反序列化步骤如下
对象实现 Serializable 接口创建一个 ObjectInputStream 对象调用 ObjectInputStream 对象的 readObject()
ObjectInputStream ois new ObjectInputStream(new FileInputStream(person01.txt));
Person person011 (Person01) ois.readObject();
System.out.println(person01.txt 反序列化内容 person011.toString());运行结果
person01.txt 反序列化内容Person01(name张三, age35, height175.4)反序列化生成的对象和序列化的对象内容一模一样完全还原了序列化时的对象。
成员为引用的序列化
上面的例子 Person 的成员变量都是基本类型如果成员变量为引用类型呢
我们去掉 Person 类实现的 Serializable 接口然后定义一个 Women 类。
public class Person {private String name;private Integer age;private Float height;
}public class Woman implements Serializable {private String hairColor;private Person person;
}
我们再来序列化 Woman 这类
public class Serializable02 {public static void main(String[] args) throws Exception {ObjectOutputStream oos new ObjectOutputStream(new FileOutputStream(woman.txt));Person person new Person(李四,30,180F);Woman woman new Woman(黄颜色,person);oos.writeObject(woman);}
}执行时你会发现程序会抛出异常
java.io.NotSerializableException: com.sike.javacore.serializer.serializable.dto.Person
...所以一个可序列化的类如果它含有引用类型的成员变量那么这个引用类型也必须是可序列化的。
自定义序列化
有些时候我们并不需要将一个对象的所有属性全部序列化这个时候我们可以使用 transient 关键字来选择不需要序列化的字段。
transient** 的作用就是用来标识一个成员变量在序列化应该被忽略。**
public class Person_1 implements Serializable {private String name;// 标识为 transientprivate transient Integer age;private Float height;
}将 age 属性标识为 transient。
public class Serializable03 {public static void main(String[] args) throws Exception {// 先序列化ObjectOutputStream oos new ObjectOutputStream(new FileOutputStream(person_1.txt));Person_1 person new Person_1(王五,32,180F);oos.writeObject(person);System.out.println(原对象 person);// 再反序列化ObjectInputStream ois new ObjectInputStream(new FileInputStream(person_1.txt));Person_1 person1 (Person_1) ois.readObject();System.out.println(序列化后对象 person1);}
}运行结果
原对象Person_1(name王五, age32, height180.0)
序列化后对象Person_1(name王五, agenull, height180.0)从运行结果我们可以看出用 transient 标识的属性在进行序列化时会将该字段忽略然后在反序列化的时候被 transient 标识的属性会被设置为默认值。
Externalizable 接口
一个类除了实现 Serializable 接口外来实现序列化还有一种更加灵活的方式来实现序列化实现 Externalizable 接口。
Externalizable 接口是 Serializable 的子类它提供了 writeExternal() 和 readExternal() 方法让类能够更加灵活地实现序列化。
public interface Externalizable extends java.io.Serializable {void writeExternal(ObjectOutput out) throws IOException;void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}一个类如果实现了 Externalizable 接口即必须要实现 writeExternal() 和 readExternal() 两个方法。在这两个方法里面你可以做自己任何想做的事情。
public class Student implements Externalizable {private String name;private int age;private int grade;Overridepublic void writeExternal(ObjectOutput out) throws IOException {out.writeObject(name);out.writeInt(age - 2); // 年龄我虚报 2 岁// 成绩我不报了}Overridepublic void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {this.name (String) in.readObject();this.age in.readInt();}
}public class Serializable04 {public static void main(String[] args) throws Exception {// 先序列化ObjectOutputStream oos new ObjectOutputStream(new FileOutputStream(student.txt));Student student new Student(小明,15,55);oos.writeObject(student);System.out.println(序列化对象内容 student);ObjectInputStream ois new ObjectInputStream(new FileInputStream(student.txt));Student student1 (Student) ois.readObject();System.out.println(序列化后的内容 student1);}
}
运行结果
序列化对象内容Student(name小明, age15, grade55)
序列化后的内容Student(name小明, age13, grade0)根据运行结果我们看到Externalizable 接口可以实现自定义的序列化和反序列化。
但是使用 Externalizable 接口时要注意writeExternal() 方法和 readExternal() 的顺序要一致即 writeExternal() 是按照怎么样的顺序来 write 值的readExternal() 就必须严格按照这个顺序来 read 否则会报错。有兴趣的小伙伴可以 name 和 age 的顺序调整下就知道了。
Serializable 和 Externalizable 对比
serializableExternalizable系统自动存储 Java 对象必要的信息程序员自己来实现 Java 对象的序列化灵活度更加高不需要的属性使用 transient 修饰不需要的属性可以不写入对象在反序列化的时候不走构造方法反序列化时先走无参构造方法得到一个空对象在调用 readExternal() 方法来读取序列化文件中的内容给该空对象赋值
serialVersionUID 版本号
我们先看一个例子。
我们先将 Student 对象序列化到本地磁盘 student.txt 文件中然后在 Student 类里面增加一个字段比如 className用来表示所在的班级然后再用刚刚已经序列化的 student.txt 来反序列化试图还原 Student 对象这个时候你会发现运行报错抛出下面的异常
Exception in thread main java.io.InvalidClassException: com.sike.javacore.serializer.serializable.dto.Student; local class incompatible: stream classdesc serialVersionUID -1065600830313514941, local class serialVersionUID 2126309100823681
异常信息说明序列化前后的 serialVersionUID 不一致。一个是 serialVersionUID -1065600830313514941另外一个是 serialVersionUID 2126309100823681。
为什么两个 serialVersionUID 会不一样呢因为我们对 Student 类做了变更即所谓的升级。
在我们实际开发中我们的 Class 文件不可能一成不变它是随着项目的升级Class 文件也会 升级但是我们不能因为升级了 Class 类就导致之前的序列化对象无法还原了我们需要做到升级前后的兼容性。怎么保证呢显示声明 serialVersionUID。
Java 序列化提供了一个 private static final long serialVersionUID xxxx 的序列化版本号只要版本号相同就可以将原来的序列化对象还原。
类的序列化版本号 serialVersionUID 可以随意指定如果不指定则 JVM 会根据类信息自己生成一个版本号但是这样就会无法保证类升级后的序列化了。同时不指定版本号也不利于 JVM 间的移植因为可能不同的 JVM 版本计算规则可能就不一样了这样也会导致无法反序列化。所以凡是实现 Serializable 接口的类我们都需要显示声明一个 serialVersionUID 版本号。
缺点
说实在话现在几乎不会有人使用 Java 原生的序列化了有如下几个原因使得我们不得不嫌弃他。
无法跨语言
通过 Java 原生 Serializable 接口与 ObjectOutputStream 实现的序列化只能通过 Java 语言自己的ObjectInputStream 来反序列化其他语言如 C、Python、Go 等等都无法对其进行反序列化这不很坑么
同时跨平台支持也不是很好客户端与服务端如果因为 JDK 的版本不同都有可能导致无法进行反序列化这个就更加坑了。
序列化字节流太大
Java 序列化它需要将类的描述信息和属性进行序列化如果不这样做它根本无法还原这就会导致序列化字节流变得很大。我们来做一个比较一个是 Java 原生序列化一个是通用的二进制编码。
public class UserInfo implements Serializable {private static final long serialVersionUID 1L;private Long id;private String userName;private String nickName;public byte[] codeC() {ByteBuffer buffer ByteBuffer.allocate(1024);byte[] userNameBytes this.userName.getBytes();buffer.putInt(userNameBytes.length);buffer.put(userNameBytes);byte[] nickNameBytes this.nickName.getBytes();buffer.putInt(nickNameBytes.length);buffer.put(nickNameBytes);buffer.putLong(this.id);buffer.flip();byte[] result new byte[buffer.remaining()];buffer.get(result);return result;}
}UserInfo 类有一个 codeC() 方法该方法返回 UserInfo 的字节流。
public class Serializable01 {public static void main(String[] args) throws Exception {ObjectOutputStream oos new ObjectOutputStream(new FileOutputStream(person01.txt));Person person01 new Person(张三,35,175.4F);oos.writeObject(person01);oos.close();ObjectInputStream ois new ObjectInputStream(new FileInputStream(person01.txt));Person person011 (Person) ois.readObject();System.out.println(person01.txt 反序列化内容 person011.toString());}
}运行结果
原生 JDK 序列化生成的字节流大小246
UserInfo 对象字节流大小31有 8 倍的差距这差距还是有点儿大的。
序列化时间太长
还是上面那个类我们把上面的程序改下
public class Serializable05 {public static void main(String[] args) throws Exception {UserInfo userInfo new UserInfo(1001L,zhangshan,张三);// 序列化long startTime System.currentTimeMillis();ByteArrayOutputStream bout new ByteArrayOutputStream();ObjectOutputStream out new ObjectOutputStream(bout);out.writeObject(userInfo);out.flush();out.close();System.out.println(原生 JDK 序列化消耗时间 (System.currentTimeMillis() - startTime));bout.close();// 原生字节码startTime System.currentTimeMillis();userInfo.codeC();System.out.println(UserInfo#codeC 消耗时间 (System.currentTimeMillis() - startTime));}
}运行结果
原生 JDK 序列化消耗时间9
UserInfo#codeC 消耗时间1这差距依然很巨大啊
所以Java 原生序列化这么弱也不能不让我们嫌弃他啊
总结
下面对 Java 序列化做一个总结。
序列化的目的是为了将 Java 对象的状态持久化存储起来或者在网络上传输。对象的类名、实例变量包括基本类型数组对其他对象的引用都会被序列化方法、类变量、transient实例变量都不会被序列化。如果要序列化的类中包含有引用类型的成员变量那么该成员变量也需要支持序列化。反序列化时必须要有序列化对象的 Class 文件这里埋坑了。对于 Serializable 接口而言它只是起到一个标识作用。实现了该接口就意味着该类支持序列化。 如果我们不想要某个变量被序列化使用 transient 修饰。 对于 Externalizable 接口 Externalizable 接口是 Serializable 的子类它提供了 writeExternal() 和 readExternal() 方法类实现自定义的序列化和反序列化。writeExternal() 和 readExternal() 两个方法对属性的加工顺序要一致。 建议所有实现了 Serializable 接口的类都显示申明 serialVersionUID 版本号。