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