一、IO 流概述
流(Stream)的概念源于 UNIX 中管道(pipe)的概念。在 UNIX中,管道是一条不间断的字节流,用来实现程序或进程间的通信,或读写外围设备、外部文件等。一个流,必有源端和目的端,它们可以是计算机内存的某些区域,也可以是磁盘文件,甚至可以是Internet上的某个URL。实际上,流的源端和目的端可简单地看成是字节的生产者和消费者,对输入流,可不必关心它的源端是什么,只要简单地从流中读数据,而对输出流,也可不知道它的目的端,只是简单地往流中写数据。
定义
流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
分类
根据数据流向不同分为:输入流和输出流。
注意输入流和输出流是相对于程序而言的。把程序(内存)中的内容输出到磁盘、光盘等存储设备中是为输出;读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。根据处理数据类型分为:字符流和字节流。
数据流中最小的数据单元是字节的流为字节流;数据流中最小的数据单元是字符的流为字符流。Java 中的字符是 Unicode 编码,一个字符占用两个字节(无论中文还是英文都是两个字节)。根据功能不同分为:节点流和包装流。
1)节点流:可以从或向一个特定的地方(节点)读写数据,直接连接数据源。如最常见的是文件的 FileReader,还可以是数组、管道、字符串,关键字分别为 ByteArray/CharArray,Piped,String。
2)处理流(包装流):并不直接连接数据源,是对一个已存在的流的连接和封装,是一种典型的装饰器设计模式,使用处理流主要是为了更方便的执行输入输出工作,如PrintStream,输出功能很强大,又如BufferedReader提供缓存机制,推荐输出时都使用处理流包装。
注意:一个 IO 流可以即是输入流又是字节流又或是以其他方式分类的流类型,是不冲突的。比如 FileInputStream,它既是输入流又是字节流还是文件节点流。
- 一些特别的的流类型
1)转换流:转换流只有字节流转换为字符流,因为字符流使用起来更方便,我们只会向更方便使用的方向转化。如:InputStreamReader 与 OutputStreamWriter。
2)缓冲流:有关键字 Buffered,也是一种处理流,为其包装的流增加了缓存功能,提高了输入输出的效率,增加缓冲功能后需要使用 flush() 才能将缓冲区中内容写入到实际的物理节点。但是,在现在版本的 Java 中,只需记得关闭输出流(调用 close() 方法),就会自动执行输出流的 flush() 方法,可以保证将缓冲区中内容写入。
3)对象流:有关键字Object,主要用于将目标对象保存到磁盘中或允许在网络中直接传输对象时使用(对象序列化)。
操作 IO 流的模板
第一步:创建源或目标对象
输入:把文件中的数据流向到程序中,此时文件是源,程序是目标;
输出:把程序中的数据流向到文件中,此时文件是目标,程序是源。
第二步:创建 IO 流对象
输入:创建输入流对象;
输出:创建输出流对象;
第三步:具体的 IO 操作
第四步:关闭资源
输入:输入流的 close() 方法;
输出:输出流的 close() 方法。
复制文件示例代码:
注意:程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源。如果不关闭该资源,那么磁盘的文件将一直被程序引用着,不能删除也不能更改,所以应该手动调用 close() 方法关闭流资源。
二、整体架构
JAVA 流操作相关的类和接口
类 | 说明 |
---|---|
File | 文件类 |
RandomAccessFile | 随机存取文件类 |
InputStream | 字节输入流 |
OutStream | 字节输出流 |
Reader | 字符输入流 |
Writer | 字符输出流 |
Java IO 流的整体架构
从图中可以看出四大基类流,如下表所示。这四大基流都是抽象类,其他流都是继承于这四大基流的。
字节流 | 字符流 | |
---|---|---|
输入流 | InputStream | Reader |
输出流 | OutStream | Writer |
用法分析
- 按数据来源(去向)使用
是文件: FileInputStream, FileOutputStream, FileReader, FileWriter
是 byte[]:ByteArrayInputStream, ByteArrayOutputStream
是 char[]:CharArrayReader, CharArrayWriter
是 String:StringBufferInputStream, StringReader, StringWriter
网络数据流:InputStream, OutputStream, Reader, Writer - 按是否格式化输出使用
要格式化输出:PrintStream, PrintWriter - 按是否要缓冲使用
要缓冲:BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter。 - 按数据格式使用
二进制格式(只要不能确定是纯文本的): InputStream, OutputStream 及其所有带 Stream 结尾的子类;
纯文本格式(含纯英文与汉字或其他编码方式);Reader, Writer 及其所有带 eader, Writer 的子类 。 - 按输入输出使用
输入:Reader, InputStream 类型的子类;输出:Writer, OutputStream 类型的子类。 - 特殊需要
从 Stream 到 Reader、Writer 的转换类:InputStreamReader, OutputStreamWriter;
对象输入输出:ObjectInputStream, ObjectOutputStream;
进程间通信:PipeInputStream, PipeOutputStream, PipeReader, PipeWriter ;
合并输入:SequenceInputStream;
更特殊的需要:PushbackInputStream, PushbackReader, LineNumberInputStream, LineNumberReader。
虽然四大基类流的实现类很多,功能繁杂,但是他们的命名非常有规律,可以“顾名思义”。如字节流的输入与输出的对应:
图中蓝色的为主要的对应部分,红色的部分就是不对应部分。紫色的虚线部分代表这些流一般要搭配使用。
三、IO 流文件操作
读取文件内容:
向文件中写入字符串:
向文件中追加新内容:
使用 RandomAccessFile 写入文件:
四、字节流
InputStream
InputStream 是所有字节输入流的父类,是一个抽象类。
子类:
ByteArrayInputStream、StringBufferInputStream、FileInputStream 是三种基本的介质流,它们分别从 Byte 数组、StringBuffer 和本地文件中读取数据。PipedInputStream 是从与其它线程共用的管道中读取数据。ObjectInputStream 和所有 FilterInputStream 的子类都是装饰流(装饰器模式的主角)。
方法:
示例:
OutputStream
OutputStream 是所有的输出字节流的父类,它是一个抽象类。
子类:
(1)OutputStream是以字节为单位的输出流的超类,提供了write()函数从输出流中读取字节数据。
(2)ByteArrayOutputStream是字节数组输出流,写入ByteArrayOutputStream的数据被写入到一个byte数组,缓冲区会随着数据的不断写入而自动增长,可使用toByteArray()和toString()获取数据。
(3)PipedOutputStream是管道输出流,和PipedInputStream一起使用,能实现多线程间的管道通信。
(4)FilterOutputStream是过滤输出流,是DataOutputStream,BufferedOutputStream和PrintStream的超类
(5)DataOutputStream是数据输出流,用来装饰其他的输出流,允许应用程序以与机器无关方式向底层写入基本Java数据类型。
(6)BufferedOutputStream是缓冲输出流,它的作用是为另一个输出流添加缓冲功能。
(7)PrintStream是打印输出流,用来装饰其他输出流,为其他输出流添加功能,方便的打印各种数据值
(8)FileOutputStream是文件输出流,通常用于向文件进行写入操作。
(9)ObjectOutputStream是对象输出流,它和ObjectInputStream一起对基本数据或者对象的持久存储。
方法:
示例:
示例:复制文件内容
|
|
五、字符流
使用字节流操作汉字或特殊符号语言的时候容易乱码,因为汉字不止一个字节,为了解决这个问题,建议使用字符流。一般可以用记事本打开的文件,我们可以看到内容不乱码的。就是文本文件,可以使用字符流。而操作二进制文件(比如图片、音频、视频)必须使用字节流。
Reader
Reader 是所有的输入字符流的父类,它是一个抽象类。
子类:
CharReader、StringReader 是两种基本的介质流,它们分别将Char 数组、String中读取数据。PipedReader 是从与其它线程共用的管道中读取数据。
BufferedReader 很明显就是一个装饰器,它和其子类负责装饰其它Reader 对象。
FilterReader 是所有自定义具体装饰流的父类,其子类PushbackReader 对Reader 对象进行装饰,会增加一个行号。
InputStreamReader 是一个连接字节流和字符流的桥梁,它将字节流转变为字符流。
FileReader 可以说是一个达到此功能、常用的工具类,在其源代码中明显使用了将 FileInputStream 转变为 Reader 的方法。我们可以从这个类中得到一定的技巧。
Reader 中各个类的用途和使用方法基本和 InputStream 中的类使用一致。
方法:
示例:
Writer
Writer 是所有的输出字符流的父类,它是一个抽象类。
子类:
CharArrayWriter、StringWriter 是两种基本的介质流,它们分别向Char 数组、String 中写入数据。PipedWriter 是向与其它线程共用的管道中写入数据,
BufferedWriter 是一个装饰器为 Writer 提供缓冲功能。
PrintWriter 和 PrintStream 极其类似,功能和使用也非常相似。
OutputStreamWriter 是 OutputStream 到 Writer 转换的桥梁,它的子类 FileWriter,功能和使用和 OutputStream 极其类似。
方法:
示例代码:
示例
用字符流完成文件的复制。
六、包装流
包含缓冲流,转换流对象流等等。
①、包装流隐藏了底层节点流的差异,并对外提供了更方便的输入\输出功能,让我们只关心这个高级流的操作。
②、使用包装流包装了节点流,程序直接操作包装流,而底层还是节点流和IO设备操作。
③、关闭包装流的时候,只需要关闭包装流即可。
缓冲流
缓冲流:是一个包装流,目的是缓存作用,加快读取和写入数据的速度。
字节缓冲流:BufferedInputStream、BufferedOutputStream
字符缓冲流:BufferedReader、BufferedWriter
示例:
字符缓冲流使用与字节缓冲流非常像,直接替换上例中对应的类即可。
转换流
把字节流转换为字符流。
InputStreamReader:把字节输入流转换为字符输入流
OutputStreamWriter:把字节输出流转换为字符输出流
示例:用转换流进行文件的复制
OutputStreamWriter 和 InputStreamReader 是字符和字节的桥梁:也可以称之为字符转换流。字符转换流原理:字节流+编码表。
FileWriter 和 FileReader:作为子类,仅作为操作字符文件的便捷类存在。当操作的字符文件,使用的是默认编码表时可以不用父类,而直接用子类就完成操作了,简化了代码。
这三句代码的功能是一样的,其中第三句最为便捷。
注意:一旦要指定其他编码时,绝对不能用子类,必须使用字符转换流。
内存流
把数据先临时存在数组中,也就是内存中。所以关闭内存流是无效的,关闭后还是可以调用这个类的方法。底层源码的 close() 是一个空方法。
字节内存流:ByteArrayOutputStream 、ByteArrayInputStream
字符内存流:CharArrayReader、CharArrayWriter
字符串流:StringReader,StringWriter(把数据临时存储到字符串中)
示例:
合并流
把多个输入流合并为一个流,也叫顺序流,因为在读取的时候是先读第一个,读完了在读下面一个流。
七、序列化与反序列化(对象流)
what
序列化:指把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。这个过程称为序列化。通俗来说就是将数据结构或对象转换成二进制串的过程。
反序列化:把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。也就是将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
why
①、在分布式系统中,此时需要把对象在网络上传输,就得把对象数据转换为二进制形式,需要共享的数据的 JavaBean 对象,都得做序列化。
②、服务器钝化:如果服务器发现某些对象好久没活动了,那么服务器就会把这些内存中的对象持久化在本地磁盘文件中(Java对象转换为二进制文件);如果服务器发现某些对象需要活动时,先去内存中寻找,找不到再去磁盘文件中反序列化我们的对象数据,恢复成 Java 对象。这样能节省服务器内存。
how
①、需要做序列化的对象的类,必须实现序列化接口:Java.lang.Serializable 接口(这是一个标志接口,没有任何抽象方法),Java 中大多数类都实现了该接口,比如:String,Integer。
②、底层会判断,如果当前对象是 Serializable 的实例,才允许做序列化,Java对象 instanceof Serializable 来判断。
③、在 Java 中使用对象流来完成序列化和反序列化。
ObjectOutputStream:通过 writeObject()方法做序列化操作
ObjectInputStream:通过 readObject() 方法做反序列化操作
demo
第一步:创建一个 JavaBean 对象
第二步:使用 ObjectOutputStream 对象实现序列化
第三步:使用ObjectInputStream 对象实现反序列化
question
问题1:如果某些数据不需要做序列化,比如密码,比如上面的年龄?
解决办法:在字段面前加上 transient。
那么我们在反序列化的时候,打印出来的就是Person [name=vae, age=0],整型数据默认值为 0。
问题2:序列化版本问题,在完成序列化操作后,由于项目的升级或修改,可能我们会对序列化对象进行修改,比如增加某个字段,那么我们在进行反序列化就会报错,怎么办?
解决办法:在 JavaBean 对象中增加一个 serialVersionUID 字段,用来固定这个版本,无论我们怎么修改,版本都是一致的,就能进行反序列化了。