文章目录
- 1. 什么是文件?
- 1.1 树型结构组织 和 目录
- 1.2 文件路径
- 1.3 文件类型
- 2. java 操作文件
- 2.1 File 概述
- 3. 文件内容的读写 数据流
- 3.1 Reader
- 3.2 Writer
- 3.3 InputStream
- 3.4 OutputStream
- 3.5 字节流转字符流
- 4. 小程序示例练习
1. 什么是文件?
所谓的“文件”是一个广义的概念,可以表示很多东西
操作系统里,会把很多的 硬件设备 和 软件资源,都抽象为“文件”,统一进行管理
但是大部分情况下,谈到的文件,都是指硬盘的文件
文件就是相当于针对“硬盘”数据的一种抽象
这是一个机械硬盘(HDD)
机械硬盘适合顺序读写,不适合随机读写
磁头移动的过程,需要时间(遵循牛店第二定律)
固态硬盘(SSD)
里面是集成程度很高的芯片(类似于 cpu,内存)
固态硬盘,就要比机械硬盘效率高
(目前的电脑基本上就是固态硬盘)
服务器开发中,涉及到的硬盘,有的是机械硬盘有的是固态硬盘
尤其是一些用来存储大规模数据的机器,仍然是机械硬盘为主
当然,即使是固态硬盘,读写速度还是比内存要低很多
内存和硬盘的对比:
- 内存的速度快,硬盘速度慢
- 内存空间小,硬盘空间大
- 内存贵,硬盘便宜
- 内存的数据,断电就丢失,硬盘的数据断电还在
1.1 树型结构组织 和 目录
一台计算机上有很多文件,这些文件是通过“文件系统”(操作系统提供的模块)来进行组织的
此电脑就是目录树的根节点
在每一个盘里面还有很多目录(directory)
把目录点进去,可以发现还有其他的目录和文件
这样就组成了一个树形结构
1.2 文件路径
我们就可以使用目录的层次结构,来描述 文件所造的位置,这样就被叫做“路径”
D:\代码仓库\learn_Java\learn_-java
形如这样的一个字符串,体现了当前文件在那个目录中
就可以通过文件路径,来确定当前文件具体在哪个位置
路径的分类:
- 绝对路径:是以 C: D: 盘符开头的,这种路径被称为“绝对路径”
- 相对路径:需要先指定一个目录 作为基准目录,看看沿着什么样的路径能够找到指定文件
此时涉及到的路径就是“相对路径”
往往是以. 或者… 开头(. 这种情况可以省略)
. 表示当前目录
… 表示当前目录的上一级
假如我需要找
- 假设当前的基准目录就是
D:\代码仓库\learn_Java\learn_-java
使用 .\J1119 system_code 就可以找到这个文件 - 假定当前的基准目录是
D:\代码仓库\learn_Java
使用 .\learn_Java\J1119 system_code 就可以找到 - 假设当前的基准目录是
D:\代码仓库\learn_Java\learn_-java\L20231202
使用 …\J1119 system_code 就可以找到
这些都是相对路径的表示方式
如果是命令行进行操作,基准目录,就是你当前所处的目录
如果是图形化界面的程序,基准目录就不好说
对于 IDEA 来说,基准目录,就是项目目录
1.3 文件类型
从编程的角度看,文件类型,主要是两大类
- 文本文件(文件中保存的数据,都是字符串,保存的内容都是 合法的字符)
- 二进制文件(文件中保存的数据,仅仅是二进制文件忙不要求保存的内容是 合法的字符)
什么是合法的字符?
字符集/字符编码
uft8 有一个大的表格,就列出了什么字符,对应到什么编码
如果你的文件是 utf8 编码的
此时文件中的每个数据都是合法的 utf8 编码的字符
就可以认为这个文件是文本文件了
如果存在一些不是 utf8 合法字符的情况,就是二进制了
如何判定一个文件是文本还是二进制?
就直接使用记事本,打开这个文件如果打开之后,是乱码,文件就是二进制,否则就是文本
(记事本就是尝试按照字符的方式来展示内容,这个过程就会自动查码表)
很多文件都是二进制的,docx,pptx… 都属于二进制文件
区分文本和二进制,是很重要的!!!
写代码的时候,文本文件和二进制文件,代码编写方式是不同的
2. java 操作文件
java 针对文件的操作 分成两类
- 文件系统的操作 File
创建文件、删除文件、判定文件是否存在、判定文件类型、重命名… - 文件内容的操作 流对象
读文件/写文件
2.1 File 概述
属性
pathSeparator 是一个路径中,分割目录的符号
\ 就是 pathSeparator
但是 Windows 上,\ 和 / 都可以作为分隔符
Linux 和 Mac 上 / 作为分隔符
一般我们使用 / ,使用 \ 在代码中同时还需要搭配转义字符使用
构造方法
一个 File 对象,就表示了一个硬盘上的文件
在构造 对象 的时候,就需要把这个文件的路径给指定出来(使用绝对/相对路径)
方法
getName()
文件名 = 前缀 + 扩展名
使用路径构造 File 对象,一定要把前缀和扩展名都带上
public static void main(String[] args) throws IOException {
File f = new File("d:/test.text");
System.out.println(f.getParent());
System.out.println(f.getName());
System.out.println(f.getPath());
System.out.println(f.getAbsolutePath());
System.out.println(f.getCanonicalPath());
}
Windows 上的盘符,是不区分大小的
如果改为相对路径
public static void main(String[] args) throws IOException {
File f = new File("./test.text");
System.out.println(f.getParent());
System.out.println(f.getName());
System.out.println(f.getPath());
System.out.println(f.getAbsolutePath());
System.out.println(f.getCanonicalPath());
}
后面两行的结果,就是把当前的相对路径拼接到基准目录上,就得到了绝对路径
getCanonicalPath 就是针对绝对路径进行简化后的路径
public static void main(String[] args) throws IOException {
File file = new File("./test.text");
System.out.println(file.exists());
System.out.println(file.isDirectory());
System.out.println(file.isFile());
//创建文件
boolean ret = file.createNewFile();
System.out.println("ret = " + ret);
System.out.println(file.exists());
System.out.println(file.isDirectory());
System.out.println(file.isFile());
}
public static void main(String[] args) {
File f = new File("./test.text");
boolean ret = f.delete();
System.out.println("ret = " + ret);
}
public static void main(String[] args) throws InterruptedException {
File f = new File("./test.text");
/*boolean ret = f.delete();
System.out.println("ret = " + ret);*/
f.deleteOnExit();
Thread.sleep(5000);
System.out.println("进程结束3");
}
这里的 deleteOnExit 会在进程结束之后才删除
public static void main(String[] args) {
File f = new File("d:/");
String[] files = f.list();
//System.out.println(files);//会打印出数组的哈希值
System.out.println(Arrays.toString(files));
}
这个时候,d盘中的目录就会被获取到
public static void main(String[] args) {
File f = new File("d:/java1213");
boolean ret = f.mkdir();
System.out.println("ret = " + ret);
}
但是mkdir 没有办法创建多层目录
这里就可以使用 mkdirs
public static void main(String[] args) {
File f = new File("d:/java1213/aaa/bbb/ccc");
//boolean ret = f.mkdir();
boolean ret = f.mkdirs();
System.out.println("ret = " + ret);
}
public static void main(String[] args) {
//src 就是“源” , dest 就是“目标”
File srcFile = new File("d:/test.txt");
File destFile = new File("d:/test2.txt");
boolean ret = srcFile.renameTo(destFile);
System.out.println("ret = " + ret);
}
3. 文件内容的读写 数据流
流对象(Stream)
在说到流对象之前,我们先介绍一下什么是流
比如我们要接100ml水
- 一次性接完
- 分两次,一次接50ml
- 分五次,一次接20ml
- 分十次,一次接10ml
- …
完成接水的操作,我们有无数的方法
水的特点,我们称为“水流”
那么现在我要读/写 100 字节的文件数据
- 一次性直接读写 100 字节
- 分两次,一次读写 50 字节
- 分五次,一次读写 20 字节
- 分十次,一次读写 10 字节
- …
完成上述的读写文件操作,同样也有无数种方案
文件的特点 我们称为“文件流”
在标准库中,提供的读写文件的流对象,不是一两个类,而是有很多类
虽然这里有很多类,实际上这些类都可以归结为两个大的类别中
- 字节流(对应着 二进制 文件)
每次读/写的最小单位,都是“字节”(8bit) - 字符流(对应着 文本文件)
每次读/写的最小单位,是“字符”(一个字符可能是对应多个字节)
GBK,一个中文字符(两个字节)
UTF8,一个中文字符(三个字节)
字符流,本质上,是针对字节流进行了又一层封装
字符流,就能自动把文件中几个相邻的字节,转换成一个字符(完成了一个自动查字符集表)
字节流有相对应的输入输出
InputStream
OutputStream
字符流也有相对应的输入输出
Reader
Writer
但是 到底什么叫做输入?什么叫做输出?
我如果把一个数据保存到硬盘中,这是输入就还是输入?
如果在 硬盘的视角,这就是输入
如果在 cpu 的视角,这就是输出
但是我们一般都是站在 cpu 的视角
3.1 Reader
由于 Reader 是一个抽象类,没有办法直接 new,需要 new 一个子类
这个时候标准库就已经提供了现成的类了
创建对象的过程就是在打开文件
接下来我们用 reader 的 read 方法
- 无参数 read:一次读取一个字符
- 一个参数 read:一次读取若干个字符,会把参数指定的 cbuf 数组给填充满
- 三个参数 read:一次读取若干非字符,会把参数执行的 cbuf 数组中的,从 off 这个位置开始,到 len 这么长的位置尽量填满
但是我们看到原码,返回类型是一个 char
如果读到一个正确的字符,就会返回 0 - 65535 之间的数
如果读到末尾,读完了,就会返回 -1
用 int 就是希望能得到无符号的 char 和 -1
但是如果是 utf8 编码,一个中文字符,应该是 3 个字节
在 Java 标准库内部,对于字符编码是进行了很多的处理工作的
如果是只使用 char,此时使用的字符集,固定就是 unicode
如果是使用 String,此时就会自动的把每个字符的 unicode 转换成 utf8
charl] c => 包含的每个字符,都是 unicode 编码,是固定的
一旦使用这个字符数组构造成 String
String s = new String©;
就会在内部把每个字符,都转换成 utf8
s.charAt() => char
也就会把对应的 utf8 的数据,转换成 unicode
把多个 unicode 连续放到一起,是很难区分出从哪里到哪里是一个完整的字符的
utf8 是可以做到区分的
utf8 可以认为是针对连续多个字符进行传输时候的一种改进方案 ( 也是从 unicode 演化过来的 )
public class Demo7 {
public static void main(String[] args) throws IOException {
Reader reader = new FileReader("d:/test.txt");
while (true) {
int c = reader.read();
if (c == -1) {
//读取完毕
break;
}
char ch = (char)c;
System.out.println(ch);
}
}
}
应该往这个 read 里传入的是一个空的字符数组(不是 null,而是没有实际意义数据的数组)
然后由 read 方法内部,对这个数组内容进行填充
此时 cbuf 这个参数,称为“输出型参数”
//2.一次 read 多个字符
while (true) {
char[] cbuf = new char[1024];
//n 表示当前读到的字符的个数
int n = reader.read(cbuf);
if (n == -1) {
//读取完毕
break;
}
System.out.println("n = " + n);
for (int i = 0; i < n; i++) {
System.out.println(cbuf[i]);
}
}
这个 read 会尽可能的把 cbuf 这个数组给填满
如果实际上文件的内容填不满,填不满也没关系,就直接把所有的数据都读出来就好了
使用 close 方法,最主要的目的,是为了释放 文件描述符
为了管理进程,我们会使用到 PCB
这里会包含很多的属性
- pid
- 内存指针
- 文件描述符表
- …
文件描述表,相当于一个顺序表
一个进程每次打开一个文件,就需要在这个表里,分配一个元素
这个数组的长度是存在上限的
如果你的代码,一直打开文件,而不去关闭文件
就会使这个表里的元素,越来越多,一直到把这个数组占满了
后续再尝试打开文件,就会出错了
这样就会导致 文件资源泄露
非常类似于 内存泄漏
public class Demo7 {
public static void main(String[] args) throws IOException {
Reader reader = new FileReader("d:/test.txt");
/*//1.一次 read 一个字符
while (true) {
int c = reader.read();
if (c == -1) {
//读取完毕
break;
}
char ch = (char)c;
System.out.println(ch);
}*/
//2.一次 read 多个字符
while (true) {
char[] cbuf = new char[1024];
//n 表示当前读到的字符的个数
int n = reader.read(cbuf);
if (n == -1) {
//读取完毕
break;
}
System.out.println("n = " + n);
for (int i = 0; i < n; i++) {
System.out.println(cbuf[i]);
}
}
//3.一个文件使用完了,要记得,close
reader.close();
}
}
在这串代码中就很有可能导致 close 执行不到
这个时候我们就需要用到 try finally
public class Demo7 {
public static void main(String[] args) throws IOException {
Reader reader = new FileReader("d:/test.txt");
/*//1.一次 read 一个字符
while (true) {
int c = reader.read();
if (c == -1) {
//读取完毕
break;
}
char ch = (char)c;
System.out.println(ch);
}*/
try {
//2.一次 read 多个字符
while (true) {
char[] cbuf = new char[1024];
//n 表示当前读到的字符的个数
int n = reader.read(cbuf);
if (n == -1) {
//读取完毕
break;
}
System.out.println("n = " + n);
for (int i = 0; i < n; i++) {
System.out.println(cbuf[i]);
}
}
}finally {
//3.一个文件使用完了,要记得,close
reader.close();
}
}
}
虽然用上面的方式可以达到效果,但是代码不够美观
我们还需要进行改变
public class Demo8 {
public static void main(String[] args) throws IOException {
try (Reader reader = new FileReader("d:/test.txt")) {
while (true) {
char[] cbuf = new char[3];
int n = reader.read(cbuf);
if (n == -1) {
break;
}
System.out.println("n = " + n);
for (int i = 0; i < n; i++) {
System.out.println(cbuf[i]);
}
}
}
}
}
这个语法就叫做 try with resources
这个语法的目的就是:
()中定义的变量,会在 try 代码块结束的时候(正常结束,还是抛出异常 都会执行)
自动调用其中的 close 方法
要求写到()里面的对象必须实现 Closeable接口
流对象都可以这样写
Reader Writer InputStream OutputStream 的使用方法都很类似,下面就简单演示
3.2 Writer
- 一次写一个字符
- 一次写一个字符串
- 一次写多个字符(字符数组)
- 带有 offset 和 len,offset 就指的是从 数组/字符串 中的第几个字符开始写
Writer 写入文件,默认情况下就会把原有文件的内容清空掉
如果不想清空,需要在构造方法中多加个参数
此时就不会清空原来文件的内容,而是写入到原有文件的末尾
public class Demo9 {
public static void main(String[] args) {
try (Writer writer = new FileWriter("d:/test.txt", true)) {
// 直接使用 write 方法就可以写入数据.
writer.write("我在学习文件IO");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
3.3 InputStream
后面看起来是 int 实际上也是 byte.
0-255 之间的数据
使用 -1 表示读到文件末尾
public static void main(String[] args) {
try (InputStream inputStream = new FileInputStream("d:/test.txt")) {
byte[] buffer = new byte[1024];
int n = inputStream.read(buffer);
System.out.println("n = " + n);
for (int i = 0; i < n; i++) {
System.out.printf("%x\n",buffer[i]);
}
} catch (IOException e) {
e.printStackTrace();
}
}
运行出来的数字,就是文件中这行字母的 ASCII 码
3.4 OutputStream
public static void main(String[] args) {
try (OutputStream outputStream = new FileOutputStream("d:/test.txt")){
String s = "你好世界";
outputStream.write(s.getBytes());
}catch (IOException e) {
e.printStackTrace();
}
}
和 Writer 类似,OutputStream 打开一个文件,默认就会清空文件的原有内容
写入的数据,就会成为文件中新的数据
如果不想清空就可以使用追加写的方式 ( 在构造方法中,第二个参数传入 true )
3.5 字节流转字符流
java 标准库中支持的流对象 种类非常的多,功能也很丰富
以上四种只是最基本的流对象使用
在刚刚写的字节流代码中,是可以转成字符流了
在别人给提供的字节流对象,但是你知道实际的数据内容是文本文件,就可以把上述字节流转成字符流
用 scanner
public class Demo12 {
public static void main(String[] args) {
try (InputStream inputStream = new FileInputStream("d:/test.txt")) {
// 此时 scanner 就是从文件读取了!!
Scanner scanner = new Scanner(inputStream);
// 就可以使用 scanner 读取后续的数据.
String s = scanner.next();
System.out.println(s);
} catch (IOException e) {
e.printStackTrace();
}
}
}
scanner 可以通过键盘读,可以通过文本文件读,也可以通过网络读
public static void main(String[] args) {
try (OutputStream outputStream = new FileOutputStream("d:/test.txt")) {
// 这就相当于把字节流转成字符流了.
PrintWriter writer = new PrintWriter(outputStream);
writer.println("hello");
} catch (IOException e) {
e.printStackTrace();
}
}
我们运行结束之后可以看到,文件内部还是空着的,这是为什么?
是因为,这里面涉及到了“缓冲区”
PrintWriter 这样的类,在进行写入的时候,不一定是直接写硬盘,而是先把数据写入到一个 内存构成的“缓冲区”中 ( buffer )
引入缓冲区,目的是为了提高效率
把数据写入内存,是非常快的
把数据写入硬盘,是非常慢的 ( 比内存慢个几干倍,上万倍 )
为了提高效率,就也应该想办法减少写硬盘的次数
但是,这样听起来美好,但是有个问题
当我们写入缓冲区之后,如果还没来得及把缓冲区的数据写入硬盘,进程就结束了
此时数据就丢了,这其实没有真正写入硬盘呢
为了能够确保数据确实被写入硬盘
就应该在合适的时机,使用 flush 方法手动 刷新缓冲区
这样做就可以确保当前的 数据 确实是落到硬盘上
4. 小程序示例练习
扫描指定⽬录,并找到名称中包含指定字符的所有普通⽂件(不包含⽬录),并且后续询问用户是否要删除该⽂件
这个主要用到了文件系统操作
- list 列出目录内容
- 判定文件类型
- 删除文件
找到目录中的所有文件,以及子目录中的所有文件
只要遇到子目录都能往里找
“递归”的方式,把所有的子目录都给扫描一遍
public class Demo14 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
//1.先让用户输入一个要扫描的目录
System.out.println("请输入要扫描的路径:");
String path = scanner.next();
File rootPath = new File(path);
if (!rootPath.isDirectory()) {
System.out.println("您输入的扫描路径有误!");
return;
}
//2.再让用户输入一个要查询的关键词
System.out.println("请输入要删除文件的关键词:");
String word = scanner.next();
//3.可以进行递归的扫描
// 通过这个方法进行递归
scanDir(rootPath, word);
}
private static void scanDir(File rootPath, String word) {
//1.先列出 rootPath 中所有的文件和目录
File[] files = rootPath.listFiles();
if (files == null) {
//如果当前目录为空,直接返回
return;
}
//2.遍历这里的每个元素,针对不同类型做出不同的处理
for (File f : files) {
//加个日志,方便观察当前的执行过程
System.out.println("当前扫描的文件:" + f.getAbsolutePath());
if (f.isFile()) {
//普通文件,检查文件是否要删除,并执行
checkDelete(f, word);
}else {
//目录,递归的再去判定子目录里面包含的内容
scanDir(f, word);
}
}
}
private static void checkDelete(File f, String word) {
if (!f.getName().contains(word)) {
//不必删除,方法结束
return;
}
//需要删除
System.out.println("当前文件为:" + f.getAbsolutePath() + ",请确定是否要删除(N/Y)");
Scanner scanner = new Scanner(System.in);
String choice = scanner.next();
if (choice.equals("Y") || choice.equals("y")) {
//真正执行删除操作
f.delete();
System.out.println("删除完毕");
}else {
//如果输入其他值,不一定非要是 N ,都会取消删除操作
System.out.println("取消删除操作!");
}
}
}