对象操作流
序列化
序列化:把对象转化为可传输的字节序列过程称为序列化。
反序列化:把字节序列还原为对象的过程称为反序列化。
-
对象序列化:就是将对象保存到磁盘中,或者在网络中传输对象
-
这种机制就是使用一个字节序列表示一个对象,该字节序列包含:对象的类型、对象的数据和对象中存储的属性等信息
-
字节序列写到文件之后,相当于文件中持久保存了一个对象的信息
-
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化
为什么要序列化?
其实序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。
因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则(序列化),那么我们从IO流里面读出数据的时候再以这种规则把对象还原回来(反序列化)。
如果我们要把一栋房子从一个地方运输到另一个地方去,序列化就是我把房子拆成一个个的砖块放到车子里,然后留下一张房子原来结构的图纸,反序列化就是我们把房子运输到了目的地以后,根据图纸把一块块砖头还原成房子原来面目的过程
什么是对象操作流?
对象操作流分为两类:对象操作输入流 与 对象操作输出流
对象操作输入流(对象序列化流):就是将对象写到磁盘中,或者在网络中传输对象。
对象操作输出流(对象反序列化流):就是把写到磁盘中的对象读到内存中中,或者在网络中接收对象。
对象序列化流: ObjectOutputStream
将Java对象的原始数据类型和图形写入OutputStream。 可以使用ObjectInputStream读取(重构)对象。 可以通过使用流的文件来实现对象的持久存储。 如果流是网络套接字流,则可以在另一个主机上或另一个进程中重构对象
构造方法
方法名 | 说明 |
---|---|
ObjectOutputStream(OutputStream out) | 创建一个写入指定的OutputStream的ObjectOutputStream |
序列化对象的方法
方法名 | 说明 |
---|---|
void writeObject(Object obj) | 将指定的对象写入ObjectOutputStream |
代码实现
我们先准备一个Student类,在TestDemo类中实例化Student对象,并将其写入磁盘中。
package IO流;
import java.io.Serializable;
public class Student implements Serializable {
private String username;
private String password;
public Student() {
}
public Student(String username,String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "Student{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
TestDemo测试类
package IO流;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class TestDemo {
public static void main(String[] args) throws IOException {
Student stu = new Student("zhangsan","123");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
oos.writeObject(stu);
oos.close();
}
}
不难发现,在Student类中,我们实现了Serializable接口,如果不去使用implements关键字实现就会报错。
实现Serializable接口,但是我们并没有重写里面的抽象方法,这是为什么呢???
* @author unascribed
* @see java.io.ObjectOutputStream
* @see java.io.ObjectInputStream
* @see java.io.ObjectOutput
* @see java.io.ObjectInput
* @see java.io.Externalizable
* @since JDK1.1
*/
public interface Serializable {
}
源码中,Serializable接口里面没有都没有!(里面没有抽象方法!)
那么为什么要有这个接口呢???
Serializable的注意点
Serializable接口是一个标记性接口,里面没有任何的抽象方法,只要一个类实现了Serializable接口就表示这个类可以被序列化。反之如果没有使用该接口,而又序列化了这个类就会报上述的错误!
对象反序列化流: ObjectInputStream
ObjectInputStream反序列化先前使用ObjectOutputStream编写的原始数据和对象
构造方法
方法名 | 说明 |
---|---|
ObjectInputStream(InputStream in) | 创建从指定的InputStream读取的ObjectInputStream |
反序列化对象的方法
方法名 | 说明 |
---|---|
Object readObject() | 从ObjectInputStream读取一个对象 |
代码实现
就是上述的Student类,在上面的案例已经将Student类对象实例化后,并存入a.txt文件中,里面将其读出!
package IO流;
import java.io.*;
public class TestDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
Student o = (Student)ois.readObject();
System.out.println(o);
ois.close();
}
}
//Student{username='zhangsan', password='123'}
也可以不用强制类型转换,第2行这样写亦可。
Object o = ois.readObject();
关于对象操作流的注意点
问题一
还是使用上述案例来描述这个问题,用对象序列化流序列化了一个对象后,假如我们修改了对象所属的类文件,读取数据会不会出问题呢?
会出问题,会抛出InvalidClassException异常
Exception in thread "main" java.io.InvalidClassException: IO流.Student; local class incompatible: stream classdesc serialVersionUID = -1644295352893360624, local class serialVersionUID = -5870085774455688069
可以直接搜索异常,JDK给出了出现该异常的几种可能
如果出问题了,如何解决呢?
-
重新序列化
-
给对象所属的类加一个serialVersionUID
-
private static final long serialVersionUID = 1L;
-
例如下描述:
先使用上述的Student类对象序列化,写入a.txt中(有使用serialVersionUID),然后反序列化之前,我们修改Student.java中的代码,将其构造方法改为
public Student(String username,String password) {
this.username = "lisi";
this.password = password;
}
进行反序列化,运行结果为
Student{username='zhangsan', password='123'}
也就是说我们修改的代码没有生效 ,这个很好解释:我们之前将Student序列化写入a.txt中,之后修改Student.java中的代码并没有重新序列化,所以我们反序列化的结果理应为之前序列化输入的值。
而不加serialVersionUID报错,是因为本地序列号与类中的序列号不一致,所以报了异常!
问题二
如果一个对象中的某个成员变量的值不想被序列化,又该如何实现呢?
给该成员变量加transient关键字修饰,该关键字标记的成员变量不参与序列化过程
private transient String password;
可以设置password的为transient,这样子password就不会序列化,运行结果如下:
Student{username='lisi', password='null'}
完整案例
要求:创建多个学生类对象写到文件中,再次读取到内存中
实现步骤:
-
创建序列化流对象
-
创建多个学生对象
-
将学生对象添加到集合中
-
将集合对象序列化到文件中
-
创建反序列化流对象
-
将文件中的对象数据,读取到内存中
Student类
public class Student implements Serializable{
private static final long serialVersionUID = 2L;
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
测试类
package IO流;
import java.io.*;
import java.util.ArrayList;
public class TestDemo {
/**
* read():
* 读取到文件末尾返回值是 -1
* readLine():
* 读取到文件的末尾返回值 null
* readObject():
* 读取到文件的末尾 直接抛出异常
* 如果要序列化的对象有多个,不建议直接将多个对象序列化到文件中,因为反序列化时容易出异常
* 建议: 将要序列化的多个对象存储到集合中,然后将集合序列化到文件中
*/
public static void main(String[] args) throws Exception {
// 序列化
//1.创建序列化流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
ArrayList<Student> arrayList1 = new ArrayList<>();
//2.创建多个学生对象
Student s1 = new Student("李明",30);
Student s2 = new Student("张三",30);
Student s3 = new Student("王五",30);
//3.将学生对象添加到集合中
arrayList1.add(s1);
arrayList1.add(s2);
arrayList1.add(s3);
//4.将集合对象序列化到文件中
oos.writeObject(arrayList1);
oos.close();
// 反序列化
//5.创建反序列化流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
//6.将文件中的对象数据,读取到内存中
Object obj = ois.readObject();
ArrayList<Student> arrayList2 = (ArrayList<Student>)obj;
for (Student stu : arrayList2) {
System.out.println(stu.getName() + "," + stu.getAge());
}
ois.close();
}
}
运行结果:
李明,30
张三,30
王五,30