前言
在这里对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
// 在这里首先第一次会把数组填满 后面第二次循环文件内没字节了就返回-1
break;
}
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内容 只需在构造对象时将第二个参数设为true
outputStream.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());
}
}
}