什么是Java的对象流?
Java对象流是用于存储和读取基本数据类型数据或对象数据的输入输出流。
Java的对象流可分为两种:
- 1,对象输入流类ObjectInputStream 用于从数据源读取对象数据,它是可以读取基本数据类型数据或对象数据的输入流。
- 2,对象输出流类ObjectOutputStream 用于把对象写入到数据源,它是可以输出基本数据类型数据或对象数据的输出流。
Java中提供了ObjectInputStream、ObjectOutputStream这两个类用于对象序列化操作,这两个类是用于存储和读取对象的输入输出流类,只要把对象序列化转换成平台无关的二进制数据存储起来,就等于保存了这个对象;之后根据需要再把保存的对象的二进制数据读取进来就可以创建并使用此对象。
ObjectInputStream、ObjectOutputStream是可以帮助开发者完成保存和读取对象的二进制数据的输入输出流。但前提是对象必须实现Serializable接口。
什么是对象的序列化和反序列化?
序列化与反序列化
- 序列化:序列化是将内存中的对象转换成与平台无关的二进制流(二进制字节数组)的过程。序列化的二进制流可通过网络传输到别的网络节点;或者保存为二进制的磁盘文件,从而达到持久化的目的。
- 反序列化:反序列化则是把从网络接收或者从磁盘文件读取的二进制流,重新构建为对象的过程。
序列化的作用 序列化是将可序列化的Java对象转换为字节序列,这此序列可存储在磁盘中,或者通过网络传输,以便在需要的时候可重新构建为Java对象。序列化机制使得Java对象可以脱离程序而独立存在,从而实现持久化。
序列化版本UID
序列化版本UID,是指可序列化的类的serialVersionUID参数。
Java序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,Java虚拟机会把传过来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的实体类,可以进行反序列化;否则Java虚拟机会拒绝对这个实体类进行反序列化并抛出异常。
serialVersionUID的三种生成方式:
- 1、默认的serialVersionUID是1L(长整型数 1)。
- 2、用户指定一个长整型数作为serialVersionUID。
- 3、根据类名、接口名、成员方法以及属性等通过散列(Hash)生成一个64位的长整型数作为serialVersionUID。
如果实现java.io.Serializable接口的实体类没有显式定义一个名为serialVersionUID、类型为long的属性时,Java序列化机制会根据编译的.class文件自动散列(Hash)生成一个serialVersionUID。如果.class文件没有改变,那么多次编译,其serialVersionUID也不变。
对象中有三种属性不可序列化:
1、static 修饰的属性。因为,序列化存储的是对象的属性状态,而static属性是属于类的,不属于对象,所以序列化时不会被序列化。
2、transient 修饰的属性,在序列化时不会被序列化。例如,有些敏感信息,如密码等可用transient修饰来避开序列化。
3、未实现序列化(serializable)接口的属性,无法被序列化。
一、默认序列化
一个类实现了java.io.Serializable接口,才能支持对象序列化。
默认序列化:Java语言为用户定义了默认的序列化、反序列化方法,其实就是ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法。
我们先来看一个默认序列化的演示程序:
类PersonSerial
默认序列化测试演示使用的类PersonSerial,实现了Serializable接口,但没有重写writeObject()和readObject()方法。
package IOStream.ObjectSerialization;
import java.io.Serializable;
public class PersonSerial implements Serializable {
private String name;
private int age;
private String address;
public PersonSerial(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public String toString() {
return "PersonSerial {" +
"姓名='" + name + '\'' +
", 年龄=" + age +
", 地址='" + address + '\'' +
'}';
}
}
默认序列化的测试演示程序:
package IOStream.ObjectSerialization;
import java.io.*;
public class ObjectSerialization {
// 序列化对象方法
public static void serializable(File file) throws Exception
{
OutputStream outputFile = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(outputFile);
oos.writeObject(new PersonSerial("张三丰", 108,"五台山"));
oos.writeObject("海内存知己,天涯若比邻。");
oos.writeDouble(3.1416D);
oos.close();
}
// 反序列化对象方法
public static void deserializable(File file) throws Exception
{
InputStream inputFile = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(inputFile);
PersonSerial p = (PersonSerial)ois.readObject();
System.out.println(p);
String txt = (String)ois.readObject();
double pi = (double)ois.readDouble();
System.out.println(txt);
System.out.println("PI = "+pi);
ois.close();
}
public static void main(String[] args) throws Exception
{
File file = new File("D:/temp/对象序列化.txt");
serializable(file);
deserializable(file);
}
}
默认序列化的测试演示结果图:
说明:
1,序列化的效果:文本文件“对象序列化.txt”中的内容是程序序列化后写入的与平台无关的二进制流(二进制字节数据)。这 个序列化产生的文件,其文件头,包括序列化协议、序列化协议版本等信息,后面才是真正对象序列化后的二进制字节数据。
2,反序列化的效果:文本文件上面控制台上显示的三行信息,是程序从文本文件“对象序列化.txt”中读入的信息,再创建的对象、字符串和双精度的浮点数。
测试结果说明:序列化和反序列化成功了,类PersonSerial只实现了Serializable接口,并没有重写writeObject()和readObject()方法,但类PersonSerial具备了序列化的能力。因此,序列化和反序列化过程中,默认调用的是ObjectOutputStream的defaultWriteObject()以及ObjectInputStream的defaultReadObject()方法。
注意事项: 无论是默认序列化,还是定制序列化,writeObject()方法存储属性变量的顺序都应该和readObject()方法恢复属性变量的顺序一致,否则将不能正确恢复该Java对象。
二、定制序列化
定制序列化的实现:当一个类实现java.io.Serializable接口时,就可以支持对象序列化。如果用户有特殊需求,可以通过在类的自定义writeObject()和readObject()方法来达到定制的目的。两个方法的方法签名如下所示:
private void writeObject(java.io.ObjectOutputStream s) throws Exception {
//在这里编写定制的代码
}
private void readObject(java.io.ObjectInputStream s) throws Exception {
//在这里编写定制的代码
}
进行序列化、反序列化时,虚拟机会首先试图调用对象里的writeObject()和readObject()方法,进行用户自定义的序列化和反序列化。如果没有这样的方法,那么默认调用的是ObjectOutputStream的defaultWriteObject()以及ObjectInputStream的defaultReadObject()方法。换言之,利用自定义的writeObject()方法和readObject()方法,用户可以自己控制序列化和反序列化的过程。
这是非常有用的。比如:
1、有些场景下,某些字段我们并不想要使用Java提供给我们的序列化方式,而是想要以自定义的方式去序列化它,比如ArrayList的elementData、HashMap的table(至于为什么在之后写这两个类的时候会解释原因),就可以通过将这些字段声明为transient,然后在writeObject()和readObject()中去使用自己想要的方式去序列化它们
2、因为序列化并不安全,因此有些场景下我们需要对一些敏感属性进行加密再序列化,然后再反序列化的时候按照同样的方式进行解密,这样会有更高的安全性。要这么做,就必须自定义编写writeObject()和readObject(),writeObject()方法在序列化前对字段加密,readObject()方法在序列化之后对字段解密。
三、另一种定制序列化机制,使用Externalizable接口
对象的序列化和反序列化非常复杂,Java语言还有另一种自定义序列化机制,这种方式完全由程序员决定如何存储和恢复Java对象数据。这种机制要求Java类必须实现Externalizable接口
Externalizable接口提供了更高级的自定义序列化能力,我们可以控制序列化的过程,选择序列化哪些属性,以及如何序列化。
Externalizable接口
Externalizable接口是Java提供的自定义序列化接口,它继承自Serializable接口,并添加了两个方法:writeExternal(ObjectOutput out)和readExternal(ObjectInput in)。这两个方法分别用于序列化和反序列化对象。
public interface Externalizable extends Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
在某些场景,例如,在处理大型对象或复杂对象图形时,使用Externalizable接口进行序列化定制可能会获得更好的性能。
我们来看一个简单的Externalizable序列化与反序列化例子:
首先,我们来定义一个PersonExter类,该类是实现了Externalizable接口的Person,并重写实现writeExternal()方法和readExternal()方法。
package IOStream.ObjectSerialization;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class PersonExter implements Externalizable {
private String name;
private int age;
private String address;
public PersonExter() { //无参数的构造器是必须的。
}
public PersonExter(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 序列化name、age和address属性
out.writeObject(name);
out.writeInt(age);
out.writeObject(address);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 反序列化name、age和address属性
name = (String) in.readObject();
age = in.readInt();
address = (String) in.readObject();
}
@Override
public String toString() {
return "PersonExter {" +
"姓名='" + name + '\'' +
", 年龄=" + age +
", 地址='" + address + '\'' +
'}';
}
}
Externalizable接口实现序列化的注意事项:
- 必须有默认构造器:这个类定义中,无参数构造器是必须的,不定义,会出现“无有效构造器”例外。
public PersonExter() { //无参数的构造器是必须的。
}
- 必须重写实现writeExternal()方法和readExternal()方法。
@Override
public void readExternal(ObjectInput arg0) throws IOException, ClassNotFoundException {
}
@Override
public void writeExternal(ObjectOutput arg0) throws IOException {
}
一个简单的使用Externalizable接口序列化与反序列化演示例程
下面是Externalizable接口序列化与反序列化的简单演示例程:
package IOStream.ObjectSerialization;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ExternalizableDemo {
public static void main(String[] args) {
String path = "D:/temp/person.txt";
PersonExter person = new PersonExter("左中堂", 80, "湖南");
// 序列化
try (FileOutputStream foS = new FileOutputStream(path);
ObjectOutputStream out = new ObjectOutputStream(foS)) {
out.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (FileInputStream fis = new FileInputStream(path);
ObjectInputStream in = new ObjectInputStream(fis)) {
PersonExter restoredPerson = (PersonExter) in.readObject();
System.out.println(restoredPerson);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
例程测试结果图:
对象序列化与反序列化有一些经典的应用场景:
1,在联机游戏中各参与方中的场景处理,;
2,网络电子画板中各个画板内容的同步显示;
3,线上教育中电子黑板上内容的同步显示。
参考文献:
- JAVA中的对象流ObjectInputStream
- JAVA中的ObjectOutputStream类
- java 序列化 之 externalizable
- Java IO流详解(七)----对象流(序列化与反序列化)