You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

18 KiB

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

在上图中,Side_ASide_B分别代表通讯系统的两个端点,如果这两个端点要实现通讯,最简单的方式需要实现两个函数:

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

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

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

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

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

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因此可以沿用上面网络的例子。只不过这是一个真实的

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

image-20230326142026039

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

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

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

1.2.1. InputStream

image-20230326144650268

注意:read()函数后面的返回标注的是int其实返回的值是0255。因为java没有unsigned类型byte类型的取值范围达不到0255所以使用int作为返回。

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

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

1.2.2. OutputStream

image-20230326151620467

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

2. 文件流的操作

2.1. FileInputStream/FileOutputStream

image-20230326151754475

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

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

在学习DataInputStreamDataOutputStream 前,我们先来看看其直接超类:FilterInputStreamFilterOutputStream

image-20230326210655128

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.

这个类很奇怪,书上说如果需要做不同数据类型的读写,需要该类型。其实情况要复杂得多,有兴趣可以研究其源代码。奇怪的是,这个类型是非抽象类型,但是却不能使用 new 方法创建(看源码可以分析出来)。

关于这个类其实要讲的话会很多,现在大家知道是 InputStream 和 OutputStream 的超类就可以了。

再来看看InputStream 和 OutputStream这两个类是可以实例化的并可以读写文件的各种基本类型原始类型

image-20230326154038645

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

image-20230326222237336

image-20230326222303342

现在看看例子:

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

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

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

2.3. 文件复制

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

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

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

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

例如,无缓冲的情况:人很快,水流较慢;人每次接一杯水去浇花(一个字节)。这样人会累死,效率也非常低。

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

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

Alt text

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

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

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

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

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. 理解ImputStream和OutStrem以及子类间的关系
  2. 掌握FileInputStream和FileOutputStream的基本使用
  3. 理解随即文件访问对象RandomAccessFile的使用
  4. 其他部分是了解。