在Java程序中,数据的输入输出都是以“流”(Stream)方式进行的。JDK提供了java.io.*包,包中提供了许多接口和类,用于处理不同数据类型的输入输出。
学习FileInputStream流read方法,需要熟悉如下概念:
- java.io包中提供了处理文件的File类,用于对文件进行所有的操作:建立、读取、删除、重命名,建立、删除文件夹等。
- 继承自InputStream抽象类的流,都是向程序中输入数据,且数据的单位是字节(8位),继承自OutputStream抽象类的流,都是程序输出数据,且数据的单位是字节(8位)。
- 继承自Reader抽象类的流,都是向程序中输入数据,且数据的单位是字符(16位),继承自Writer抽象类的流,都是程序输出数据,且数据的单位是字符(16位)。
- 节点流直接连接数据源,它可以从一个特定的数据源(节点)读写数据。例如文件、内存等。常用的节点流有FileInputStream、FileOutputStream、FileReader、FileWriter等。
FileInputStream主要用来处理字节文件,它们是InputStream的子类,实现了父类的抽象方法。FileInputStreanm用来读取字节文件,通过打开一个到实际文件的连接来创建一个FileInputStrem类对象,构造方法如下:
public FileInputStream (String name) throw FileNotFoundException //name :文件的路径
public FileInputStream (File file) throw FileNotFoundException // 通过File类创建的文件的对象
下面是通过一段代码较深入地学习FileInputStream的read()方法。
import java.io.*;
public class FileInputStreamTest {
public static void main(String[] args){
FileInputStream fis = null; // 使用FileInputStream类,声明文件读取对象,创建变量fis
File f = null; // 使用File类,声明一个文件对象,创建变量f
try{
// 通过File类的构造方法指定文件的路径
f = new File(System.getProperty("user.dir"),"/src/SystemTest.java");
// 通过FileInputStream的构造函数中传入指定的文件路径对象f,文件字节流对象fis将直接连接到数据源(文件SystemTest.java)
fis = new FileInputStream(f);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
int end = 0; // 获取读取的字节
byte[] by = new byte[(int)f.length()]; // 创建读取内容的字节数组变量by,大小为所读取文件的长度
System.out.println("----按字节读取");
/*
通过文件字节输入流FileInputStream类提供的read(byte[] by)方法,读取文件SystemTest.java中的f.length个字节的内容,
存放在字节数据by中,再创建指定缓冲区内容的字符串s,然后以字符串的形式输出到控制台。
调用read方法可能抛出IOException异常,需要在程序中捕获进行处理。最后关闭文件时,也可能抛出IOException异常,也须进行捕获处理。
*/
try{
while ((end=fis.read(by)) != -1) { // 读取f.length个字节,放入到字节数组by中
String s = new String(by); // 创建有字节数组by指定缓冲区内容的字符串s
System.out.println(s);
System.out.println("一共读取了:" + end + "个字节。");
}
}catch (IOException e) {
System.out.println("读取数据错误。");
System.exit(-1);
}
System.out.println("----读取数据结束----");
if (fis != null) {
try{
fis.close();
}catch (IOException e){
System.out.println("文件关闭异常");
System.exit(-1);
}
}
}
}
需要深入思考及了解的是:
(1)上面代码中数组变量by的长度指定了是文件的长度(f.lenght()),为何还有while循环判断d的意义是什么?有必要吗?
在上面的代码中,虽然by
数组的大小被初始化为文件的长度,但是FileInputStream
的read(byte[] b)
方法并不保证一次性读取整个文件。实际上,它尝试读取一定数量的字节到数组中,但具体读取的字节数取决于底层操作系统的缓冲区大小以及文件系统中可用的数据。
因此,while
循环的目的是确保从FileInputStream
中读取所有的数据,直到没有更多的数据可读(即read()
方法返回-1
)。
这里有几个关键点:
- 按块读取:
FileInputStream
通常不会一次性将整个文件加载到内存中,特别是当文件很大时。相反,它会分块读取数据,每次读取一部分。这是为了效率和资源管理的考虑。 - 返回读取的字节数:
read(byte[] b)
方法返回实际读取的字节数,这可以少于请求的字节数(即b.length
)。当到达文件的末尾时,它会返回-1
。 - 文件长度与实际读取:虽然数组
by
的大小是根据文件长度设置的,但read()
方法并不保证一次性填充整个数组。实际上,如果文件很大,或者由于某些其他原因(如磁盘I/O、网络延迟等),可能需要多次调用read()
方法来读取整个文件。
因此,while
循环是必要的,以确保即使文件很大或读取过程中出现问题,代码也能正确读取整个文件的内容。如果没有这个循环,代码可能只会读取文件的一部分,或者在第一次read()
调用后停止,即使还有更多数据需要读取。
(2)上述代码,如果读取的文件比较大,一次性没有读取完,那么while循环体内代码是不是会循环执行多次?
如果读取的文件比较大,无法一次性读取完整,while
循环体内的代码会循环执行多次。这是因为FileInputStream
的read(byte[] b)
方法会分批次读取文件内容,每次读取的字节数取决于文件系统中可用的数据量和底层操作系统的I/O操作。
在每次循环中,fis.read(by)
会尝试读取更多的字节到by
数组中,并返回实际读取的字节数。这个返回值被赋给end
变量,并且如果这个返回值不是-1
(表示文件末尾),循环就会继续执行。
循环体内的代码:
String s = new String(by); // 创建有字节数组by指定缓冲区内容的字符串s
System.out.println(s);
System.out.println("一共读取了:" + end + "个字节。");
将会多次执行,每次处理上一次调用read()
方法读取到的字节。这里有一个需要注意的地方:
String s = new String(by);
这行代码将整个by
数组转换为一个字符串。但是,由于read()
方法可能并没有填充整个数组(尤其是在循环的最后一次迭代时),直接使用整个数组来创建字符串可能会导致字符串包含未初始化的字节(通常是0值),这可能会导致输出包含不可见或不可预测的字符。
更正确的做法是只处理实际读取到的字节。这可以通过创建一个新的字符串,其只包含by
数组中前end
个元素来实现:
String s = new String(by, 0, end);
此外,如果文件非常大,将整个文件内容转换为字符串可能会导致内存溢出。在实际应用中,处理大文件时通常会采用流式处理,即一次处理文件的一小部分,而不是试图一次性将整个文件加载到内存中。
所以,如果文件比较大,上述代码中的while
循环会循环多次,每次处理文件的一部分,直到整个文件被读取完毕或遇到I/O错误为止。
(3)假如读取的文件非常大,while循环体内代码将多次执行,关于while 循环体内创建的字符串对象变量s会怎样哪?
在while
循环体内,每次迭代都会创建一个新的字符串对象s
。这是因为每次循环时都会执行String s = new String(by, 0, end);
每次调用new String(by, 0, end);
都会在内存中分配一个新的字符串对象,并用by
数组中前end
个字节的内容来初始化它。因此,每次循环都会有一个新的s
对象,它与之前循环中创建的s
对象是不同的。
在循环结束后,前一个s
对象(以及它在堆上占用的内存)将不再被引用,因此它可能会成为垃圾回收的目标(具体取决于Java的垃圾回收机制和当前的内存状态)。但是,重要的是要注意,如果文件非常大,并且循环次数非常多,这可能会导致大量的字符串对象被创建和销毁,这可能对垃圾回收器造成压力,并可能影响程序的性能。