Java 的 I/O 建立于流(stream)之上,输入流用来读取数据,输出流用来写入数据。不同的流会读、写某个特定的数据源,但所有输入、输出流都使用相同的基本方法来读取、写入数据。Reader 和 Writer 可以串链到输入流或输出流上,允许程序读、写文本(即字符)而不是字节。
传统 IO 的交互方式是同步、阻塞的,当程序请求一个流读、写一段数据时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。传统 IO 的好处是代码简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为性能瓶颈。
因此在 Java 1.4 中引入了 NIO 框架,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。并且在 Java 7 中 NIO 有了进一步改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也叫 AIO。AIO 基于事件和回调机制,即应用操作会直接返回,而不会阻塞在那里,当后台处理完成后,操作系统会通知相应线程进行后续工作。
字节流
字节流的基本单位为字节,一个 byte 通常为 8 位。字节流有两个基类:InputStream(输入字节流)和 OutputStream(输出字节流)。常用字节流的继承关系图如下图所示:
1. OutputStream
Java 的基本输出流是 java.io.OutputStream,这个类提供了写入数据所需的基本方法,包括:
public abstract class OutputStream implements Closeable, Flushable {
public abstract void write(int b) throws IOException;
public void write(byte b[]) throws IOException
public void write(byte b[], int off, int len) throws IOException
public void flush() throws IOException
public void close() throws IOException
......
}
OutputStream 的子类就使用这些方法向某种特定介质写入数据,例如 FileOutputStream 使用这些方法将数据写入文件,ByteArrayOutputStream 使用这些方法将数据写入可扩展的字节数组。
OutputStream 的基本方法是 write(int b),这个方法接受一个 0 到 255 之间的整数作为参数,用于将对应的字节写入到输出流中。这个方法声明为抽象方法,因为各个子类需要修改这个方法来处理特定的介质。注意,虽然这个方法接受一个 int 作为参数,但它实际上会写入一个无符号字节。Java 没有无符号字节数据类型,所以这里要使用 int 来代替。如果将一个超出 0 到 255 的 int 传入该方法,将写入这个数的最低字节,其他 3 字节将被忽略(这正是将 int 强制转换为 byte 的结果)。
一次写入 1 字节通常效率不高,如果有多字节要发送,则一次全部发送不失为一个好主意。使用 write(byte b[]) 或 write(byte b[], int off, int len) 通常比一次写入 data 数组中的 1 字节要快得多。
与在网络硬件中缓存一样,流还可以在软件中得到缓冲,可通过把 BufferedOutputStream 或 BufferedWriter 串链到底层流上来实现。因此,在写入数据完成后,刷新(flush)输出流非常重要。flush() 方法可以强迫缓冲的流发送数据,即使缓冲区还没有满。相应地,应当在关闭流之前立即刷新输出所有流。否则,关闭流时留在缓冲区中的数据可能会丢失。
最后,当结束一个流的操作时,要通过调用它的 close() 方法将其关闭。这会释放与这个流关联的所有资源,如文件句柄或端口。一旦输出流关闭,继续写入时就会抛出 IOException 异常。在一个长时间运行的程序中,如果未能及时关闭一个流,则可能会泄漏文件句柄、网络端口和其他资源
2. InputStream
Java 的基本输入流是 java.io.InputStream,这个类提供了将数据读取为原始字节所需的基本方法,包括:
public abstract class InputStream implements Closeable {
public abstract int read() throws IOException;
public int read(byte b[]) throws IOException
public int read(byte b[], int off, int len) throws IOException
public int available() throws IOException
public void close() throws IOException
......
}
InputStream 的子类就使用这些方法从某种特定介质中读取数据,例如 FileInputStream 使用这些方法从文件中读取数据,ByteArrayOutputStream 使用这些方法从字节数组中读取数据。
InputStream 的基本方法是没有参数的 read() 方法,这个方法从输入流的源中读取 1 字节数据,作为一个 0 到 255 的 int 返回,流的结束通过返回 -1 来表示,read() 方法会阻塞等待直到有 1 字节的数据可供读取。这个方法声明为抽象方法,因为各个子类需要修改这个方法来处理特定的介质。
与一次写入 1 字节的数据一样,一次读取 1 字节的效率也不高。因此,有两个重载的 read() 方法,可以用从流中读取的多字节的数据填充一个指定的数组:read(byte b[]) 尝试填充指定的字节数组、read(byte b[], int off, int len) 尝试填充指定的从 off 开始连续 len 字节的字节数组。注意:尝试填充有时不一定会成功,有时也只会填充部分字节,因此这些 read() 方法会返回实际读取的字节数。
下面示例从 inputStream 向数组 input 中读入 1024 个字节,注意要在循环中读取并且对 -1 进行判断:
int bytesRead = 0;
int bytesToRead = 1024;
byte[] input = new byte[bytesToRead];
while(bytesRead < bytesToRead) {
int result = inputStream.read(input, bytesRead, bytesToRead - bytesRead);
if (result == -1) {
break;
}
bytesRead += result;
}
如果不想等待所需的全部字节都立即可用,可以使用 available() 方法来确定在不阻塞的情况下有多少字节可以读取。它会返回可以读取的最少字节数,事实上还能读取更多字节,但至少可以读取 available() 建议的字节数。
与输出流一样,一旦结束对输入流的操作,应当调用它的 close() 方法将其关闭。这会释放与这个流关联的所有资源,如文件句柄或端口。一旦输入流关闭,进一步读取这个流就会抛出 IOException 异常。
InputStream 还有 3 个不太常用的方法,允许程序备份和重新读取已经读取过的数据:
public synchronized void mark(int readlimit)
public synchronized void reset() throws IOException
public boolean markSupported()
为了重新读取数据,要用 mark() 方法标记流的当前位置,在以后的某个时刻,可以用 reset() 方法把流重置到之前标记的位置,接下来的读取操作会返回从标记位置开始的数据。一个流在任何时刻都只能有一个标记,标记第二个位置会清除第一个标记。不过,不是所有输入流都支持重置,在尝试使用 mark 和 reset 之前,要先检查 markSupported() 方法是否返回 true。
读取字符串示例:
StringBuilder sb = new StringBuilder();
for (int ch; (ch = inputStream.read()) != -1; ) {
sb.append((char) ch);
}
String myString = sb.toString();
过滤器流
InputStream 和 OutputStream 是相当原始的类,为此 Java 又提供了很多过滤器类,通过装饰者模式可以附加到原始流中,在原始字节和各种格式之间来回转换。过滤器有两个版本:过滤器流以及 Reader/Writer,过滤器流主要将原始数据作为字节处理,而 Reader/Writer 用于操作字符,增加了字符编解码等功能,本质上计算机操作的都是字节,不管是网络通信还是文件读取,Reader/Writer 相当于构建了应用逻辑和原始数据间的桥梁。
1. BufferedOutputStream
BufferedOutputStream 将写入的数据存储在缓冲区中,直到缓冲区满或刷新缓冲区,然后将数据一次性全部写入底层输出流。一次写入多个字节与多次写入少量字节相比要快得多。
对于网络连接尤其是这样,因为每个 TCP 片或 UDP 包本身都有一定数量的开销,一般约为 40 字节。这意味着,如果一次发送 1 字节,那么发送 1K 数据实际上需要发送 40K,而一次全部发送只需要发送 1K 多一点数据。大多数网卡和 TCP 实现自身都提供了一定程度的缓冲,所以实际数量不会那么夸张。但缓冲网络输出通常会带来巨大的性能提升。
public class BufferedOutputStream extends FilterOutputStream {
protected byte buf[];
protected int count;
......
}
BufferedOutputStream 没有声明自己的任何新公共方法,调用它的方法与调用任何输出流的方法是一样的。区别在于,每次写入会把数据放在缓冲区中,而不是直接放入底层的输出流。因此,需要发送数据时应用刷新输出流,这一点非常重要!
2. BufferedInputStream
BufferedInputStream 也有一个作为缓冲区的字节数组,当调用该类的 read() 方法时,它首先尝试从缓冲区获取请求的数据。只有当缓冲区没有数据时,流才从底层的源中读取数据。这时,它会从源中读取尽可能多的数据存入缓冲区,而不管是否马上需要所有这些数据。不会立即用到的数据可以在以后调用 read() 时读取。
当从本地磁盘中读取文件时,从底层流中读取几百字节的数据与读取 1 字节数据几乎一样快。因此,缓冲可以显著提升性能。对于网络连接,这种效果则不明显,在这里瓶颈往往是网络传送数据的速度,而不是网络接口向程序传送数据的速度。尽管如此,缓冲输入没有什么坏处,随着网络的速度加快会变得更为重要。
public class BufferedInputStream extends FilterOutputStream {
protected volatile byte buf[];
protected int count;
protected int pos;
protected int markpos = -1;
protected int marklimit;
......
}
BufferedInputStream 也没有声明自己的任何新公共方法,只重写了 InputStream 的方法,支持标记和重置。重载的 read() 方法尝试根据需要多次从底层输入流中读取数据,从而完全填充指定的字节数组。只有当字节数组完全填满、到达流的末尾或底层流阻塞而无法进一步读取时,重载的这两个 read() 方法才返回。大多数输入流都不这样做,它们在返回前只从底层流或数据源中读取一次。
读取字符串示例:
BufferedInputStream bis = new BufferedInputStream(inputStream);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
for (int result = bis.read(); result != -1; result = bis.read()) {
buf.write((byte) result);
}
String myString = buf.toString("UTF-8");
字符流
字符流的基本单位为 Unicode,大小为两个字节(Byte),通常用来处理文本数据。Java 提供了字符流的两个基类:java.io.Reader 指定读字符的 API、java.io.Writer 指定写字符的 API。对应输入和输出流使用字节的地方 Reader/Writer 会使用 Unicode 字符。常用字符流的继承关系图如下图所示:
Reader/Writer 最重要的具体子类是 InputStreamReader 和 OutputStreamWriter 类。把面向字节的接口改为面向字符的接口。InputStreamReader 包含一个底层输入流,可以从中读取原始字节,并根据指定的编码方式将这些字节转换为 Unicode 字符。OutputStreamWriter 从运行的程序中接收 Unicode 字符,然后使用指定的编码方式将这些字符转换为字节,再将这些字节写入底层输出流中。
1. Writer
Writer 类是 OutputStream 类的映射,它是一个抽象类,主要有 5 个 write() 方法,另外还有 flush() 和 close() 方法:
public abstract class Writer implements Appendable, Closeable, Flushable {
public void write(int c) throws IOException
public void write(char cbuf[]) throws IOException
public abstract void write(char cbuf[], int off, int len) throws IOException;
public void write(String str) throws IOException
public void write(String str, int off, int len) throws IOException
......
}
其中 write(char cbuf[], int off, int len) 是基础方法,其他四个 write() 都是根据它实现的。子类至少要重写这个方法及 flush() 和 close() 方法。
2. OutputStreamWriter
OutputStreamWriter 是 Writer 最重要的子类,它根据指定的编码方式将这些字符转换成字节,并写入底层输出流。它的构造函数指定了要写入的输出流和使用的编码方式:
public OutputStreamWriter(OutputStream out, String charsetName)
throws UnsupportedEncodingException
如果没有指定编码方式,就使用平台的默认编码方式。但默认字符集可能会在出乎意料的时候导致意外的问题,如果能明确指定字符集,这往往比让 Java 为你选择一个字符集要好。
3. Reader
Reader 类是 InputStream 类的映射,它是一个抽象类,主要有 3 个 read() 方法,另外还有 skip()、close()、ready()、mark()、reset() 和 markSupported() 方法:
public abstract class Reader implements Readable, Closeable {
public int read() throws IOException
public int read(char cbuf[]) throws IOException
public abstract int read(char cbuf[], int off, int len) throws IOException;
public long skip(long n) throws IOException
public boolean ready() throws IOException
public boolean markSupported()
public void mark(int readAheadLimit) throws IOException
public void reset() throws IOException
public abstract void close() throws IOException;
......
}
其中 read(char cbuf[], int off, int len) 是基础方法,其他两个 read() 都是根据它实现的。子类至少要重写这个方法及 close() 方法。
read() 方法将一个 Unicode 字符作为一个 int 返回,可以是 0~65535 的一个值或在流结束时返回 -1。其他两个 read() 尝试使用字符填充字符数组,并返回实际读取的字符数或在流结束时返回 -1
skip() 方法跳过 n 个字符
mark() 和 reset() 方法允许一些 Reader 重置到字符序列中做标记的位置。markSupported() 方法告知该 Reader 是否支持标记和重置
ready() 方法与 InputStream 的 available() 的用途相同,但语义不尽相同。available() 返回一个 int 表示可以无阻塞地最少读取多少字节,但 ready() 只返回一个 boolean 表示是否可以无阻塞地读取。因为有些字符编码方式对于不同字符使用不同数量的字节,因此在实际从缓冲区读取之前,很难说有多少个字符正在网络或文件系统的缓冲区中等待。
4. InputStreamReader
InputStreamReader 是 Reader 最重要的子类,它从底层输入流中读取字节,并根据指定的编码方式将这些字节转换成字符,并返回这些字符。它的构造函数指定了要读取的输入流和使用的编码方式:
public InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException
读取字符串示例:
Reader in = new InputStreamReader(inputStream, "UTF-8");
for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) {
out.append(buffer, 0, numRead);
}
String myString = out.toString();
5. BufferedReader、BufferedWriter
BufferedReader 和 BufferedWriter 是基于字符的缓冲区流,相应的,BufferedReader 和 BufferedWriter 也使用一个内部字符数组作为缓冲区。
当程序从 BufferedReader 读取时,文本会从缓冲区得到,而不是直接从底层输入流或其他文本源读取。当缓冲区清空时,将用尽可能多的文本再次填充,尽管这些文本不是全部都立即需要,这样可以使以后的读取速度更快。当程序写入一个 BufferedWriter 时,文本被放置在缓冲区中,只有当缓冲区满或者显示刷新时,文本才会被移到底层输出流或其他目标,这使得写入也要快很多。
此外,BufferedReader 中还有一个 readLine() 方法,它读取一行文本,并作为一个字符串返回;在 Java 8 中还增加了一个 lines() 方法用于返回字符流:
public String readLine() throws IOException
public Stream<String> lines()
在 BufferedWriter 中也增加了一个新的 newLine() 方法,用于向输出流插入一个与平台有关的行分隔字符串
public void newLine() throws IOException
读取字符串示例:
String newLine = System.getProperty("line.separator");
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder result = new StringBuilder();
for (String line; (line = reader.readLine()) != null; ) {
if (result.length() > 0) {
result.append(newLine);
}
result.append(line);
}
String myString = result.toString();