2.1 输入/输出流
在Java API中,可以从其中读入一个字节序列的对象叫做输入流,而可以向其中写入一个字节序列的对象叫做输出流。这些字节序列的来源和目的是文件,文件是一个抽象概念,可以是磁盘上的文件,也可以是网络连接,或者是一个内存块。抽象类InputStream和OutputStream构成了输入/输出(I/O)类层级的基础。
在Java应用中,字符都是用UTF-16格式编码的char值来表示的(2个字节),即读取的字节(无论其本身是什么字符编码)最终需要转换为char,而写出的字节最终需要转化为二进制字节(无论其本身是什么字符编码)。为了方便char值的读写,Java 提供了基于抽象类Reader和Writer的独立类层次结构。
2.1.1 读写字节
InputStream抽象类定义了以下抽象方法:
public abstract int read() throws IOException;
这个方法将从当前游标处读入一个字节,并返回读入的字节,或者在遇到输入源结尾时范围-1。在继承并实现具体的输入流时,子类必须实现这个方法。例如,FileInputStream类的read()方法将从某个文件中读入一个字节。
InputStream还有其他非抽象方法,它们都调用read()方法来完成更复杂的功能,比如读入一个字节数组,或者跳过指定数量的字节。另外available()方法将放回可读入的字节数量,具体实现由各子类决定。有些会返回总子节数,有一些返回剩余字节数。
类似的,OutputStream抽象类定义了以下抽象方法
public abstract void write(int b) throws IOException;
它可以向当前游标处写出一个字节。
read()和write()方法在执行时都将阻塞,直至字节确实被读入或写出。这就意味着如果流不能被立即访问(比如网络繁忙),那么当前线程将被阻塞
当你完成对输入/输出流的读写时,应该通过调用close方法来关闭它,这个调用会释放掉十分珍贵的OS资源。关闭一个输出流的同时还会冲刷用于该输出流的缓冲区(若有的话):所有被临时置于缓冲区中,以便用更大的包的形式传递的字节在关闭输出流时都将被送出。当然,也可以手动调用flush()方法来手动冲刷。如果既不手动冲刷,也不关闭流,那么此时缓冲区中的数据可能永远也不会真的写出。
注意,这里所谓的flush(),只能保证冲刷流自己实现的缓冲区(比如BufferedOutputStream)。而底层操作系统本身其实也会在内核中自动开辟一个缓冲区,那个缓冲区是由操作系统维护的,Java程序控制不了。因此,流的flush()方法,只保证所有字节通过操作系统调用提交了,具体操作系统怎么写入目标文件,那是操作系统的事情。 参考资料:JAVA NIO之浅谈内存映射文件原理与DirectMemory
2.1.2 完整的流家族
流分为字节I/O和char I/O两个独立类体系,其中又各自包含输入和输出两个独立类体系。
Reader和Writer抽象类分别定义了如下抽象方法:
// Reader
// 返回一个UTF-16的码元(0~65545之间的整数),或者在碰到文件结尾时返回-1
abstract public int read(char cbuf[], int off, int len) throws IOException;
// Writer
// 写出一个UTF-16的码元(0~65545之间的整数)
abstract public void write(char cbuf[], int off, int len) throws IOException;
I/O相关的其他接口
- Closeable:代表流可以调用Close()方法进行关闭。另外由于这个接口扩展了AutoCloseable,因此实现类可以使用try-with-resource语法。InputStream、OutputStream、Reader、Writer都实现了这个接口。(为何不直接使用而是扩展AutoCloseable?这是因为Closeable只能抛出IOException,而AutoCloseable可以抛出任何异常。)
- Flushable:代表流可以调用flush()方法冲刷缓冲区。OutputStream和Writer实现了这个接口。
- Readable:提供一个CharBuffer的能力。
Appendable:提供添加单个字符和字符序列的能力。只有Writer实现了它。
2.1.3 组合输入/输出过滤器
Java中的流可以分为两类,
一类是“底层流”,这些流负责直接与底层文件系统打交道。比如FileInputStream或FileOutputStream,直接与磁盘文件打交道。
- 一类是“过滤器流”,这些流负责与其他流打交道,完成各种转换工作。比如BufferedInputStream或BufferedOutputStream,提供缓冲功能。
通过在底层流外面包裹多层过滤器流,从而构建出功能各异的流管道。
2.2 文本输入与输出
在Java存在内码和外码的概念。
- 内码:即Java应用中表示字符所用的编码,即char值的编码,这个编码总是使用UTF-16。
- 外码:外部文件所采用的编码,可能是任何编码,比如UTF-8、UTF-32、ASCII等等。
在Java中,我们操作的总是char。而在读取和写出时,外部文件可能是任何编码。因此,这里存在这内码和外码之间的相互转换问题。而处理这个问题的类,就是文本流:Reader和Writer。
文本流本身不负责字节对外的读写,因此它也不和底层文件打交道。如果文本最终的交互目标是外部文件,那文本流底层必须依赖一个二进制流。换言之,在和外部文件打交道的场景下,文本流本身就是过一种滤器流。而字节流的直接包装器,是InputStreamReader和OutputStreamWriter。
2.2.1 如何写出文本输出
2.2.2 如何读取文本输入
2.3 读写二进制数据
2.3.1 DataInput和DataOutput接口
2.3.2 随机访问文件
2.3.3 ZIP文档
2.4 对象输入/输出流与序列化
2.4.1 保存和加载序列化对象
首先,要求对象本身是支持序列化的。这就要求类实现Serializable接口,这是一个标记接口。
为了保存对象,需要使用ObjectOutputStream对象。
为了将对象读回,需要使用ObjectInputStream对象。
2.5 操作文件
Paths和Files工具类封装了在用户机器上处理文件系统所需的所有功能。
2.5.1 Path
Path类代表一个与系统无关的路径,用于在文件系统中定位一个文件,通常可配合下方的Files工具类完成各种文件操作功能。Paths则是Path相关的工具类。
2.5.2 Files
读取和操作文件的工具类。通常需要配合Path类来使用。Path负责定位文件,Files负责操作文件。