黑马程序员——————> 序列化流

如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的。为了让某个类是可序列化的,该类必须实现如下两个接口之一。

Serializable

Externalizable

java的很多类已经实现了Serializable,该接口是一个标记接口,实现该接口无须实现任何方法,它只是表明该类的实例是可序列化的。

所有可能在网络上传输的对象的类都应该是可序列化的否则程序将会出现异常,比如RMI过程中的参数和返回值;所有需要保存到磁盘里的对象的类都必须可序列化。

因为序列化是RMI过程的参数和返回值都必须实现的机制,而RMI又是javaee技术的基础,所有的分布式应用常常需要跨平台,跨网络,所以要求所有传递的参数,返回值必须实现序列化。因此序列化机制是javaEE平台的基础。

使用Serializable来实现序列化非常简单,主要让目标类实现Serializable标记接口即可,无须实现任何方法。

一旦某个类实现了Serializable接口,该类的对象就是可序列化的,程序可以通过如下两个步骤来序列化该对象。

1:创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其他节点流的基础之上。如下代码所示:

ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("a.tat"));

2:调用ObjectOutputStream对象的writeObject()方法输出可序列化对象,如下代码所示:

//将一个Person对象输出到输出流中
obj.writeObject(p);

下面程序定义了一个Person类,这个Person类就是一个普通的java类,只是实现了Serializable接口,该接口标识该类的对象是可序列化的。

 1 import java.io.Serializable;
 2
 3 public class Person implements Serializable
 4 {
 5     private String name;
 6     private int age;
 7     //此处没有提供无参数的构造器
 8     public Person(String name , int age)
 9     {
10         System.out.println("有参数的构造器");
11         this.name = name;
12         this.age = age;
13     }
14     public String getName()
15     {
16         return name;
17     }
18     public void setName(String name)
19     {
20         this.name = name;
21     }
22     public int getAge()
23     {
24         return age;
25     }
26     public void setAge(int age)
27     {
28         this.age = age;
29     }
30
31
32 }

下面程序使用ObjectOutputStream将一个Person对象写入磁盘文件。

 1 public class WriteIbject
 2 {
 3     public static void main(String[] args)
 4     {
 5         try(
 6                 //创建一个ObjectOutputStream输出流
 7                 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("b.txt")))
 8         {
 9             person p = new person("孙悟空", 500);
10             //将per对象写入输出流
11             oos.writeObject(p);
12         }
13         catch(IOException ex)
14         {
15             ex.printStackTrace();
16         }
17     }
18 }

上面程序中的第一行粗体字代码创建了一个ObjectOutputStream输出流,这个OvjectOutputStream输出流建立在一个文件输出流的基础之上;程序使用write()方法将一个Person对象写入输出流。

如果希望从二进制流中恢复java对象,则需要使用反序列化,反序列化的步骤如下。

1:创建一个ObjectInputStream输入流,这个输入流是一个处理流,所以必须建立在其他节点流的基础之上。如下代码所示。

//创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(b.txt));

2:调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回一个Object类型的java对象,如果程序知道java对象的类型,则可以将该对象强制类型转换成其真实的类型,如下代码所示:

//从输入流中读取一个java对象,并将其强制类型转换为Person类
Person p = (Person)ois.readObject();

下面程序示范了从刚刚生成的b.txt文件中读取Person对象的步骤:

 1 public class ReadObject
 2 {
 3
 4     public static void main(String[] args)
 5     {
 6         try(
 7                 //创建一个ObjectInputStream输入流
 8                 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("b.txt")))
 9         {
10             //从输入流中读取一个java对象,并将其强制类型转换为Person类
11             Person p = (Person)ois.readObject();
12             System.out.println("名字为:"+ p.getName() + "\n年龄为:" + p.getAge());
13         }
14         catch(Exception ex)
15         {
16             ex.printStackTrace();
17         }
18
19     }
20
21 }

反序列化读取的仅仅是java对象的数据,而不是java类,因此采用反序列化恢复java对象时,必须提供该java对象所属类的calss文件,否则将会引发异常。

Person类只有一个有参数的构造器,没有无参数的构造器,而且该构造器内有一个普通的打印语句。当反序列化读取java对象时,并没有看到程序调用该构造器,这表明反序列化机制无须通过构造器来初始化java对象。

 

如果受用序列化机制向文件中写入了多个java对象,使用反序列化机制恢复对象时必须按照写入的顺序读取。

当一个可序列化类有多个父类时,这些父类要么有无参数构造器,要么也是可序列化的,否则反序列化时将抛出异常。如果父类是不可序列化的,之上带有无参数的构造器,则该父类中定义的成员变量值不会序列化到二进制流中。

 

 

对象引用的序列化

前面的person类的两个成员变量分别是String类型和int类型,如果某个类的成员变量的类型不是基本类型或者String类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的。

如下Teacher类持有一个Person类的引用,只有Person类是可序列化的,Teacher类才是可序列化的。如果Person类不可被序列化,则无论Teacher类是否实现Serilizable Externalizable接口,Teacher类都是不可序列化的。

 1 public class Teacher implements Serializable
 2 {
 3     private String name;
 4     private Person student;
 5
 6     public Teacher(String name, Person student)
 7     {
 8         this.name = name;
 9         this.student = student;
10     }
11
12     public String getName() {
13         return name;
14     }
15
16     public void setName(String name) {
17         this.name = name;
18     }
19
20     public Person getStudent() {
21         return student;
22     }
23
24     public void setStudent(Person student) {
25         this.student = student;
26     }
27
28
29 }

现在假设有如下一种特殊情形:程序中有两个Teacher对象,他们的student实例变量都引用同一个Person对象,而日该Person对象还有一个引用变量引用它。如下代码所示:

Person per = new Person("孙悟空",500);
Teacher t1 = new Teacher("唐僧",per);
Teacher t2 = new Teacher("菩提老祖",per);

上面代码创建了两个Teacher对象和一个Person对象。

这里产生了一个问题,如果先序列化t1对象,则系统将该t1对象所引用的Person对象一起序列化;如果程序再显式序列化per对象,系统将一样会序列化该t2对象,并且将再次序列化该t2对象所引用的Person对象;如果程序再显式序列化per对象,系统将再次序列化该Person对象。这个过程似乎会想输出流中输出三个Person对象。

如果系统想输出流中写入了三个Person对象,从而引起t1和t2所引用的Person对象不是同一个对象。

 

所以,java序列化机制采用了一种特殊的序列化算法,其算法内容如下:

1:所有保存到此哦鞍中的对象都有一个序列化编号

2:当程序视图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未被序列化过,系统才会将该对象转换成字节序列并输出。

3:如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重写序列化该对象。

 

 

下面程序序列化了两个Teacher对象,两个Teacher对象都持有一个引用到同一个Person对象的引用,而且程序两次调用writeObject()方法输出同一个Teacher对象。

 1 public class WriteTeacher
 2 {
 3     public static void main(String[] args)
 4     {
 5         try(
 6                 //创建一个ObjectOutputStream输出流
 7                 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt")))
 8         {
 9             Person p = new Person("孙悟空",500);
10             Teacher t1 = new Teacher("唐僧",p);
11             Teacher t2 = new Teacher("菩提老祖",p);
12
13             //依次将4个对象写入输出流
14             oos.writeObject(t1);
15             oos.writeObject(t2);
16             oos.writeObject(p);
17             oos.writeObject(t2);
18         }
19         catch(IOException ex)
20         {
21             ex.printStackTrace();
22         }
23     }
24
25 }

上面程序4次调用了writeObject()方法来输出对象,实际上只序列化了三个对象,而且序列的两个Teacher对象的stydent引用实际是同一个Person对象。下面程序读取虚礼欸文件中的对象即可证明这一点。

 1 public class ReadTeacher
 2 {
 3     public static void main(String[] args)
 4     {
 5         try(
 6                 //创建一个ObjectInputStream输入流
 7                 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt")))
 8         {
 9             //依次读取ObjectInputStream输入流中的4个对象
10             Teacher t1 = (Teacher)ois.readObject();
11             Teacher t2 = (Teacher)ois.readObject();
12             Person p = (Person)ois.readObject();
13             Teacher t3 = (Teacher)ois.readObject();
14
15             //输出true
16             System.out.println("t1的student引用和p是否相同:"+ (t1.getStudent() == p));
17
18             //输出true
19             System.out.println("t2的student引用和p是否相同:"+ (t2.getStudent() == p));
20
21             //输出true
22             System.out.println("t2和t3是否是同一个对象:"+ (t2 == t3));
23         }
24         catch(Exception ex)
25         {
26             ex.printStackTrace();
27         }
28
29     }
30
31 }

上面程序依次读取了序列化文件中的4个java对象,但通过比较判断,不难发现t2和t3是同一个java对象,t1的student引用的,t2的student引用的和p引用变量引用的也是同一个java对象。

 

由于java序列化机制使然:如果多层虚礼欸同一个java对象时,只有第一次序列化时才会把该java对象转换成字节序列并输出,这样可能引起一个潜在的问题,但程序序列化一个可变对象时,只有第一次使用writeObject()方法输出时才会将该对象转换成字节序列并输出,当程序再次调用writeObject()方法时,程序之上输出前面的序列化编号,即使后面该对象的实例变量值已被改变,改变的实例变量值也不会被输出。如下程序所示:

 1 public class SerializaMutable
 2 {
 3     public static void main(String[] args)
 4     {
 5         try(
 6             //创建一个ObjectOutputStream输出流
 7             ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("mutable.txt"));
 8             //创建一个ObjectInputStream输入流
 9             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("mutable.txt")))
10         {
11             Person per = new Person("孙悟空",500);
12             //系统将per对象转换成字节序列并输出
13             oos.writeObject(per);
14             //改变per对象的name实例变量的值
15             per.setName("猪八戒");
16             //系统只是输出序列化编号,所以改变后的name不会被序列化
17             oos.writeObject(per);
18
19             Person p1 = (Person) ois.readObject();
20             Person p2 = (Person) ois.readObject();
21
22             //下面输出true,即反序列化后p1等于p2
23             System.out.println(p1 == p2);
24             //下面依然看到输出"孙悟空",即改变后的实例变量没有被序列化
25             System.out.println(p2.getName());
26         }
27         catch(Exception ex)
28         {
29             ex.printStackTrace();
30         }
31
32     }
33
34 }

程序中先使用writeObject()方法写入了一个Person对象,接着程序改变了Person对象的name实例变量值,然后程序再次输出Person对象,但这次的输出已经不会将Person对象转换成字节序列并输出了,而是仅仅输出了一个序列化编号。

程序中代码两次调用readObject()方法读取了序列化文件的java对象,比较两次读取的java对象将完全相同,程序输出第二次读取Person对象的name实例变量的值依然是“孙悟空”,表明改变后的Person对象并没有被写入----这与java序列化机制相符。

当使用java序列化机制序列化可变对象时一定要注意,只有第一次调用wirteObject()方法来输出对象时才会将对象转换成字节序列,并写入到ObjectOutputStream;在后面程序中即使该对象的实例变量发生了改变,再次调用writeObject()方法输出该对象时,改变后的实例变量也不会被输出。

时间: 2024-10-13 04:08:36

黑马程序员——————> 序列化流的相关文章

黑马程序员_IO流2

File类 1.File类 不属于流对象.作用:将机器上的路径和目录封装成一个File对象,提供了很多的方法和成员变量,让我们操作目录和路径   目录就是文件夹.路径,指由文件夹组成的到达文件夹的通道. 2.File类的静态成员变量   一共提供的4个变量,掌握的只有2个,跨平台 static String separator  结果\ Windows目录分隔符  Linux下 / static String pathSeparator 结果 ; Windows下的路径与路径之间的分隔符 Lin

黑马程序员_IO流1

异常1.异常的继承体系: java.lang.Throwable类,所有异常和错误的父类 Error类所有错误的父类 Exception类所有异常的父类 RuntimeException NullPointerException ClassCastException IndexOutOfBoundsException 错误,比较严重的问题,一旦出现了错误,程序根本不能运行,必须修改源代码. 异常,比较轻微的问题,一旦出现了异常,程序可以处理掉异常,继续运行.异常一般是程序的最后一个问题. 2.T

黑马程序员——打印流小结

//打印流 /* 1.字节打印流,他可以获取的参数有: 1.File对象 2.字符串 3.字节输出流,可以实现自动刷新 2.字符打印流,他可以获取的参数有: 1.File对象 2.字符串 3.字节输出流,可以实现自动刷新 4.字符输出流,可以实现自动刷新 */ import java.io.*; import java.io.PrintWriter; import java.io.BufferedReader; public class Print{ public static void mai

黑马程序员 IO流 文件的分割与合并

---------------------- ASP.Net+Unity开发..Net培训.期待与您交流! ----------------------package cn.itcast.IO; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException;

黑马程序员——IO流总结

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 什么是IO流? IO流是用来处理设备之间的数据传输 有哪些流? 流按操作数据的种类分为:字符流和字节流 流按流的方向分:输入流和输出流 字节流的基类:InputStream和OutputStream 字符流的基类:Writer和Reader 常用写字符流:FileWriter,BufferedWriter,OutputStreamWriter,PrinterWriter 常用读字符流  : F

黑马程序员-IO流其他流与File类

------<a href="http://www.itheima.com" target="blank">Java培训.Android培训.iOS培训..Net培训</a>.期待与您交流! ------- 1:其他的流 1: DataOutputStream ;多了一些操作基本数据类型的功能 DataInputStream:读取文件. 用DataOutputStream流写进去的数据,就用DataInputStream流来读取. 1 imp

黑马程序员——IO流

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- IO流 IO流概述 IO流用来处理设备之间的数据传输.Java对数据的操作是通过流的方式.Java用于操作流的对象都在IO包中. 流按照流向分为两种:输入流和输出流. 输入流和输出流相对于内存设备而言.将外设中的数据读取到内存中:输入.将内存的数写入到外设中:输出. 流按照操作数据分为两种:字节流和字符流. 字节流读取文字字节数据后,不直接操作而是先查指定的编码表,获取对应的文字.再对这个文字

黑马程序员——JAVAIO流学习总结

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- www.itheima.com JAVA IO流类图结构: 显而易见了,IO流就是这个.下面我们来做些解释性的分析: 流的概念和作用 流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象.即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作. IO流的分类 根据处理数据类型的不同分为:字符流和字节流 根据数据流向不同分为:输

黑马程序员_javaIO流概述

------- android培训.java培训.期待与您交流! ---------- 一.IO流概述 (1) IO流用来处理设备之间的数据传输Java对数据的操作是通过流的方式Java用于操作流的对象都在IO包中流按操作数据分为两种:字节流与字符流流按流向分为:输入流,输出流. (2) IO流常用基类字节流的抽象基类InputStream,OutputStream字符流的抽象基类Reader,Writer这四个类派生出的子类名称都是以其父类名作为子类名的后缀. 二.字符流 1.FileWrit