文章目录
- 前言
- 一、字节流
- 1.1 读文件
- 1.2 写文件
- 二、字符流
- 2.1 读文件
- 2.2 写文件
- 三、文件IO三道例题
前言
在这里对Java标准库中对文件内容的操作进行总结,总体上分为两部分,字节流和字符流,就是以字节为单位读取文件和以字符为单位读取文件内容。
一、字节流
1.1 读文件
字节流在Java中的类就是InputStream,他是一个抽象类,我们在这里操作的文件所以就需要通过它的子类FileInputStream向上转型的方式,因为InputStream不能初始化。后面如果我们要进行网络IO,就也是使用相对应的子类来进行实现。
代码示例1:
package io;import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;public class Demo6 {public static void main(String[] args) throws IOException {
// 读文件的两种不同参数的read方法的使用
// InputStream inputStream = new FileInputStream("F:/test.txt");
// while (true) {
// int b = inputStream.read();
// if (b == -1) {
// break;
// }
// System.out.printf("0x%x ", b);
// }
// System.out.println();// while (true) {
// byte[] arrB = new byte[1024];
// int n = inputStream.read(arrB);
// System.out.println("n = " + n);
// if(n==-1) {
// //读毕 n就会返回-1
// // 在这里首先第一次会把数组填满 后面第二次循环文件内没字节了就返回-1
// break;
// }
// for (int i = 0; i < n; i++) {
// System.out.printf("0x%x ", arrB[i]);
//
// }
// System.out.println();
// }
//
// } }}
这段代码是使用字节流来读取文件内容的一个简单过程,因为我们读取硬盘中的文件内容给内存中的变量去接受,显然数据是往cpu的方向流动的,因此是一个输入的过程,我们使用InputStream抽象类以及FileInputStream类进行向上转型来构造这样的一个字节流对象。我们想要读取哪个文件中的信息就可以将文件的路径或者File类对象用以初始化字节流对象,如下。
InputStream inputStream = new FileInputStream("F:/test.txt");
FileInputStream这样的字节流对象提供了一些读取文件数据的方法如下图。
这三种方法有些许的不同,首先read方法,它每次读取一个字节,然后返回值就是就是这个字节的相应的ASCII值,如果说读到文件末尾那么此时就会返回-1。那么既然每次都是读一个字节,返回的就是一个字节的范围,那么返回值类型直接设为byte即可,那么为什么会设为int类型的返回呢?
有以下几点原因:
(1)确保每次返回的数都是正数,因为从原则上说字节这样的概念本身是无符号的但是byte类型本身是有符号的,如果说你拿byte来返回无符号数,那么范围是0~255,此时就无法表示-1。只有你使用int类型,才能够返回值是正数,还能使用-1来表示文件结尾。
(2)这时我们又会想到,那么我们直接用short不就行了,这样也就可以包含-1以及0到255的整数。但是这里就涉及到计算机发展的问题了,因为计算机发展到现在存储空间不再是核心矛盾了,存储设备的成本是越来越便宜了,此时随着cpu越来越牛,它单次处理数据的长度也越来越长。对于32位cpu,一次就能够处理四个字节的数据,此时要是使用short还要将其转成int再按int进行处理,显然64位cpu也是类似的,此时的short就更没意义了,我们学过的c语言中的整形提升也是类似的道理。因此在我们使用short的场景换成int,在我们使用float的场景我们换成double。
这里补充一下,为什么说字节数据从原则上是无符号的,因为字节数据不是用来进行算数运算的,例如一张图片就是由很多字节数据构成的,如果对其字节进行加一或者减一操作,那么这张图像很可能直接崩掉
第二个read方法和第一个read方法不同的点在于它的参数是一个字节数组,这个数组是一个空数组,读文件数据的时候就会把读到的数据全放到这个数组当中,去把这个字节数组给填满,能填多少填多少。返回值也是类似,你读到多少个字节就返回多少,读到文件尾就返回-1。
第三个read方法其实和第二个read方法就很类似了,也是建立一个空数组来作为参数,然后指定一个区间,读到的文件数据只放到这个空数组的对应区间当中。返回值就和第二个read方法一样了,读到多少字节就返回多少,读到文件尾就返回-1。
这里提一嘴,read()和read(byte[] b)这两种方法谁的效率比较高?
事实上是第二个方法的效率高,我们都知道第一个方法是一次读取一个字节,第二个方法是一次读一个数组的字节,对于固定的文件内容,肯定是第二个方法读取的次数比较少,文件的IO在我们的代码中是一个比较低效的操作,每次读取都要进行一次IO,显然IO次数少的方法效率就更高。
此时我们把视角转回上面的代码示例1,我们分别使用方法1和方法2来读取文件数据,然后在外面套一个死循环,当read方法返回-1代表读到文件尾此时跳出循环,逻辑还是比较简单的。
代码示例2:
package io;import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;public class Demo6 {public static void main(String[] args) throws IOException {// 但是每当建立一个线程或进程 建立的PCB中的文件描述符表这个顺序表它的长度是有限的// 每当打开一个文件 它的长度就要加一、// 所以打开文件后要及时关闭// 所以这里联想到处理unlock()的方法 即使用try finally语句
// InputStream inputStream = new FileInputStream("F:/test.txt");
// try {
// while (true) {
// byte[] arrB = new byte[1024];
// int n = inputStream.read(arrB);
// System.out.println("n = " + n);
// if (n == -1) {
// //读毕 n就会返回-1
// // 在这里首先第一次会把数组填满 后面第二次循环文件内没字节了就返回-1
// break;
// }
// for (int i = 0; i < n; i++) {
// System.out.printf("0x%x ", arrB[i]);
// }
// System.out.println();
// }
// } finally {
// inputStream.close();
// }}}}
上述代码相对于代码示例1加入了try-finally这样的代码,因为如同代码中注释所描述的每次你打开一个文件,就会在PCB当中的文件描述符表当中添加一个元素,这个元素当中就是文件的相关信息,文件描述符表类似于一个顺序表,它总会有上限当这样不关闭文件的操作多了,会占满文件描述符表,此时若是再想打开文件就不行了。因此每次我们使用FileInputStream构造流对象打开文件读取文件内容等一系列操作之后就要关闭文件,使用try-finally就可以保证每次使用完文件之后能够关闭文件的流对象,此时文件描述符表中的对应元素就会被释放。
代码示例3:
package io;import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;public class Demo6 {public static void main(String[] args) throws IOException {// 使用try finally确实可以 但是作为一个程序员要追求去写优雅的代码// 上述代码修改如下// 直接将流对象的创建放到try右边的括号中 这样java会自动帮你调用close()// 注意不是什么对象都可以这样 必须要实现Closeable接口的类才可以try (InputStream inputStream = new FileInputStream("F:/test.txt");){while (true) {byte[] arrB = new byte[1024];int n = inputStream.read(arrB);System.out.println("n = " + n);if (n == -1) {//读毕 n就会返回-1// 在这里首先第一次会把数组填满 后面第二次循环文件内没字节了就返回-1break;}for (int i = 0; i < n; i++) {System.out.printf("0x%x ", arrB[i]);}System.out.println();}}}}
代码示例2我们解决了关闭文件流对象释放文件描述符表中元素的问题,但是使用try-catch好像不够优雅,于是我们将InputStream流对象构造的这条语句放入try,这样java会在文件操作完成之后自动给我们的流对象调用close方法,但是要注意的一点就是这种编写方式的前提是放入try后的括号的对象对应的类必须实现了Closeable接口。
另外在我们使用FileInputStream类对象时可以配合Scanner对象来进行使用,代码示例如下:
package io;import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;public class Demo10 {public static void main(String[] args) {try (InputStream inputStream = new FileInputStream("F:/test.txt")) {Scanner sc = new Scanner(inputStream);// 本来这里要在控制台输入 但是现在直接将文件中的数据读走了sc.next();} catch (IOException e) {throw new RuntimeException(e);}}}
以前我们使Scanner对象都是在终端输入数据,这样写代码就是将我们从终端输入的数据换成了文件中的数据。另外能这么写的原因看下图Scanner类的构造函数的参数就不难理解了,本来就是InputStream类型的,另外我们在之前写入Scanner对象的System.in也是InputStream类型的。
1.2 写文件
写文件的流程和读文件的流程类似的,需要使用OutputStream抽象类以及其子类FileOutputStream。写文件也要通过FileOutputStream对象的write方法来实现,FileOutputStream对象的write也是要分为三个版本,如下图。
和前面的read参数很类似,第一个版本就是一次在文件中写入一个字节,写入的字节由b指定。第二个版本就是一次写入文件一整个数组,第三个版本也是往文件中写入一个数组,只是只写入数组在区间内的部分。
代码示例如下:
package io;import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;public class Demo7 {public static void main(String[] args) {try (OutputStream outputStream = new FileOutputStream("F:/test.txt",true)) {// 注意这里打开文件会发现文件内原本的内容被清空 里面全是新添加的内容// 这里的清空操作是打开文件时清空的,即构造对象时清空的// 要是想在文件内追加即append内容 只需在构造对象时将第二个参数设为trueoutputStream.write(97);outputStream.write(98);outputStream.write(98);outputStream.write(99);} catch (IOException e) {throw new RuntimeException(e);}}}
这里写入文件的代码需要注意一点就是构造FileOutputStream对象时会直接将对应路径上的文件内容给清空,如果想要实现写入文件是一种append的效果,就要在构造流对象时将第二个参数设为true。示例代码中的注释也说明了。
二、字符流
使用字符流和字节流操作文件内容基本的流程是类似的,但是字符流读取和写入文件内容的基本单位是字符,字节流是字节。
2.1 读文件
使用字符流读文件过程和使用字节流读文件的流程是相似的,只有很少的差别。
代码示例如下:
package io;import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;public class Demo8 {public static void main(String[] args) {try (Reader reader = new FileReader("./testDir/test2.txt")) {// 读入字符的时候要思考一个问题// 文件中的字符编码集用的是utf8 一个中文字是三个字节// java中的一个中文字是两个字节// 为什么java中的char还能接收并且打印中文字// 原因:因为在java中的char类型在接收字符时会自动将utf8转换为unicode编码集// 实际上java中很多类型在接收数据时都会进行字符集的转换while (true) {int n = reader.read();if(n==-1) {break;}System.out.printf("%c ",n);}} catch (IOException e) {throw new RuntimeException(e);}}}
如以上的示例代码,与字节流的FileinputStream不同这里读文件内容使用的是FileReader类,过程还是类似的,外面套个循环,每次读取一个字符,直到读取操作返回值为-1代表文件读完此时跳出循环。
但是这里会有一个疑问,在windows下文件中的字符是采用utf8的编码集,在这个编码集当中单个汉字的字节数是三个,为什么在java中还能使用char类型接收汉字并且打印,char类型在java中只有两个字节。这个问题的答案就是说java中读取到文件中内容时会自动转换编码集,对于char类型java中使用的时unicode编码集,读到的文件中汉字会自动转为char类型,也就会经过utf8到unicode这个过程,在unicode当中汉字占两个字节。
在java中不同的类型实际上用的编码集都是不同的,比如说String类型内部是使用utf8,char类型使用的是unicode。使用String类型变量保存你好就需要6个字节,当使用字符串s.charAt()这种方法将字符赋给char类型变量时,就会将编码集从utf8转为unicode,此时一个字就从三个字节转为了两个字节。
2.2 写文件
流程都是类似的,写文件的操作主要是通过Writer类中的write方法来实现的。
代码示例如下:
package io;import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;public class Demo9 {public static void main(String[] args) {try (Writer writer = new FileWriter("F:/test.txt")) {// 字符流写文件和字节流一样 都是要加上一个true这个参数才能实现append这样的效果writer.write("你好世界");} catch (IOException e) {throw new RuntimeException(e);}}
}
和字节流的write一样,要想在写文件时实现append的效果就需要在构建FileWriter对象时将第二个参数设为true,否则写文件时都会先清空路径上的文件内容。
三、文件IO三道例题
(1)扫描指定目录,并找到名称中包含指定字符的所有普通文件(包含目录)。
package io;import java.io.File;
import java.util.Scanner;public class Demo11 {public static void main(String[] args) {Scanner sc = new Scanner(System.in);System.out.println("输入要查找文件的目录:");String dirPath = sc.nextLine();System.out.println("输入要查找文件的关键词:");String keyWord = sc.nextLine();// 根据输入的路径构造file对象File dir = new File(dirPath);// 如果输入的路径不是目录则直接返回并报错if (!dir.isDirectory()) {System.out.println("输入路径非法!");return;}// 关键词和目录都是正确的那么就开始查找文件searchFile(dir, keyWord);}private static void searchFile(File dir, String keyWord) {// 将目录下的所有文件输出成数组File[] files = dir.listFiles();// 如果数组为空直接返回 这也是递归结束的条件if (files == null) {return;}// 遍历数组for (File file : files) {// 如果文件是文件 那么判断是否包含关键词if (file.isFile()) {// 包含则匹配成功if (file.getName().contains(keyWord)) {System.out.println("匹配成功:" + file.getAbsoluteFile());}// 如果是目录则进行递归 在下一级目录进行查找} else if (file.isDirectory()) {searchFile(file, keyWord);}}}
}
(2)复制文件,输入一个路径,表示要被复制的文件,输入另一个路径,表示要复制到的目标路径。
package io;import java.io.*;
import java.util.Scanner;public class Demo12 {public static void main(String[] args) throws IOException {Scanner sc = new Scanner(System.in);System.out.println("输入要复制的文件路径:");String srcPath = sc.nextLine();System.out.println("输入要复制到的文件路径");String destPath = sc.nextLine();File fileSrc = new File(srcPath);File fileDest = new File(destPath);if (!fileSrc.isFile()) {System.out.println("输入的要复制的文件路径不合法!");return;}if (!fileDest.getParentFile().isDirectory()) {System.out.println("输入的复制到的文件路径不合法!");return;}byte[] bytes = new byte[1024];// OutPutStream会自动创建文件try (InputStream inputStream = new FileInputStream(fileSrc);OutputStream outputStream = new FileOutputStream(fileDest)) {while (true) {int n = inputStream.read(bytes);if (n == -1) {break;}outputStream.write(bytes, 0, n);}} catch (IOException e) {throw new RuntimeException(e);}}
}
(3)输入一个路径再输入一个查询词,搜索这个路径中文件内容包含这个查询次的文件。
package io;import java.io.*;
import java.util.Scanner;public class Demo13 {public static void main(String[] args) {Scanner sc = new Scanner(System.in);System.out.println("请输入目标目录:");String dirPath = sc.nextLine();System.out.println("请输入要匹配的关键词:");String keyWord = sc.nextLine();File dirFile = new File(dirPath);if (!dirFile.isDirectory()) {System.out.println("输入路径不合法!!");return;}search(dirFile, keyWord);}private static void search(File dirFile, String keyWord) {File[] files = dirFile.listFiles();if (files == null) {return;}for (File f :files) {if (f.isFile()) {match(f, keyWord);} else if (f.isDirectory()) {search(f, keyWord);}}}private static void match(File f, String keyWord) {StringBuilder stringBuilder = new StringBuilder();try (Reader reader = new FileReader(f)) {while (true) {int b = reader.read();if (b == -1) {break;}stringBuilder.append((char) b);}} catch (IOException e) {throw new RuntimeException(e);}if (stringBuilder.indexOf(keyWord) >= 0) {System.out.println("匹配成功:" + f.getAbsolutePath());}}}