流
所谓“流”,就是数据(字节或者是字符)的有序排列。
输入流和输出流
如果按照流动的方向来划分,流可以分为:输入流和输出流。
这里的输入和输出都是相对 Java 程序而言的。
Java 程序从输入流读取数据,向输出流写入数据。
字节流和字符流
如果按照数据类型进行划分,流又可以分为:字节流和字符流。
字节流:数据流中最小的数据单元是字节(byte)。
字符流:数据流中最小的数据单元是字符(char,Java 采用 Unicode 编码, 1 char = 2 byte)。
以下是使用字节流和字符流的一个示例:
1 | public void testByteStream() throws IOException { |
可以看出,这两段代码非常相似,其最主要的区别在于它们分别使用了不同的类。
字节流的输入输出流分别使用 FileInputStream
和 FileOutputStream
,而字符流则使用了 FileReader
和 FileWriter
。
字节流每次读写一个字节,而字符流则每次读写一个字符。
在磁盘中,所有文件都是按字节储存的,这也就意味着,字节流可以用于任意类型的对象,而字符流只能处理字符对象。
那么,既然字节流是万能的,为什么还需要字符流呢?
原因在于,字符对象(例如:字符串,文本文件)是我们经常需要操作的对象,而从字节到字符的转换,也需要进行一些额外的操作,因此,在字节流之上再包装一层,以提供更便捷的 API 是很有必要的。
设计思想
从 java.io
包的层次结构来看,所有的字节流都继承自 InputStream
和 OutputStream
,而所有的字符流都继承自 Reader
和 Writer
。InputStreamReader
和 OutputStreamWriter
则提供了二者流通的桥梁。
除此之外,还包含许多 InputStream
、OutputStream
、Reader
、Writer
的子类。
Java IO 库的设计,使用了两个结构模式:装饰模式和适配器模式。
装饰模式的应用
装饰模式在 IO 库中主要体现在,链接流处理器。
通过把不同的流整合到一个链中,就可以动态地添加功能,以便实现更高级的输入、输出操作。
例如,在前面的例子中,我们读取数据使用的是 in.read()
,这样一次读取一个字节是很慢的,我们可以从磁盘中一次读取一大块数据,然后再从读到的数据块中获取字节。这也就是我们常说的“缓冲”。
为了实现缓冲,可以把 FileInputStream
包装到 BufferedInputStream
中:
1 | public void testBufferedStream() throws IOException { |
当然,缓冲只是通过流整合实现的其中的一种效果,我们还可以把 InputStream
包装到 DataInputStream
中,来获得 Java 基本数据类型的读取能力。
甚至,还可以自行编写包装标准流的类,来实现我们想要的效果。
适配器模式的应用
适配器模式通常有 2 种实现方式:
- 一种是类适配,通过类似多继承的形式实现(例如:
class Adapter extends Adaptee implements Target
); - 而另外一种是对象适配,一般是通过聚合来实现。
在 java.io
包中,所有的原始流处理器都是适配器类。
还是以 FileInputStream
为例,它继承了 InputStrem
,同时还持有一个对 FileDiscriptor
的引用。这是一个将 FileDiscriptor
对象适配成 InputStrem
类型的对象的适配器模式。
InputStreamReader
(严格来说,应该是 StreamDecoder
)的设计也应用了适配器模式:
1 | public void testByteStreamToCharacterStream() throws IOException { |
文件
文件是我们最近常见的一种数据源。学习 IO ,就不能不学文件的操作。
在 Java 中,File
类是对文件和目录的抽象。通过 File
类可以获取文件和目录的信息,并管理它们。
创建
创建文件
1 | // 只是创建了一个 File 对象,并没有真正创建文件 |
创建目录
1 | File path = new File("newpath"); |
删除
1 | // 删除文件或目录 |
如果需要删除非空目录,一般需要进行递归操作:
1 | public void deleteDirectory(File dir) { |
读写
读写操作可以参考前面输入输出流的示例代码。
其他
oldFile.renameTo(newFile)
,可以重命名文件,需要注意的是,重命名无法移动文件。移动文件可以先重命名文件,再将原文件拷贝到新文件,最后删除原文件。- 判断:
isDirectory()
是否是一个目录isFile()
是否是一个文件exists()
文件或目录是否存在isHidden()
是否是一个隐藏文件canRead()
是否可以读取canWrite()
是否可以修改canExecute()
是否可以执行
getPath()
可以获取相对路径,getAbsolutePath()
可以获取绝对路径,getCanonicalPath()
可以获取标准的绝对路径(会移除多余的名称 “.” 和 “..” 之类的字符)。path.list()
和path.listFiles()
可以返回当前目录中的文件和子目录清单。FilenameFilter
文件名过滤器接口,可以用于筛选文件。RandomAccessFile
类,支持从指定的位置读写文件。
新特性
NIO
JDK 1.4 对 IO 做了大量的改进,新增了 java.nio
包(New I/O),主要对缓冲区(Buffer)、字符集(Charset)、通道(Channel)等方面做了补充。
NIO 和 IO 的主要区别在于:
- IO 是面向流的。这意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。并且,流的读写通常是单向的。而 NIO 则是基于通道( Channel )和缓冲区( Buffer )进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
- IO 中的各种流是阻塞的。这意味着当一个线程调用
read()
或write()
时,该线程会被阻塞,直到有一些数据被读取,或数据完全写入。而 NIO 是非阻塞的,当一个线程从某通道读取数据时,它仅能得到目前可用的数据,如果目前没有可用的数据,就什么都不会获取。在数据变的可以读取之前,该线程无需等待,而是可以继续去做其他的事情。非阻塞写亦是如此。对于 NIO 而言,一个线程可以同时管理多个通道,当一个通道上的读写需要等待时,它就会去执行其他通道上的读写操作。
NIO.2
JDK 1.7 针对 IO 也做了更新。新增的 java.nio.file
包,提供了一些使用起来更方便的类,例如:Files
、 Paths
和 Path
。
事实上,基础 IO 包中的一些类,也就此更新了一些 API 。
例如:java.io.File
类中新增了 toPath()
方法,可以 File
转化为新的 Path
。
这些新的类和 API 会逐渐流行起来。因此,如果使用新版本的 JDK ,建议优先使用它们。
try-with-resources
JDK 1.7 还新增了一个 IO 相关的语法糖,称为 “try-with-resource” 异常处理机制。可以帮助我们自动关闭资源,减少异常处理代码。
1 | public void testTryWithResources() throws IOException { |
总结
对于流处理器而言,我们关心的是数据的内容,而 File 关注的则是数据的存储介质。
首先确定我们的程序是需要读取数据还是写入数据。
然后按照输入、输出源(数组、文件、对象、字符)的类型选用不同类。
通过流的组合,可以实现增强的功能(例如:缓存、读写基本类型、按行读取、格式化等等)。
基础 IO 使用简单,但会阻塞线程;而 NIO 可以使用一个(或几个)线程管理多个通道(网络连接或文件),但从多个通道中解析数据可能会更加复杂。
如果需要管理同时打开的成大量连接,并且这些连接每次只是发送少量的数据(例如:即时通讯服务器),使用 NIO 可能是一个明智的选择。
相反,如果你只少量的连接,并且这些连接会使用非常高的带宽,那么,使用基础的 IO 实现可能会更为恰当。