文件与 IO
流
1.I/O流是什么?
对于陌生的东西,最好通过类比的方法去了解。我对于流的理解:流就是一系列数据。通过生活中常见的现象进行类比,比如:流中全部是水,我们称为“水流”,流中全是电子,我们称为“电流”。那我们可以根据流中的数据类型,来给流命名。再回到计算中,流中是字节的我们叫“字节流”,是字符的叫“字符流”,等等。
对于笔者来说,在Java开发中需要用到获取文件信息,经常需要用到流。那个时候不理解流,只知道抄着代码,这里改改那里修修。以至于,每当需要操作文件的时候不知道如何开始下手,对于整个流程不清楚。每当去百度文件操作的时候,又发现很多种实现方法,用的流都不一样,实现起来也有细微差别,搞得笔者也是心态爆炸,于是下决心把这块内容梳理好,方便以后使用。
关于这部分内容目前存在以下几个问题:
- 如何理解文件在流中的角色?
- 文件操作的整个流程?
- 文件操作选用什么流?
- 不同的流的差异与特点
**流的一些补充:**上面粗略解释了流,现在需要对流进行一个补充。我们说的电流和水流都是从一端流向另一端(可能并不是很准确),比如:电池的电子从负极流向正极,电池的负极像一个电子的生产者一样,而正极就像一个消费者或者接收者一般;还有地下水到你家的水龙头一样,地下水通过加工提取到人引用。通过这两个例子,可以得出一个结论,流的操作或者使用至少需要一个生产对象和一个接受对象。在计算机世界中已经为这两个对象命名,输入对象和输出对象。
2.流的分类
关于流的分类,在Java中有如下3种分类标准:
- 按照流的流向分,可以分为输入流和输出流。
- 按照操作单元划分,可以分为字节流和字符流。
- 按照流的角色划分,可以分为节点流和处理流
流的分类解读:
- 根据流向分类:在上面的内容补充中,说流都是从一端流向另一端,说明了流有方向。所以根据流的方向可以分为输入流和输出流,辨别输入流和输出流需要选参照中心。我们以程序作为参照中心,读取本地磁盘上的文件,程序端接收,就说明该是输入流;将程序产生的数据保存到磁盘,程序段输出,就说明是输出流。在Java中所有的IO流的类都是从以下4个抽象类基类中派生出来的:InputStream/Reader所有输入流的基类,前者是字节输入流,后者是字符输入流;OutputStream/Writer所有输出流的基类,前者是字节输出流,后者是字符输出流
- 根据操作单元划分:首先需要理解什么是操作单元,我把他理解为所操作数据的最小单位。我们知道一个字符等于两个字节,字节流取数据按照一个一个字节取,字符流取数据就是一个字符一个字符取。就好比,我们用调羹喝汤,一调羹的量就好比字节,而用勺子喝汤,一勺子的量就好比字符,只不过一勺子比一调羹的量多很多,而字符是一个字节的两倍。
- 根据流的角色划分:节点流和处理流。节点流:可以从一个特定的数据源(称作节点)读写数据,如:内存,硬盘,文件等。处理流:可以理解为,根据实际需求在节点流的基础上进行的二次开发和封装。
例子1:说明处理流和节点流的关系:想象一个场景,计算机每一次读写的时候都访问硬盘,如果访问次数很频繁的话,性能表现就不佳,而且硬盘的寿命也会减少。所以为了满足这种场景,就设计了缓存流,一次读取较多数据到缓存中,以后的每一次读取都从缓存中访问,直到缓存中的数据读取完毕。用一个生活的例子解释缓存流,吃饭的时候我们不用碗装饭(缓存流)的话,每吃一口都要去电饭锅里面铲,有了碗(缓存流)之后,我们可以把饭装进碗里,吃完了碗里的,我们再去电饭锅里铲。这样就大大减少了去电饭锅里装饭的次数,提高了效率。
例子2:因为我们的流的操作最小单位是字节或者字符,为了方便Java程序员使用对象,我们就需要在原来节点流的基础上进行二次开发和封装。比如我们读取一个本地的文本文件,需要一个字节流,又由于流里面是字节,而我们希望方便程序员使用对象来保存这些数据,就在该流的基础上进行开发(具体如何实现,本人暂时也不清楚),原来的字节流就变成了一个对象流,流里面的内容不在是字节,而是对象,这样就方便了程序员对于该信息的获取。
以下是关于字节流和字符流,以及节点流和处理流的详细分类图
3.关于字符流和字节流的选择
之前在学校做课设的时候,每次做到文件关于文件读取的时候,关于选择字节流还是字符流的选择都是含糊不清,每次都是去网上找代码,这部分知识不是很清楚。现在重新把知识捡起来好好梳理一遍。首先不管是文件读写还是网络发送接收,信息的最小存储的单位是字节。由此可能读者又会问了,既然最小存储单元是字节,那么有了字节流为什么还要字符流呢?
**解答:**在编程领域中,由于语言的不同,中文等一些其他语言一个字就需要两个字节表示,即一个字符,所以如果不使用字符流的话,就会出现乱码等问题。在日常使用中遇到图片,音频等媒体文件采用字节流比较好,而涉及到字符操作的使用字符流比较好。
使用文件对象
说明: 一个File对象就是一个具体的文件或者文件夹目录(作用:指定文件或文件夹保存的路径)。让然,指定的文件或文件夹目录在电脑上不一定存在。以下代码演示了文件对象的用法以及常用的方法。
1 |
|
流与文件
在做课设的时候经常会遇到文件的读取等功能,在还没弄清楚流的时候就已经强迫使用文件操作,导致每次遇到文件读取问题都是网上copy类似代码,却不能很好的理解代码执行流程,或者理解了也很快就忘记了,因为这个记忆点并不深。所以现在打算简单的梳理下文件的读取操作。
开始之前,我们需要思考下几个问题:1.先要搞清楚”流向”,我们把我们的程序作为参照物,我们的程序是要读取文件还是写入文件?2.是哪个文件?(即文件所在的位置)3.根据文件的类型,我们该选用什么类型的流?通过思考这几个问题,使得我们对于文件的读取流程就会很清晰了。
假设我们程序需要读取D盘根目录下的
zzf.txt
文本文件,通过使用File构建一个对象file,并告诉我们的程序文件位于D:\\zzf.txt
1
File file = new File("D:\\zzf.txt");
告诉程序文件的位置之后,我们就需要构建一个流了。让文件数据通过流,流向程序。构建流的时候我们又需要考虑了,是输入流还是输出流,显然我们在流的章节中说道,数据是流向程序,所以我们要选用输入流。选用输入流之后还要思考,是要字节流还是字符流呢?这个问题我们要确定文件类型,显然我们这里是文本类型的,对于文本前面也提到了最好使用字符流,防止文本里面有中文字符。所以综合来说我们选用字符输入流
1
2
3
4
5
6
7
8
9
10
11
12
13/*
*try后面加括号是Java7中的技术,这样的好处是:把流定义在try()里,try,catch或者finally结束的时候,会自
*动关闭流。这样就不要像以前一样,需要自己写代码关闭了。
* 1.首先我们要使用上面生成的文件对象file,用来构建一个输入字符流fis。
*/
try (FileReader fis = new FileReader(file)) {
//2.因为我们选用的是字符流,所以我们新建一个字符数组用来保存流中的数据,数组长度就是文件的长度
char[] content = new char[(int) file.length()];
//3.使用read方法把流中的数据全部保存到content字符数组中。
fis.read(content);
} catch (IOException e) {
e.printStackTrace();
}通过上面的步骤,我们就完成了把D盘目录下的
zzf.txt
文件内容保存到了content
数组中,接下来我们可以 对这个数组进行很多操作,比如将数据写入一个txt文件等等。继续完善我们的操作,我们把刚才读取的文件数据重新写入到一个新的文件中。根据步骤2中关于选择流的思考,这里我们同样需要思考,流的选择。在了解步骤2的基础下,我们可以快速的得出,我们是需要一个字符输出流
1
2
3
4
5
6
7
8
9
10
11
12
13File file = new File("D:\\zzf.txt");
//这里我们告诉程序,我们的文件是D盘根目录下的zz.txt文件
File file1 = new File("D:\\zz.txt");
//通过file1对象构建字符输出流
try (FileReader fis = new FileReader(file);FileWriter fileWriter = new FileWriter(file1)) {
char[] content = new char[(int) file.length()];
fis.read(content);
//将从zzf.txt文件中读取保存的数据content,写入到新文件zz.txt中
fileWriter.write(content);
} catch (IOException e) {
e.printStackTrace();
}这样我们就简单的实现了一个文件的读取。有人或许会问,你前面说的节点流和处理流你咋没 用到呢?其实回去看第一部分,再仔细分析的话我们这个字符输入输出流就是一个节点流,所 以我们可以把效率提高一点,在这个基础上我们使用缓存流。
加上缓存流,提高效率。前面也说了处理流是在节点流上进行二次开发和封装的,所以用到处理流肯定就需要节点流。就像通过文件对象构建流一样,处理流通过节点流来进行构建。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18File file = new File("D:\\zzf.txt");
File file1 = new File("D:\\zz.txt");
try (FileReader fis = new FileReader(file);FileWriter fileWriter = new FileWriter(file1)) {
char[] content = new char[(int) file.length()];
//使用节点流FileReader对象构建处理流BufferedReader
BufferedReader bufferedReader = new BufferedReader(fis);
//读取文件数据
bufferedReader.read(content);
//使用节点流FileWriter对象构建处理流BufferedWriter
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
//写入文件数据
bufferedWriter.write(content);
//有的时候,需要立即把数据写入到文件,而不是等缓存满了才写出去,这时候就需要用到flush
bufferedWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}注意事项:
- 我们使用流的时候一般都是需要手动关闭的,上面的代码都没有写出关闭的代码,是因为我们把流的构建这一步写进了
try()
里面,所以不需要手动关闭代码,自己写的话,要么和我代码一样,要么就在流使用完之后就写一行代码关闭流。 - 还有一个大家可能没有考虑到的情况,我们刚才使用了缓冲流之后,如果手动关闭的话是不是需要关闭两个流呢?实际上,我们只需要关闭节点流就行了,因为处理流是在节点流的基础上构建的,我们关闭了节点流,处理流就失去了节点流,就会自动销毁。
- 最后说一个关于缓存流写入的问题,因为缓存流是有缓存空间的,只有缓存空间满了,缓存空间的数据才会写入到文件中。通过查看源代码发现默认设置的缓存空间是8kb。因此,当你使用缓存流读取文件的时候,如果文件大小小于8kb,没有使用flush方法的话,要写入的文件就没有数据,因为数据都在缓存区中。我们使用flush方法就可以将缓存区的数据强制写入到文件中,无论缓冲区的数据大小。
- 我们使用流的时候一般都是需要手动关闭的,上面的代码都没有写出关闭的代码,是因为我们把流的构建这一步写进了