文件,我们之前在C语言中接触过,是在硬盘上存储数据的方式,操作系统帮我们把硬盘的一些细节都封装起来了,因此在这里我们只需要了解文件的相关接口即可.
首先硬盘是用来存储数据的,和内存相比,硬盘的存储空间更大,访问速度更慢,成本更低,可以实现持久化存储,而操作系统通过"文件系统"这样的模块来管理硬盘.
实际上在我们的电脑中只有一个硬盘(我这里加了一块,所以我有两块)操作系统可以通过文件系统吧这个硬盘抽象成多个硬盘一样(其实CDE盘就是操作系统通过文件系统锁划分出来的)
NTFS是Windows上的文件系统,背后有一定的格式来组织硬盘的数据
EXT4是Linux上常见的文件系统,虽然说有不同的文件系统,但是其管理文件的方式都是类似的,那么文件在系统中是一个什么样的结构呢?
答:通过目录(也就是我们口头说的文件夹)-文件构成了"N叉树"的树形结构.
我们可以看到,同样是一个cat.jpg的文件,站在不同的基准目录上,查找的路径是不相同的
文件系统上存储的文件,具体来说又分成两个大类.
1.[文本文件]存储的是字符,字符通常采用utf-8来编码,utf8就是一个大表,这个表上的数据组合就可以称为是字符
2.[二进制文件]存储的是二进制的数据
那我们如何判断一个文件是文本文件还是二进制文件呢?其实也很简单,用记事本打开,你能看懂的就是文本文件,若是乱码,则就是二进制文件
后续针对文件的操作,文本和二进制,操作方式是完全不同的
文件系统操作
首先要声明一个视角问题
IO:其实是input和output,你是站在cpu的角度来看待输入输出的
在Java中,我们可以通过java.io.file,通过file对象来描述一个具体的文件.file对象可以对应到一个真实存在的文件,也可以对应到一个不存在的文件
站在操作系统的角度看待,目录也是文件,操作系统中的文件是一个更广义的概念,具体来说里面有很多种不同的类型
1.普通文件(通常见到的文件)
2.目录文件(通常见到的文件夹)
windows上目录之间的分隔符,可以使用/也可以使用\,Windows诞生之前,表示目录的分隔符都是用 / Windows前身是dos.Linux和Mac就只是支持 / ,所以及时是在Windows,也尽量使用/,使用\在代码中需要搭配转义字符
运行结果如下
上述创建文件我们是使用了一个绝对路径,差别不是那么特别大,接下来我们使用相对路径来创建一个文件
这里是把工作目录拼接到当前目录,在IDEA中运行一个程序,工作目录就是项目所在的目录,那么加入我们在命令行中运行一个程序,工作目录就是命令行所在目录,在上述两个例子中我们可以看到,getCanonicalFile()本着上也是一个绝对目录,只不过getCanonicalFile将geyAbsoluterFile()得到的绝对目录整理了一下,使得其更加简洁
注意,我们并不是说将要创建的文件的文件路径写到构造方法中这个文件就创建好了,想要真正去创建一个文件还需要如下操作
创建文件操作
此时才是真正创建好了一个文件,当然了createNewFile是可能会抛出一个异常的,比如你当前写入的路径是一个非法路径,比如,创建的这个文件,对于所在的目录没有权限操作
文件创建好之后左边会有显示的
删除文件
左边的显示没有了
还有一种删除方式就是这个样子,在程序中检测到有这个文件,但是将程序结束之后会将这个文件删除,所以就造成了上述这种现象.那么这个东西有什么用呢?我们有的时候需要创建一个临时文件,就会用到这个功能
创建目录
创建一层目录
创建多层目录
重命名
现在test是与src同级的
代码运行之后,我们给这个文件重命名了,当然我们在这里看到的是我们把文件移动了位置,这是它的另外一种功能
以上文件系统的操作,都是基于File类来完成的,我们还需要针对文件内容的操作
文件流
什么是流?(stream)
文件这里的内容本质是来自于硬盘,硬盘又是操作系统管理的.使用某个编程语言操作文件,本质上都是需要调用系统API.虽然不同的编程语言操作文件的API有所差别,但基本步骤都是一样的,文件内容的核心操作步骤有四个
1.打开文件
2.关闭文件
3.读文件
4.写文件
在Java中操作文件内容也是有一系列的类的,但主要分为两个大类
字节流
inputStream 和 OutputStream 后续的一些操作字节的类都是衍生自这两个类 是以操作字节为单位的.(主要针对二进制文件)
字符流
Reader 和 Writer 后续操作字符的类,衍生自这两个类 是以操作字符为单位的.(主要针对文本文件)
JavaIO流是一个比较庞大的体系,涉及到非常多的类,这些不同的类,都有各自不同的特性.但是总的来说,使用方法都是类似的.
1.构造方法,打开文件
2.close方法,关闭文件
3.如果衍生自InputStream 或者 Read,就可以使用read方法来读数据
4.如果衍生自 OutputStream 或者 Writer 就可以使用write 方法来写数据了
我们先尝试一下创建一个文本文件使用一下,这个操作会抛出FileNotFoundException这样的一个异常
我们若是要把这个文件关闭,此时要抛出IOException
其实我们不难想到,FileNotFoundException是IOException的一个子类
注意close这个操作是非常重要的,这个操作是用来释放必要的资源的
让一个进程打开一个文件,是要从系统这里申请一定的资源的.(占用进程的pcb里的文件描述符表(本质上是一个顺序表,而且长度有限,不会自动扩容)中的一个表项)
如果不释放,就会出现"文件资源泄露"这样的很严重的问题.
一旦一直打开文件,而不去关闭不用的文件,文件描述符表就会终有一天被占满,后续就无法打开新的文件了
使用try{}finally{}来避免这样的情况
但是这样的操作不够优雅我们可以这样写,这样写的话只要代码执行完了,就会自动调用到close方法,这里的设定类似于synchronized,当然了try();()里面可以写多个打开文件的操作,而且文件流中的任意对象,都可以按照上述的方法来进行close.
读文件
reader
reader的返回值是int,假如你现在的文件中的内容不足1024那么就会把文件所有的内容都填到数组中(剩下的会有空余),此时返回值就会表示实际读到的字符的个数
InputStream
InputStream是字节流,用法和Reader非常相似,注意文本文件也是可以使用字节流打开,只不过此时你读到的每个字节,就不是完整的字符了
我们可以看到,数据是正确的
但是Java虽然有char,但是我们很少会用,更多的是使用String,此处,借助一些额外的工具类就可以完成字节/字符=>字符串的转换,虽然我们也可以直接使用String的构造方法完成char[]或者byte[]=>字符串的转换,比较的麻烦也不优雅,String的构造方法进行转换的时候,实际上构造方法内部是对字节数组进行了编码转换,当然了我们也可以使用工具类来进行操作
Scanner
所以我们就可以这样来写
以前学过的Scanner的操作,在这里完完全适用,但是需要注意Scanner只是用来读文本文件的,不适合读取二进制文件.
写文件
输出,使用的方法和输入非常相似,关键操作是write,write之前要打开文件,用完了也要关闭文件
输出流对象(无论字节流还是字符流)会在打开文件之后,清空文件内容!!
我们还可以按照追加写方式打开,此时就不会清空文件内容
此外读操作和写操作也是支持"随机访问"的,也就是说可以移动光标到指定位置,进行读写(此处就不加介绍了)
OutputStream使用方式完全一样,只不过write方法,不能支持字符串参数,只能按照字节或者字节数组来写入
练习
1.扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件(经典面试题)
import java.io.File;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA
* Description:
* User: lenovo
* Date: 2024 -01 -13
* Time: 13:42
*/
public class Test {
public static Scanner scanner=new Scanner(System.in);
public static void main(String[] args) {
System.out.println("请输入您要查找的目录:");
File rootPath=new File(scanner.next());
if (!rootPath.isDirectory()){
System.out.println("该目录无效");
return;
}
System.out.println("请输入你要查找的关键字:");
String word =scanner.next();
//创建一个遍历目录的方法
scanDir(rootPath,word);
}
//遍历目录
public static void scanDir(File rootPath,String word) {
//将rootPath目录下所有文件列出来
File[] cur=rootPath.listFiles();
if(cur.length==0||cur==null){
System.out.println("该目录为空");
return;
}
//深度优先遍历(一条路走到头后再转过来走另一条路)(也就是递归)
for (File f: cur) {
System.out.println(f.getAbsolutePath());
if(f.isFile()){
//题录中要求必须是文件,不可以是目录,所以这里要判断是文件还是目录
delDir(f,word);
}else{
scanDir(f,word);
}
}
}
private static void delDir(File f,String word) {
//来判断文件名中是否包含所要查找的关键字
if(f.getName().contains(word)){
System.out.println("是否要删除"+f.getAbsolutePath()+"?(Y/N)");
String choice=scanner.next();
if(choice=="Y"||choice=="y"){
f.delete();
System.out.println("删除成功");
}else{
System.out.println("已取消删除");
}
}
}
}
2.进行普通文件的复制
把一个文件,复制成另一个文件,也就是打开文件A,一次读出每一个字节,再写入到文件B就可以了
import java.io.*;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA
* Description:
* User: lenovo
* Date: 2024 -01 -13
* Time: 15:28
*/
public class Test {
public static void main(String[] args) throws IOException {
System.out.println("请输入你的源路径:");
Scanner scanner=new Scanner(System.in);
File src=new File(scanner.next());
if(!src.isFile()){
System.out.println("当前路径非法!");
return;
}
System.out.println("请输入你的目标路径:");
File dest=new File(scanner.next());
try(InputStream inputStream=new FileInputStream(src);OutputStream outputStream=new FileOutputStream(dest)){
byte[] bytes=new byte[2048];
while(true){
int n=inputStream.read(bytes);
System.out.println("n="+n);
if(n==-1){
System.out.println("读取到eof,读取结束");
return;
}
outputStream.write(bytes,0,n);
}
}
}
}
注意,每次进行read都是在访问硬盘,访问硬盘所造成的开销是比较大的,此时我们把bytes变大,就能降低访问的次数,从而提高效率,但是bytes大的前提是内存空间足够