《Java编程思想第四版》笔记---18章(4) I/O 流式部分--管道流与对象流

管道流

         管道流主要用于连接两个线程的通信。 管道流也分为字节流(PipedInputStream、PipedOutputStream)和字符流(PipedReader、 PipedWriter)。比如一个PipedInputStream必须和一个PipedOutputStream对象进行连接而产生一个通信管  道,PipedOutputStream向管道中写入数据,PipedInputStream从管道中读取数据。管道流的工作如下图所示:


下面看一下管道流的用法。既然管道流的作用是用于线程间的通信,那么势必有发送线程和接收线程,两个线程通过管道流交互数据。首先写一个发送数据的线程:

     
     
  1. public class Sender implements Runnable{
  2. private PipedOutputStream out = new PipedOutputStream();
  3. public PipedOutputStream getOutputStream(){
  4. return out;
  5. }
  6. public void run(){
  7. String str = "Receiver, 你好!";
  8. try{
  9. out.write(str.getBytes()); // 向管道流中写入数据(发送)
  10. out.close();
  11. } catch (IOException e){
  12. e.printStackTrace();
  13. }
  14. }
  15. }

用流写数据的时候注意关注一下,该流是否支持直接写String,不可以的话要用String的getBytes()方法获取字符串的字节。既然有一个发送数据的线程了,接下来来一个接收数据的线程:

     
     
  1. public class Receiver implements Runnable{
  2. private PipedInputStream in = new PipedInputStream();
  3. public PipedInputStream getInputStream(){
  4. return in;
  5. }
  6. public void run(){
  7. String s = null;
  8. byte b0[] = new byte[1024];
  9. try{
  10. int length = in.read(b0);
  11. if (-1 != length){
  12. s = new String(b0, 0 , length);
  13. System.out.println("收到了以下信息:" + s);
  14. }
  15. in.close();
  16. } catch (IOException e){
  17. e.printStackTrace();
  18. }
  19. }
  20. }

两个线程都有了,写一个main线程,利用管道输出流的connect方法连接管道输出流和管道输入流:

     
     
  1. public static void main(String[] args){
  2. try{
  3. Sender sender = new Sender();
  4. Receiver receiver = new Receiver();
  5. Thread senderThread = new Thread(sender);
  6. Thread receiverThread = new Thread(receiver);
  7. PipedOutputStream out = sender.getOutputStream(); // 写入
  8. PipedInputStream in = receiver.getInputStream(); // 读出
  9. out.connect(in);// 将输出发送到输入
  10. senderThread.start();
  11. receiverThread.start();
  12. } catch (IOException e){
  13. e.printStackTrace();
  14. }
  15. }

输出结果应该很明显了,大家都知道,接收线程接收到了来自发送线程通过管道流输出流发送的数据:

收到了以下信息:Receiver, 你好!

注意一下,PipedInputStream运用的是一个1024字节固定大小的循环缓冲区,写入PipedOutputStream的数据实际上保存到了对应的PipedInputStream的内部缓冲区。PipedInputStream执行读操作时,读取的数据实际上来自这个内部缓冲区。如果对应的PipedInputStream输入缓冲区已满,任何企图写入PipedOutputStream的线程都将被阻塞。而且这个写操作线程将一直阻塞,直至出现读取PipedInputStream的操作从缓冲区删除数据。

这意味着,向PipedOutputStream写入数据的线程不应该是负责从对应PipedInputStream读取数据的唯一线程(所以这里开了两个线程分别用于读写)。假定t线程试图一次对PipedOutputStream的write()方法的调用中向对应的PipedOutputStream写入2000字节的数据,在t线程阻塞之前,它最多能够写入1024字节的数据(PipedInputStream内部缓冲区的大小)。然而,一旦t被阻塞,读取PipedInputStream的操作就再也不能出现了,因为t是唯一读取PipedInputStream的线程,这样,t线程已经完全被阻塞。

 

对象流

      ObjectInputStream、ObjectOutputStream这两个类是用于存储和读取对象的输入输出流类。这些对象必须是可以序列化的对象,Java中最简单的做法是实现Serializable接口,序列化是为了保存对象的状态信息。

序列化:将一个对象转换成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。

反序列化:将字节数组重新构造成对象。

1、简单序列化

         序列化只需要实现java.io.Serializable接口就可以了。序列化的时候有一个serialVersionUID参数, Java序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的 。在进行反序列化,Java虚拟机会把传过来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的实体类,可以进行反序列化,否则Java虚拟机会拒绝对这个实体类进行反序列化并抛出异常。

serialVersionUID有两种生成方式:

1、默认的1L

2、根据类名、接口名、成员方法以及属性等来生成一个64位的Hash字段

      如果实现java.io.Serializable接口的实体类没有显式定义一个名为serialVersionUID、类型为long的变量时,Java序列化机制会根据编译的.class文件自动生成一个serialVersionUID,如果.class文件没有变化,那么就算编译再多次,serialVersionUID也不会变化。换言之,Java为用户定义了默认的序列化、反序列化方法,其实就是ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法

看一个例子:

     
     
  1. public class SerializableObject implements Serializable {
  2. private static final long serialVersionUID = 1L;
  3. private String str0;
  4. private transient String str1;
  5. private static String str2 = "abc";
  6. public SerializableObject(String str0, String str1) {
  7. this.str0 = str0;
  8. this.str1 = str1;
  9. }
  10. public String getStr0() {
  11. return str0;
  12. }
  13. public String getStr1() {
  14. return str1;
  15. }
  16. public static void main(String[] args) throws Exception {
  17. File file = new File("D:" + File.separator + "s.txt");
  18. OutputStream os = new FileOutputStream(file);
  19. ObjectOutputStream oos = new ObjectOutputStream(os);
  20. oos.writeObject(new SerializableObject("str0", "str1"));
  21. oos.close();
  22. InputStream is = new FileInputStream(file);
  23. ObjectInputStream ois = new ObjectInputStream(is);
  24. SerializableObject so = (SerializableObject) ois.readObject();
  25. System.out.println("str0 = " + so.getStr0());
  26. System.out.println("str1 = " + so.getStr1());
  27. ois.close();
  28. }
  29. }

运行一下,打开s.txt文件,文件内容就是序列化的对象信息:


第1部分是序列化文件头

◇AC ED:STREAM_MAGIC序列化协议

◇00 05:STREAM_VERSION序列化协议版本

◇73:TC_OBJECT声明这是一个新的对象

第2部分是要序列化的类的描述,在这里是SerializableObject类

◇72:TC_CLASSDESC声明这里开始一个新的class

◇00 1F:十进制的31,表示class名字的长度是31个字节

◇63 6F 6D ... 65 63 74:表示的是“com.xrq.test.SerializableObject”这一串字符,可以数一下确实是31个字节

◇00 00 00 00 00 00 00 01:SerialVersion,序列化ID,1

◇02:标记号,声明该对象支持序列化

◇00 01:该类所包含的域的个数为1个

第3部分是对象中各个属性项的描述

◇4C:字符"L",表示该属性是一个对象类型而不是一个基本类型

◇00 04:十进制的4,表示属性名的长度

◇73 74 72 30:字符串“str0”,属性名

◇74:TC_STRING,代表一个new String,用String来引用对象

第4部分是该对象父类的信息,如果没有父类就没有这部分。有父类和第2部分差不多

◇00 12:十进制的18,表示父类的长度

◇4C 6A 61 ... 6E 67 3B:“L/java/lang/String;”表示的是父类属性

◇78:TC_ENDBLOCKDATA,对象块结束的标志

◇70:TC_NULL,说明没有其他超类的标志

第5部分输出对象的属性项的实际值,如果属性项是一个对象,这里还将序列化这个对象,规则和第2部分一样

◇00 04:十进制的4,属性的长度

◇73 74 72 30:字符串“str0”,str0的属性值

从以上对于序列化后的二进制文件的解析,我们可以得出以下几个关键的结论:

1、序列化之后保存的是类的信息

2、被声明为transient的属性不会被序列化,这就是transient关键字的作用

3、被声明为static的属性不会被序列化,这个问题可以这么理解,序列化保存的是对象的状态,但是static修饰的变量是属于类的而不是属于变量的,因此序列化的时候不会序列化它


输出结果:
     
     
       
       
  1. str0 = str0
  2. str1 = null

       因为str1是一个transient类型的变量,没有被序列化,因此反序列化出来也是没有任何内容的,显示的null,符合我们的结论。

       当对象实现Serializable接口进行自动序列化时,类中某些字段不想被序列化,需要使用transient关键字,虽然Externalizable通过writeExternal()方法也可以实现此功能,但是序列化不是自动进行的,使用Serializable和transient关键字更加方便。

注意:由于Externalizable默认序列化不存储任何字段,所以transient关键字只在Serializable中使用。

2.序列化控制

      默认的Serializable序列化是将对象整体序列化,但是对于一些特殊的需求例如:序列化部分对象或者反序列化部分对象的情况,可以使用Externalizable接口来代替Serializable接口,重写Externalizable的writeExternal()和readExternal()方法可以实现对序列化的控制,这两个方法在对象序列化和反序列化时自动调用,例子如下:

    
    
  1. class Blip1 implements Externalizable {
  2. private int i;
  3. private String s;
  4. //不加public无参构造函数,在反序列化的时候会报错:
  5. //Exception in thread "main" java.io.InvalidClassException: Blip1; no valid constructor
  6. public Blip1(){
  7. System.out.println("Blip1 Empty Constructor");
  8. }
  9. public Blip1(String x, int a){
  10. System.out.println("Blip1 Constructor");
  11. i = a;
  12. s = x;
  13. }
  14. public String toString(){
  15. return s + i;
  16. }
  17. @Override
  18. public void writeExternal(ObjectOutput out) throws IOException {
  19. System.out.println("Blip1.writeExternal");
  20. out.writeObject(s);
  21. out.writeInt(i);
  22. }
  23. @Override
  24. public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  25. System.out.println("Blip1.readExternal");
  26. s = (String)in.readObject();
  27. i = in.readInt();
  28. }
  29. }
  30. class Blip2 implements Externalizable {
  31. Blip2(){
  32. System.out.println("Blip2 Constructor");
  33. }
  34. @Override
  35. public void writeExternal(ObjectOutput out)throws IOException{
  36. System.out.println("Blip2.writeExternal");
  37. }
  38. @Override
  39. public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  40. System.out.println("Blip2.readExternal");
  41. }
  42. }
  43. public class Blips {
  44. public static void main(String[] args)throws IOException, ClassNotFoundException{
  45. System.out.println("Constructing objects:");
  46. Blip1 b1 = new Blip1("Blip1",100);
  47. Blip2 b2 = new Blip2();
  48. //序列化
  49. ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Blips.out"));
  50. System.out.println("Saving objects:");
  51. o.writeObject(b1);
  52. o.writeObject(b2);
  53. //反序列化
  54. ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blips.out"));
  55. System.out.println("Recovering b1:");
  56. b1 = (Blip1)in.readObject();
  57. System.out.println(b1);
  58. //由于Blip2的默认无参数构造方法不是public的,所以会抛异常
  59. //System.out.println("Recovering b2:");
  60. //b2 = (Blip2)in.readObject();
  61. }
  62. }

输出结果:

    
    
  1. Constructing objects:
  2. Blip1 Constructor
  3. Blip2 Constructor
  4. Saving objects:
  5. Blip1.writeExternal
  6. Blip2.writeExternal
  7. Recovering b1:
  8. Blip1 Empty Constructor
  9. Blip1.readExternal
  10. Blip1100

注意:

(1)使用Externalizable反序列化时,只会调用默认的public无参构造方法,对象必须要有public类型的无参数构造方法,所以Blip2非public类型无参数构造方法无法反序列化。


总结:

1、当父类继承Serializable接口时,所有子类都可以被序列化

2、子类实现了Serializable接口,父类没有,父类中的属性不能序列化(不报错,数据丢失),但是在子类中属性仍能正确序列化

3、如果序列化的属性是对象,则这个对象也必须实现Serializable接口,否则会报错

4、反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失,但不会报错

5、反序列化时,如果serialVersionUID被修改,则反序列化时会失败

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

繁星点点-

请我喝杯咖啡呗

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值