Java漫游笔记-10-IO

所谓“流”,就是数据(字节或者是字符)的有序排列。

输入流和输出流

如果按照流动的方向来划分,流可以分为:输入流和输出流。
这里的输入和输出都是相对 Java 程序而言的。
Java 程序从输入流读取数据,向输出流写入数据

输入流和输出流

字节流和字符流

如果按照数据类型进行划分,流又可以分为:字节流和字符流。
字节流:数据流中最小的数据单元是字节(byte)。
字符流:数据流中最小的数据单元是字符(char,Java 采用 Unicode 编码, 1 char = 2 byte)。

以下是使用字节流和字符流的一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public void testByteStream() throws IOException {
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream("Java漫游笔记.txt");
out = new FileOutputStream("Java漫游笔记副本1.txt");
int c; // c 存储的是一个字节,8 bit
while ((c = in.read()) != -1) {
out.write(c);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}

public void testCharacterStream() throws IOException {
Reader in = null;
Writer out = null;
try {
in = new FileReader("Java漫游笔记.txt");
out = new FileWriter("Java漫游笔记副本2.txt");
int c; // c 存储的是一个字符,16 bit
while ((c = in.read()) != -1) {
out.write(c);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}

可以看出,这两段代码非常相似,其最主要的区别在于它们分别使用了不同的类。
字节流的输入输出流分别使用 FileInputStreamFileOutputStream ,而字符流则使用了 FileReaderFileWriter
字节流每次读写一个字节,而字符流则每次读写一个字符。
在磁盘中,所有文件都是按字节储存的,这也就意味着,字节流可以用于任意类型的对象,而字符流只能处理字符对象。
那么,既然字节流是万能的,为什么还需要字符流呢?
原因在于,字符对象(例如:字符串,文本文件)是我们经常需要操作的对象,而从字节到字符的转换,也需要进行一些额外的操作,因此,在字节流之上再包装一层,以提供更便捷的 API 是很有必要的。

设计思想

java.io 包的层次结构来看,所有的字节流都继承自 InputStreamOutputStream ,而所有的字符流都继承自 ReaderWriter
InputStreamReaderOutputStreamWriter 则提供了二者流通的桥梁。
除此之外,还包含许多 InputStreamOutputStreamReaderWriter 的子类。

IO类概览

Java IO 库的设计,使用了两个结构模式:装饰模式和适配器模式。

装饰模式的应用

装饰模式在 IO 库中主要体现在,链接流处理器。
通过把不同的流整合到一个链中,就可以动态地添加功能,以便实现更高级的输入、输出操作。
例如,在前面的例子中,我们读取数据使用的是 in.read() ,这样一次读取一个字节是很慢的,我们可以从磁盘中一次读取一大块数据,然后再从读到的数据块中获取字节。这也就是我们常说的“缓冲”。
为了实现缓冲,可以把 FileInputStream 包装到 BufferedInputStream 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void testBufferedStream() throws IOException {
InputStream in = null;
OutputStream out = null;
try {
in = new BufferedInputStream(new FileInputStream("Java漫游笔记.txt"));
out = new BufferedOutputStream(new FileOutputStream(
"Java漫游笔记副本3.txt"));
byte[] buf = new byte[8192];
int len = 0; // 读入缓冲区的总字节数
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}

当然,缓冲只是通过流整合实现的其中的一种效果,我们还可以把 InputStream 包装到 DataInputStream 中,来获得 Java 基本数据类型的读取能力。
甚至,还可以自行编写包装标准流的类,来实现我们想要的效果。

IO装饰模式

适配器模式的应用

适配器模式通常有 2 种实现方式:

  • 一种是类适配,通过类似多继承的形式实现(例如:class Adapter extends Adaptee implements Target);
  • 而另外一种是对象适配,一般是通过聚合来实现。

java.io 包中,所有的原始流处理器都是适配器类。
还是以 FileInputStream 为例,它继承了 InputStrem ,同时还持有一个对 FileDiscriptor 的引用。这是一个将 FileDiscriptor 对象适配成 InputStrem 类型的对象的适配器模式。

IO适配器模式

InputStreamReader (严格来说,应该是 StreamDecoder )的设计也应用了适配器模式:

IO适配器模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void testByteStreamToCharacterStream() throws IOException {
Reader in = null;
Writer out = null;
try {
// 将 InputStream 适配成 Reader
in = new InputStreamReader(new FileInputStream("Java漫游笔记.txt"));
out = new OutputStreamWriter(
new FileOutputStream("Java漫游笔记副本4.txt"));
int c; // c 存储的是一个字符,16 bit
while ((c = in.read()) != -1) {
out.write(c);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}

文件

文件是我们最近常见的一种数据源。学习 IO ,就不能不学文件的操作。
在 Java 中,File 类是对文件和目录的抽象。通过 File 类可以获取文件和目录的信息,并管理它们。

创建

创建文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 只是创建了一个 File 对象,并没有真正创建文件
// 默认在当前工作目录下创建,可以通过 System.getProperty("user.dir") 获取具体的路径
File file = new File("filename.suffix");

// 在 D 盘下创建文件
File anotherFile = new File("D:" + File.separator + "filename.suffix");
try {
// 创建一个新的空文件
// 如果指定的文件不存在并成功地创建,则返回 true
// 如果指定的文件已经存在,则返回 false
anotherFile.createNewFile();
} catch (IOException e) {
// 如果没有写权限,会抛出异常,java.io.IOException: 拒绝访问。
}

创建目录

1
2
3
4
5
6
7
File path = new File("newpath");
// 创建目录
path.mkdir();

File paths = new File("home" + File.separator + "user");
// 如果父目录不存在,也会一并创建
paths.mkdirs();

删除

1
2
3
// 删除文件或目录
// 需要注意的是:如果是删除目录,必须为空才能删除
fileOrPath.delete();

如果需要删除非空目录,一般需要进行递归操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void deleteDirectory(File dir) {
if (dir != null && dir.exists()) {
if (dir.isDirectory()) {
File[] files = dir.listFiles();
for (File file : files) {
deleteDirectory(file);
}
// 删除目录
dir.delete();
System.out.println("Directory is deleted : " + dir.getAbsolutePath());
} else {
// 直接删除文件
dir.delete();
System.out.println("File is deleted : " + dir.getAbsolutePath());
}
}
}

读写

读写操作可以参考前面输入输出流的示例代码。

其他

  • 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 包,提供了一些使用起来更方便的类,例如:FilesPathsPath
事实上,基础 IO 包中的一些类,也就此更新了一些 API 。
例如:java.io.File 类中新增了 toPath() 方法,可以 File 转化为新的 Path
这些新的类和 API 会逐渐流行起来。因此,如果使用新版本的 JDK ,建议优先使用它们。

try-with-resources

JDK 1.7 还新增了一个 IO 相关的语法糖,称为 “try-with-resource” 异常处理机制。可以帮助我们自动关闭资源,减少异常处理代码。

1
2
3
4
5
6
7
8
9
10
11
public void testTryWithResources() throws IOException {
try (
FileReader fr = new FileReader("Java漫游笔记.txt");
BufferedReader br = new BufferedReader(fr);
) {
String str;
while ((str = br.readLine()) != null) {
System.out.println(str);
}
}
}

总结

对于流处理器而言,我们关心的是数据的内容,而 File 关注的则是数据的存储介质。
首先确定我们的程序是需要读取数据还是写入数据。
然后按照输入、输出源(数组、文件、对象、字符)的类型选用不同类。
通过流的组合,可以实现增强的功能(例如:缓存、读写基本类型、按行读取、格式化等等)。
基础 IO 使用简单,但会阻塞线程;而 NIO 可以使用一个(或几个)线程管理多个通道(网络连接或文件),但从多个通道中解析数据可能会更加复杂。
如果需要管理同时打开的成大量连接,并且这些连接每次只是发送少量的数据(例如:即时通讯服务器),使用 NIO 可能是一个明智的选择。
相反,如果你只少量的连接,并且这些连接会使用非常高的带宽,那么,使用基础的 IO 实现可能会更为恰当。