> Data stored in a text file is represented in human-readable form. Data stored in a binary file is represented in binary form. You cannot read binary files. They are designed to be read by programs. For example, Java source programs are stored in text files and can be read by a text editor, but Java classes are stored in binary files and are read by the JVM. The advantage of binary files is that they are more efficient to process than text files.

## 1. Java的I/O抽象

### 1.1. 网络通讯对象的抽象

我们考虑一个问题，如果要实现两个程序之间的通讯应该如何使用面向对象的思维来设计？首先我们需要把通讯进行抽象化，如果使用一个对象来进行通讯，应该如何操作？
![Alt text](img/comm.drawio.svg)

在上图中，`Side_A`和`Side_B`分别代表通讯系统的两个端点，如果这两个端点要实现通讯，最简单的方式需要实现两个函数：

```java
public byte[] read(){...};                  // 读取
public void write(byte buf[]){...};         // 写入
```

这里无论是读取还是写入都使用到一个最基本的结构-字节数组；因为在计算机中，字节是处理数据的基本单位。当然，我们没有考虑到实际通讯当中的复杂性，例如：连接、断开、错误处理等等。但是这两个基本的函数可以是我们通讯的基础。

那么，要实现一个可以通讯的类就需要对这两个函数进行实现（完成函数体中的算法）。但是问题又来了，通讯可以是基于以太网的，也可以是基于无线的，或者是光通讯等多种通讯媒介。那么我们势必为每个通讯类都实现这两个函数。这样就有了下面的这个UML：

```mermaid
classDiagram
      class ComEthernet{
          +read()
          +write()
      }
      class ComWifi{
          +read()
          +write()
      }
      class ComFiber{
          +read()
          +write()
      }
```

通过前面的知识，我们可以看出，这三个类都有同样的函数签名，应该进行抽象，因此我们增加了一个抽象父类-Com：

```mermaid
classDiagram
	  Com <|-- ComEthernet
	  Com <|-- ComWifi
	  Com <|-- ComFiber

	  class Com{
	  <<Abstract>>
          +read()*
          +write()*
      } 	

      class ComEthernet{
          +read()
          +write()
      }
      class ComWifi{
          +read()
          +write()
      }
      class ComFiber{
          +read()
          +write()
      }
```

从上个例子中我们看到了如何从面向对象的思维来解决实际的问题。在即将学到的网络课程当中会发现网络的构建真的也有面向对象的思想。

### 1.2. 文件的抽象

好了，现在我们来看看主题：文件系统。文件中存放的最小单元也是字节（byte），因此可以沿用上面网络的例子。只不过，这是一个真实的

环境了，比上述的网络的例子要包含更多的内容，但是基本的思路是一致的。

![alt text](img/BufferedSteam.drawio.svg)

在文件系统中有个流（Stream）的概念，初学者可能对这个概念理解起来比较困难。上述两个抽象类（InputStream，OutputStream）其实主要的目的就是定义读和写的抽象方法：InputStream（读）；OutputStream（写）。后面的派生类都需要实现这两个方法。只不过加入了更多的内容。

分离读/写主要目的是因为有些类只需要读取或者写入功能。

**注意：InputStream（读）；OutputStream（写）这两个类不是只针对文件系统，凡是可以进行字节读写的类都可以由这两个类进行扩展（例如网络中的通讯）。从这里可以看到，Java不仅仅是面向对象的语言，由于Java提供了一套基本的API（类），使得Java也是一套编码规范或者是框架。多数情况下，需要满足Java中基本类型和接口的继承，才能使程序更具有通用性。**

#### 1.2.1. InputStream

![image-20230326144650268](img/image-20230326144650268.png)

**注意：`read()`函数后面的返回标注的是int，其实返回的值是0～255。因为Java没有unsigned类型，byte类型的取值范围达不到0～255，所以使用int作为返回。**

可以看到有多个关于读的函数，具体含义看书上的解释。

**提高：为什么只是read()函数是抽象函数，其几个重载的read函数并不是抽象函数？可以通过分析源码来理解。**

#### 1.2.2. OutputStream

![image-20230326151620467](img/image-20230326151620467.png)

同样，write函数的参数类型虽然是int，实际是写入一个字节。

## 2. 文件流的操作

### 2.1. FileInputStream/FileOutputStream

![alt text](img/BufferedSteam.drawio.svg)

这两个类型实现了对文件的读和写操作。

```java
import java.io.*;

public class TestFileStream {
	public static void main(String[] args) throws IOException {
		try (
				// Create an output stream to the file
				FileOutputStream output = new FileOutputStream("/home/danny/temp.txt");) {
			// Output values to the file
			for (int i = 1; i <= 10; i++)
				output.write(i);
		}

		try (
				// Create an input stream for the file
				FileInputStream input = new FileInputStream("/home/danny/temp.txt");) {
			// Read values from the file
			int value;
			while ((value = input.read()) != -1)
				System.out.print(value + " ");
		}
	}
}
```

还记得`try(...){...}`的语法形式吗？小括号中只能是构造函数的语句，大括号中可以是任意语句；小括号中打开的文件不用关闭close()，该语法可以保证打开的文件自动关闭。

**注意，这两个类实现都是字节的操作，因此文件是二进制文件。虽然后缀名是txt，如果尝试打开会得到乱码。**

> A java.io.FileNotFoundException would occur if you attempt to create a FileInputStream with a nonexistent file. 
>
> 当使用 FileInputStream 的构造函数打开一个不存在的文件的时候，将会抛出`java.io.FileNotFoundException`异常。

构造函数也是函数，因此也可以抛出异常。如果阅读源码，会发现`FileOutputStream`的构造函数也可以抛出异常。

**注意：这两个类的构造函数是重载的，有一个版本可以接受一个File对象作为需要打开的文件。其实在Java中，大多预定义类都可以接受字符串或者是File对象，具体情况需要查看源码实现。**

### 2.2. DataInputStream/DataOutputStream

在学习`DataInputStream` 和 `DataOutputStream` 前，我们先来看看其直接超类：`FilterInputStream` 和 `FilterOutputStream`

![alt text](img/BufferedSteam.drawio.svg)

> Filter streams are streams that filter bytes for some purpose. The basic byte input stream provides a read method that can only be used for reading bytes. If you want to read integers, doubles, or strings, you need a filter class to wrap the byte input stream. Using a filter class enables you to read integers, doubles, and strings instead of bytes and characters. FilterInputStream and FilterOutputStream are the base classes for filtering data. When you need to process primitive numeric types, use DataInputStream and DataOutputStream to filter bytes. 
>
> 书上没有说清楚，这两个类其实很简单，扩展自InputStream, 和 OutputStream，并实现了父类的基本方法，是一个典型的装饰器设计模式。同时，内部还包含了 InputStrem / OutputStream， 这样可以把多个 Filter 串联起来，实现数据的串联加工。例如加密、发送，接收、解密。讨论该类型超出了本课程的范围，有兴趣的同学可以自行研究一下。

关于这个类其实要讲的话会很多，现在大家知道是 DataInputStream 和 DataOutputStream 的超类就可以了。DataInputStream和DataOutputStream 内部实现了版本的管理，有兴趣的同学可以分析源码，看看写入的时候在文件的头部加入了相应的标记，以及其他一些附加数据。

再来看看DataInputStream 和 DataOutputStream，这两个类是可以实例化的，并可以读写文件的各种基本类型（原始类型）。

![alt text](img/BufferedSteam.drawio.svg)

这个类的基本接口如下图：

![image-20230326222237336](img/image-20230326222237336.png)

![image-20230326222303342](img/image-20230326222303342.png)

现在看看例子：

```java
import java.io.*;

public class TestDataStream {
	public static void main(String[] args) throws IOException {
		try ( // Create an output stream for file temp.dat
				DataOutputStream output = new DataOutputStream(new FileOutputStream("/home/danny/temp.dat"));) {
			// Write student test scores to the file
			output.writeUTF("John");
			output.writeDouble(85.5);
			output.writeUTF("Jim");
			output.writeDouble(185.5);
			output.writeUTF("George");
			output.writeDouble(105.25);
		}

		try ( // Create an input stream for file temp.dat
				DataInputStream input = new DataInputStream(new FileInputStream("/home/danny/temp.dat"));) {
			// Read student test scores from the file
			System.out.println(input.readUTF() + " " + input.readDouble());
			System.out.println(input.readUTF() + " " + input.readDouble());
			System.out.println(input.readUTF() + " " + input.readDouble());
		}
	}
}
```

如果要讨论 DataInputStream 和 DataOutputStream 内部实现可能超出了目前的内容。简单来说，无论是 DataInputStream 还是 DataOutputStream ，其构造函数都传递了一个 FileInputStream 或者 FileOutputStream 对象。研究其源码，发现，构造 DataInputStream 需要传入的是 InputStream 类型， FileInputStream 是 InputStream 的子类，这样做没有什么问题； FileInputStream 提供了读取文件字节的基本能力；DataInputStream 只需要借用 FileInputStream 的基本能力就可以完成对原始数据的基本读写了。

如果为 DataInputStream 的构造函数传递一个网络对象，且这个对象所在的类实现了 InputStream 中的抽象方法，那么 DataInputStream 也可以实现对网络数据的读取；相对的，DataOutputStream 对应的应该是 OutputStream 的实现类。

这样以来 DataInputStream 和 DataOutputStream 和具体如何读写文件就无关了，因为是FileInputStream 和 FileOutputStream 完成具体的文件读写工作。

![Alt text](img/DataStream.drawio.svg)

上图中说明了，其实DataInputStream 和 DataOutputStrem 其实和具体读写什么是无关的，要看构造的时候传进去什么对象。这样是不是就和本章开始讲的网络传输的例子联系起来了。只不过，这里更进一步，有分层实现的概念（这点在网络ISO模型中也有体现）。从另外一个角度来说，上层是服务使用者，下层是服务提供者，是不是和我们日常的组织与管理非常类似？因此面向对象是我们熟知的一种方法，Java只不过是其应用而已。

**注意：这个例子中的写入顺序和读取顺序必须完全一致，否则读取的数据会不正确。想一想为什么？**

### 2.3. 文件复制

文件复制这个例子中使用到了`BufferedInputStream`和`BufferedOutputStream`。者两个类的基本能力是读写字节，只不过内部使用缓冲的机制来降低CPU占用，提升IO操作速度。

关于缓冲机制用一个浇花的例子来解释：

1. 水管放水：输入操作，水是输入的数据；
2. 人，如果CPU，使用水（数据）进行函数处理（浇花）这个动作；
3. 水的最小单位是一杯，对应一个字节。

因为假如CPU（人的处理速度很快），数据（水管）的流速相对于人的处理来说很慢；或者说是相反水管（数据）的流速很快，人（CPU）的处理速度较慢。那么这个任务中防水速度和人的操作速度不匹配就会造成很多问题：要么累死人，要么水撒了一地（数据丢失）。

例如，无缓冲的情况：人很快，水流较慢；人每次接一杯水去浇花（一个字节）。这样人会累死，效率也非常低。

有缓冲的情况：用一个桶装水，桶满了后，人可以一次性的去浇花；然后再等桶接满水。在桶接水的这段时间，人可以休息，或者做其他的事情。这里的桶就是缓冲。

缓冲机制在计算机中大量使用，因为各种任务耗时不同，或者是I/O设备间的速度差异。因此使用缓冲机制可以提升效率，降低CPU的占用，使得CPU有时间去处理其他任务。





![Alt text](img/BufferedSteam.drawio.svg)

```java
import java.io.*;

public class Copy {

	/**
	 * 
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {
		// Check command-line parameter usage
		if (args.length != 2) {
			System.out.println("Usage: java Copy sourceFile targetfile");
			System.exit(1);
		}

		// Check if source file exists
		File sourceFile = new File(args[0]);
		if (!sourceFile.exists()) {
			System.out.println("Source file " + args[0] + " does not exist");
			System.exit(2);
		}

		// Check if target file exists
		File targetFile = new File(args[1]);
		if (targetFile.exists()) {
			System.out.println("Target file " + args[1] + " already exists");
			System.exit(3);
		}

		try (
				// Create an input stream
				BufferedInputStream input = new BufferedInputStream(new FileInputStream(sourceFile));

				// Create an output stream
				BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(targetFile));) {
			// Continuously read a byte from input and write it to output
			int r, numberOfBytesCopied = 0;
			while ((r = input.read()) != -1) {
				output.write((byte) r);
				numberOfBytesCopied++;
			}

			// Display the file size
			System.out.println(numberOfBytesCopied + " bytes copied");
		}
	}
}
```

代码中使用了命令行参数，因此需要在命令行下使用，请参考数组那一章。

1. 编译程序：`javac Copy.java`
2. 运行程序：`java Copy 源文件 目的文件`

和DataInputSteam和DataOutputStream一样，BufferedInputStream 使用了 FileInputStream 完成文件读写的能力；FileOutputStream 使用 FileOutputStream 完成文件写的能力。剩下的就是 BufferedInputStream 读取一个字节在使用 BufferedOutputStream x写到一个文件中。

**注意：这个例子没有使用的是字节的读取和写入，效率比较低，没有充分使用缓冲的能力（使用杯子接水）。其实可以读取和写入字节数组（水桶）来优化效率。缓冲不宜过大，也不宜过小，根据具体的情况来确定。**

### 2.4. 随机文件访问

上面所有例子都是顺序访问，下面来看看Java的随机访问。随机访问可以把文件看成是一个数组，从数组的角度来理解从数组的任意位置读写数据就可以了。与数组不同的是，文件的长度是可以增加的。接下来先看看RadomAccessFile这个类：

![image-20230327094343661](img/image-20230327094343661.png)

RadomAccessFile 实现了DataInput 和 DataOutput的接口，意味着它可以实现原始数据类型的读写。同样，构造 RadomAccessFile 的时候，需要传入一个字符串作为打开的文件位置，或者是一个文件对象。另外还需要一个字符串确定文件的打开方式。

1. 'r'	以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
2. "rw"	打开以便读取和写入。如果该文件尚不存在，则尝试创建该文件。
3. "rws"	打开以便读取和写入，对于 "rw"，还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
4. "rwd"  	打开以便读取和写入，对于 "rw"，还要求对文件内容的每个更新都同步写入到底层存储设备。

目前不用具体关心打开文件的具体含义，只需要知道rw方式是读写方式就可以了。

```java
import java.io.*;

public class TestRandomAccessFile {
	public static void main(String[] args) throws IOException {
		try ( // Create a random access file
				RandomAccessFile inout = new RandomAccessFile("/home/danny/inout.dat", "rw");) {
			// Clear the file to destroy the old contents if exists
			inout.setLength(0);		// 设置文件长度为0 ，如果以前有文件，表示删除文件的所有内容。

			// Write new integers to the file
			for (int i = 0; i < 200; i++)	// 初始文件指针是0
				inout.writeInt(i);			// 写入200个int,一个int占用4个字节；执行完成后文件指针是800

			// Display the current length of the file
			System.out.println("Current file length is " + inout.length());		// 文件大小是800个字节

			// Retrieve the first number
			inout.seek(0); // Move the file pointer to the beginning			// 文件指针移动到0,最开始的位置
			System.out.println("The first number is " + inout.readInt());		// 读取一个int,上述循环写入的值是0

			// Retrieve the second number
			inout.seek(1 * 4); // Move the file pointer to the second number    移动到4,刚刚是下一个整形开始的位置，读出1
			System.out.println("The second number is " + inout.readInt());

			// Retrieve the tenth number
			inout.seek(9 * 4); // Move the file pointer to the tenth number		移动到第十个整形的位置，读出9
			System.out.println("The tenth number is " + inout.readInt());

			// Modify the eleventh number	上一句执行完成后，指针在10*4=40的位置，刚好是第11个整形开始的地方
			inout.writeInt(555);			// 改写第11个整形为555

			// Append a new number
			inout.seek(inout.length()); // Move the file pointer to the end  移动指针到文件末尾，800位置
			inout.writeInt(999);		// 写入一个整形后，指针是804

			// Display the new length
			System.out.println("The new length is " + inout.length());	// 打印长度是 804

			// Retrieve the new eleventh number
			inout.seek(10 * 4); // Move the file pointer to the eleventh number  移动到第11个整形的位置，读取刚刚写入的555
			System.out.println("The eleventh number is " + inout.readInt());
		}
	}
}
```

随机读写最重要的就是文件指针，可以理解成数组的位置，所有的读写操作都从这个位置开始。

1. 读取相应个数字节，文件指针向后一定相应字节；
2. 写入相应个数字节，文件指针向后一定相应字节。

seek用于将文件指针移动到绝对位置。文件指针指向的是当前准备读或者是准备写的位置。

## 3. 本章重点

1. 理解InputStream和OutputStream以及子类间的关系；
2. 掌握FileInputStream和FileOutputStream的基本使用；
3. 理解随机文件访问对象RandomAccessFile的使用；
4. 其他部分是了解。

