一.介绍文件
1.1狭义文件和广义文件
- 狭义上的文件:存储在硬盘上的文件
- 广义 上的文件:操作系统用来管理资源的方式。不管是软件还是硬件都抽象成文件,这样方便管理(一切皆文件)。
本章讲解狭义文件。
1.2文件分类
大体来说文件分为:"文件夹" 和**"普通文件"。**
但**"文件夹"** 是老百姓通俗叫法,真正的专业术语叫**"目录"(directory)。**
从开发角度,把文件分为二进制文件 和文本文件。
- 这两种文件很好区分,我们用记事本打开文件,发现没一点规律的就是"二进制文件"无疑了,因为二进制文件本来就不是给人看的,是给机器看的。典型的二进制文件就是图片(视频等):(出现中文等字符,是记事本尝试按字符集编码二进制文件,而导致的乱码)

- 使用记事本打开的文件内容是有规律的,人看的懂的就是文本文件,(Windows系统中的文件类型默认与文件后缀相匹配,在如Linux、Unix等操作系统上就没这种习惯)常见的如以.txt结尾的文件:(.json .md .xml 等)

(如.docx结尾的文件(word文件)是**富文本;**富文本指的是,文件中可以包含文字、音频、图片和各种格式信息。)(很好理解,不做过多介绍)
1.3文件路径分类
文件路径主要分为两大类:绝对路径 和相对路径。
操作系统中文件的创建和查找就是在一颗N叉树中进行的;目录下有N个文件(包括目录),这N个文件目录下又包含N个文件,形如下图:
绝对路径: 从根目录开始,经过的所有目录名组合起来就叫绝对路径,如 C:\Windows\appcompat (路径不包括"此电脑"!)。
相对路径 的路径不是以根目录为基准了,是以当前所在目录为基准,例如下图,我现在所在的位置是红圈所在的文件,现在我要选择使用旁边的绿色圈中的文件,但如果使用绝对路径从头开始查找文件就太麻烦了,直接使用相对路径--》../appcompat 就代表绿圈文件了。(.. 代表所在文件的父目录路径,. 代表目前文件路径)

二.操作文件
Java标准库提供了一些列类操作文件,这些类都是围绕下面2.1或者2.2操作。
2.1文件系统操作
File类用于文件系统操作。
2.1.1File类常见的方法和属性
1.构造函数
需要注意的是,创建了File对象并不是在指定路径下就创立了一个文件,这只是根据路径参数抽象了一个对象,想在本地真正创建文件要使用下面的createNewFile()。
|----------------------------------|------------------------|
| 方法签名 | 说明 |
| File(File parent,String child) | 根据父目录+孩子文件名,创建一个File对象 |
| File(String pathname) | 直接根据路径,创建File对象 |
| File(String parent,String child) | 根据父目录+孩子文件名,创建File对象 |
java
// 创建文件对象
File parentDir = new File("/home/user/documents");
// 使用构造函数创建文件对象
File childFile = new File(parentDir, "example.txt");
// 或者更简洁的写法
File childFile2 = new File(new File("/home/user/documents"), "example.txt");
2.属性
|---------------|-----------|-------------------------------------------------------------------------|
| 修饰符及类型 | 属性 | 说明 |
| static String | separator | 依赖于系统的路径分隔符,String类型表示。 在Windows系统上pathSeparator是**\****,其他系统就是/** |
| static char | separator | 依赖于系统的路径分隔符,char类型表示 |
大多操作系统的文件路径使用**/(斜杠)分隔(如Linux、macOS等),但Windows中使用\**(反斜杠)进行分隔。在Windows的IDEA中使用两种都可以,就算一个字符串路径同时使用 / 和 **\**分隔路径也是可以的,这归根于Windows系统做的好,两种方式都兼容。
3.方法
下面会有方法和属性的实战。
|------------|---------------------|------------------------------------------------------------------------|
| 修饰符及返回值类型 | 方法签名 | 说明 |
| String | getParent() | 返回父目录路径 |
| String | getName() | 返回File对象的文件名称 |
| String | getPath() | 返回File对象的路径 |
| String | getAbsolutePath() | 返回File对象的绝对路径 |
| String | getCanonicalPath() | 返回File对象规范的绝对路径 |
| boolean | exists() | 判断File对象描述的文件是否真实存在 |
| boolean | isDirectory() | 判断File对象代表的文件是否是一个目录 |
| boolean | isFile() | 判断File对象代表的文件是否是一个普通文件 |
| boolean | createNewFile() | 根据File对象,创建一个空文件。成功创建返回true。 |
| boolean | delete() | 根据File对象,删除该对象代表的文件,成功删除返回true。 |
| void | deleteOnExit() | 根据File对象,标注该对象代表的文件将被删除,删除动作会到JVM运行结束时才会进行(程序结束时才会删除文件)。 |
| String[] | list() | 返回File对象代表的目录下的所有子文件文件名,不包括子目录下的文件。 |
| File[] | listFile() | 与上述相同,除了返回类型不同。 |
| boolean | mkdir() | 创建File对象代表的目录;如果创建File对象的构造方法中有父目录不存在,使用mkdir()创建对象代表的目录时就会失败,返回false。 |
| boolean | mkdirs() | 与mkdir()不同的是,使用mkdirs()创建File对象代表的目录时,对象的父目录不存在,就会连同父目录一起创建了。 |
| boolean | renameTo(File dest) | 进行文件改名,也可以视为平时的剪切、粘贴操作 |
| boolean | canRead() | 判断用户是否对文件有可读权限 |
| boolean | canWrite() | 判断用户是否对文件有可写权限 |
2.1.2方法和属性实战
- 构造方法
java
package fileCSDN;
import java.io.File;
import java.io.IOException;
public class Test1 {
public static void main(String[] args) throws IOException {
//创建File对象
//File(String)
File file1 = new File("D:\\java\\javacode-25year\\File\\111");
//File(File,String)
File file2 = new File(file1,"333");
//File(String,String)
File file3 = new File("D:\\java\\javacode-25year\\File\\111","444");
//这个点代表这个项目的路径!(..代表项目路径的父目录)
File file = new File("./test.txt");
}
}
- 属性
java
package fileCSDN;
import java.io.File;
public class Test2 {
public static void main(String[] args) {
System.out.println(File.pathSeparator);
System.out.println(File.pathSeparatorChar);
System.out.println(File.separator);
System.out.println(File.separatorChar);
}
}
依次打印:

- 方法
前面5个方法:getParent()、getName()、getPath()、getAbsolutePath()、getCanonicalPath()
java
package fileCSDN;
import java.io.File;
import java.io.IOException;
public class Test3 {
public static void main(String[] args) throws IOException {
File file = new File("D:\\java\\javacode-25year\\File\\src");
//获取父目录路径
System.out.println("第1:"+file.getParent());
//获取File对象(代表的)文件名
System.out.println("第2:"+file.getName());
//获取对象路径
System.out.println("第3:"+file.getPath());
//获取对象的绝对路径
System.out.println("第4:"+file.getAbsolutePath());
//获取对象的规范路径
System.out.println("第5:"+file.getCanonicalPath());
}
}
打印出:

new对象时书写路径的方式不同,会导致上面这些方法打印出来的效果不同,这里最后两个方法的返回值还竟然一模一样。看看下面这个File对象使用同样的方法返回打印出的有什么不同。

这么比较上下两个File对象使用同样5种方法返回的值截然不同,getAbsolutePath()和getCanonicalPath()的区别还是挺大的。
6~10方法实战:exists()、isDirectory()、isFile()、createNewFile()、delete()
java
public static void main(String[] args) throws IOException, InterruptedException {
File file = new File("./test.txt");
//判断对象代表的文件是否真实存在,已存在返回true,否则false
System.out.println("文件是否存在:"+file.exists());
//创建空文件
System.out.println("文件创立:"+file.createNewFile());
//再次判断对象代表的文件是否真实存在
System.out.println("文件是否存在:"+file.exists());
//刚才创建了一个普通文件------》test.txt文件,所以isDirectory()应该返回的是false
System.out.println("是否是目录:"+file.isDirectory());
System.out.println("是否是普通文件:"+file.isFile());
//主线程休眠10秒
Thread.sleep(10000);
//删除文件
System.out.println("文件删除:"+file.delete());
}

上述单线程代码中,我们想最后再删除test.txt文件,就把delete()语句放到了程序末尾,这时可以的。但有时代码较为复杂,我们又想最后再删除某个文件,就用deleteOnExit() 指定某个文件程序结束时删除文件。
12~13方法实战:list()、listFile()
java
public static void main(String[] args) {
//创建C盘的抽象对象
File file = new File("C:\\");
//返回路径C:\的子文件(目录也是文件)
String[] list = file.list();
System.out.println(Arrays.toString(list));
System.out.println("=====================================================");
//返回路径C:\的子文件(目录也是文件)
File[] files = file.listFiles();
System.out.println(Arrays.toString(files));
}

IDEA打印出的文件包括本地隐藏的文件 、操作系统文件 和未隐藏的文件, 我们平常在本地最多见到隐藏与未隐藏的文件(文件管理器勾选查看隐藏文件的属性),操作系统的文件是看不到的,因此上述两个方法打印出的文件名和本地C:\子文件数目不匹配。
14、15、16方法实战:mkdir()、mkdirs()、renameTo(File dest)
java
public static void main(String[] args) {
//注:该项目没子目录 111
File file1 = new File("./111/222");
//要在项目路径的子目录111下创建一个名为222的目录
System.out.println("mkdir目录创建:"+file1.mkdir());
System.out.println("mkdirs目录创建:"+file1.mkdirs());
System.out.println("===========================================");
File file2 = new File("./1");
System.out.println("mkdir目录创建:"+file2.mkdir());//这里换成mkdir也行
}

renameTo(File dest)

剩下canWrite()和canRead()两个方法暂不实战。
2.2文件内容操作
文件内容操作:对一个文件进行读和写。
Java提供了一组类,表示"流",流分为"输入流"和"输出流"。
Java有几十个类表示流,但常见的也就8种。学其他类时可类比这8种。
上述的几十种流,分成两大类:1.字节流 2.字符流
1.字节流 在读写文件内容时,以字节为单位,是针对二进制文件使用的。
- InputStream 输入 从文件读取数据
- OutputStream 输出 往文件写数据
2.字符流 在读写文件内容时,以字符为单位,是针对文本文件使用的。
- Reader 输入 从文件读取数据
- Writer 输出 往文件写数据
注意:读写操作是以cpu为视角看带的;应用程序中使用 InputStream进行输入,是给cpu(寄存器)输入数据,不是给硬盘输入数据(本地文件在硬盘中)。上面4个输入输出流都是抽象类,它们构成Java I/O操作框架。
1.字节流
InputStream/OutputStream 都是抽象类,应使用实现了它们的具体类。
如FileInputStream实现了InputStream,FileOutputStream实现了OutputStream。本章使用FileInputStream从文件读取,用FileOutStream往文件写入。
OutputStream 说明
方法:
|-----------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 修饰符及返回值类型 | 方法签名 | 说明 |
| void | write(int b) | 写入0~255整数,表示输入的内容,如97就代表a |
| void | write(byte[] b) | 将数组b中的全部数据写入文件中 |
| void | write(byte[] b,int off,int len) | 从b中下标为off的地方开始往文件写入数据,共写len个字节 |
| void | close() | 关闭字节流 |
| void | flush() | 重要:我们知道I/O 的速度是很慢的,所以,大多的OutputStream为了减少设备操作的次数,在写数据的时候会将数据先暂存,存到内存的一个指定区域里,直到该区域满了或者满足其他指定条件时,才真正将数据写入设备中,这个区域一般称为缓冲区。但造成一个结果,就是我们写的数据,很可能会遗留一部分在缓冲区中。需要在最后或者合适的位置调用flush(刷新)操作,将数据刷到设备中。 |
FileOutputStream 说明
构造方法:
|------------------------------|---------------|
| 签名 | 说明 |
| FileInputStream(File file) | 利用file构造文件输入流 |
| FileInputStream(String file) | 利用文件路径构造输入流 |
FileInputStream的构造方法也是如此。
输出流实战:

注意:OutputStream outputStream = new FileOutputStream(file);这里创建对象操作一旦成功,就相当于"打开文件"。每次程序打开一个文件,就会在文件描述符表中申请一个表项,由于文件描述符表不会自动关闭不用的表项,所以在业务繁忙的服务器中,不及时 outputStream.close() 就会导致文件描述符表表项耗尽,最终"文件资源泄露"。

产生这种情况的原因是,每次OutputStream outputStream = new FileOutputStream(file) 打开文件会清除file文件中的数据,要想不清除,接着上次写就要写入第二个参数-》FileOutputStream(file,true).
InputStream 说明
方法:
|-----------|----------------------------------|----------------------------------------------|
| 修饰符及返回值类型 | 方法签名 | 说明 |
| int | read() | 返回一个字节的数据,返回-1代表已经完全读完 |
| int | read(byte[] b) | 最多读取b.length 字节的数据到 b中,返回实际读到的字节数目;返回-1代表读完 |
| int | read(byte[] b,int off,int len) | 最多返回 len-off 字节的数据到b中,从第off字节数据开始读,返回-1代表读完。 |
| void | close() | 关闭字节流 |
输入流实战:

注意:不同编码方式,中文所占的字节数也不同,最常见的编码 UTF-8中常见中文占3个字节,有些生僻字占四个字节。
上图代码也忘记写close()关闭文件了;输入流和输出流都要手动关闭,这太麻烦了,而且容易忘记写close()。其实关闭文件操作不需要我们最后手动完成,用try-with-resources(资源管理机制)就解决了。
java
public static void main(String[] args) {
//实现了Closeable接口的类,出try代码块 就隐式调用close关闭,无需手动关闭。
//多个流使用 ; 分隔
try(InputStream inputStream = new FileInputStream("./test.txt");
OutputStream outputStream = new FileOutputStream("./test.txt")){
//。。。
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
总结
文件的I/O都是以CPU为视角,如输入流InputStream调用read是向cpu输入数据,输出流是从cpu输出,往文件中写数据。
不管是字节输入流还是输出流都需要关闭,关闭这两个I/O流,意味着关闭了进程(或叫程序)中对应的表项;而且I/O流不需要手动关闭,使用try-with-resources机制会自动帮我们关闭。
2.字符流
I/O的对象如果是文本文件,使用字符流操作是最方便的。
Reader和Writer也都是接口,这两个的使用和字节流的操作一模一样。
输出流(Writer)
Writer的writer方法

输出流(Reader)
Reader的read方法

CharBuffer 相当于对char[]封装。
字符流实战:
java
package fileCSDN;
import java.io.*;
public class Test6 {
public static void main(String[] args) {
try(Reader reader = new FileReader("./test.txt");
Writer writer = new FileWriter("./test.txt",true)) {
while(true){
//往文件写数据
writer.write("你好呀!");
char[] ch = new char[]{'哦','哦'};
writer.write(ch);
//读数据,一次最多读取4096个字符
char[] chars=new char[4096];
int a = reader.read(chars);
if(a==-1){
break;
}
for(int b=0;b<a;b++){
System.out.println(chars[b]);
}
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
字符流I/O操作细节与上面字节流I/O的操作细节类似,此处不再过多讲解。

综合示例:扫描指定目录,找到文件名或文件内容包含关键字的所有普通文件
java
package fileCSDN;
import java.io.*;
import java.util.Scanner;
//扫描指定目录,找到文件名或文件内容包含关键字的所有普通文件
public class Test7 {
public static void main(String[] args) throws IOException {
Scanner sc = new Scanner(System.in);
System.out.println("输入扫描的路径:");
String dirPath = sc.next();
File file = new File(dirPath);
if(!file.isDirectory()){
System.out.println("目录路径不存在或是普通文件");
return;
}
System.out.println("输入关键字:");
String keyWord = sc.next();
search(file,keyWord);
}
private static void search(File file, String keyWord) throws IOException {
if (file==null){
return;
}
File[] files = file.listFiles();
for(File f:files){
if(f.isDirectory()) {
search(f,keyWord);
}else {
dealFile(f,keyWord);
}
}
}
private static void dealFile(File f, String keyWord) throws IOException {
//equals是判断字符串是否相等;contains是判断一个字符串是否包含另一个字符串
if(f.getName().contains(keyWord)){
System.out.println("文件内容包含关键字:"+f.getCanonicalPath());
return;
}else {
try(Reader reader = new FileReader(f)){
//文件内容是否包含关键字
StringBuffer stringBuffer = new StringBuffer();
while(true){
char[] data = new char[1024];
int num =reader.read(data);
if (num==-1){
break;
}
//这里参数不能写1024,1024是字符数组最大元素个数,我们要按读取的字符个数拼接字符串
stringBuffer.append(data,0,num);
}
//如果stringBuffer中包含关键字就返回一个下标,不存在返回-1;
if(stringBuffer.indexOf(keyWord)>=0){
System.out.println("文件内容包含关键字:"+f.getCanonicalPath());
}
}
}
}
}