目录
🐳今日良言:总要尝遍所有的路,再对生活充满期待。
🐇一、认识文件
1.认识文件
首先,认识一下文件,文件File这个概念,在计算机里面也是"一词多用".
狭义的文件:指的是我们磁盘上的文件和目录(文件夹)
广义的文件:泛指计算机中的很多软硬件资源.
操作系统中把很多的硬件设备和软件资源都抽象成了文件,按照文件的方式来统一管理.这一点在网络编程中很明显,操作系统将网卡当成了一个文件
本篇博客主要介绍的是狭义的文件.
2.两种路径
每个文件,在硬盘上都有一个具体的"路径"
上述路径叫做 d:/bin
表示一个文件的具体位置路径,就可以使用 / 来分割不同的目录级别.
d: c: e: 这些叫做盘符,cde这样的盘符是通过"磁盘分区"来的,每个盘符可以是一个单独的硬盘,也可以是若干个盘符对应一个硬盘.
那么,试想一下:为什么没有 a: b: 呢?
其实是有的,A,B盘是软盘,A、B两个盘符都是系统分配给软驱的,存储空间很小,只有几个MB
通过上述简单的介绍,对于路径有了一定的了解,接下来介绍两种表示路径的风格:
1).绝对路径:以c: d: 盘符开头的路径
2).相对路径:以当前所在的目录为基准,以 . 或者 .. 开头(有时候可以省略),找到指定的路径
这里当前所在目录称为工作目录,每个程序运行的时候,都有一个工作目录.
假定当前的工作目录是 d:/106,定位到aaa这个目录就可以表示成: ./aaa
./就表示当前的目录
如果工作目录不同,定位到同一个文件,相对路径写法是不同的.
同样是定位到aaa这里:
如果工作目录是d: 相对路径就写作 ./106/aaa
如果工作目录是d:/106 相对路径就写作 ./aaa
如果工作目录是d:/106/bbb 相对路径就写作 ../aaa (..表示当前目录的上级目录)
如果工作目录是d:/106/bbb/ccc 相对路径就写作../../aaa
3.文件的类型
我们接触到的文件有哪些呢?
word exe 图片 视频 音频 源代码... 这些都是不同的文件,但是整体可以归纳到两类中:
1).文本文件(存的是字符串,文本)
字符串,是由字符构成的.每个字符都是通过一个数字来表示的,每个文本文件里面存的数据都是合法的字符.都是在指定字符编码的码表之内的数据.
2).二进制文件(存的是二进制数据,不一定是字符串)
没有任何限制,可以存储任何想要存储的数据
通过上面的介绍,好像还是没法区分文本文件和二进制文件,那么,平常我们该如何区分这两类文件呢?
其实很简单,当随便拿到一个文件以后,直接使用记事本打开,如果看得懂里面内容,就说明是文本文件,如果看不懂,是乱码的情况下,就是二进制文件
🐉二、操作文件
在通过上面的介绍,对于文件有了初步认识,也了解到了路径,以及两类文件,接下来,详细介绍如何操作文件
Java对于文件的操作主要有:
1).针对文件系统操作(文件的创建 删除 重命名...)
2).针对文件内容操作(文件的读和写)
Java标准库提供了一个File类,通过这个File类来对文件(包括目录)进行抽象的描述,
注:有File对象,并不代表真实存在该文件.
1.File类的构造方法
方法 | 说明 |
File(File parent,String child) | 根据父目录+孩子文件路径,创建一个 新的File实例 |
File(String pathname) | 根据文件路径创建一个新的File实例, 路径可以是相对路径或者绝对路径 |
File(String parent,String child) | 根据父目录+孩子文件路径,创建一个 新的File实例,父目录用路径表示 |
注:parent表示当前文件所在的目录 child 是自身的文件名.
在new File对象的时候,构造方法参数中,可以指定一个路径(绝对路径或者相对路径都可以),此时,File对象就代表这个路径对应的文件了.
2.File类的相关方法
通过下面代码以及运行结果来观察上面的方法:
public class IODemo1 {
public static void main(String[] args) throws IOException {
// 不要求在d: 这里真的有个text.txt
File file = new File("d:/106/aaa/text.txt");//绝对路径
//File file = new File("./text.txt");// 相对路径
System.out.println(file.getName()); // file的纯文件名称
System.out.println(file.getParent());// file的父目录文件名称
System.out.println(file.getPath()); // file的文件路径
System.out.println(file.getAbsolutePath()); // file的绝对文件路径
System.out.println(file.getCanonicalFile());// 修饰过的绝对路径
}
}
运行结果
public class IODemo2 {
public static void main(String[] args) throws IOException {
File file = new File("./text.txt");
file.createNewFile();// 创建一个文件
System.out.println(file.exists()); // 是否存在
System.out.println(file.isFile()); // 是不是文件
System.out.println(file.isDirectory()); // 是不是目录
// mkdir 只能创建一级目录
// mkdirs 创建多级目录
file.mkdirs()
}
}
运行结果
剩下的方法,UU们可以自己试一下.
3.文件内容的读写
针对文件内容,使用"流对象" 进行操作.
"流对象"是一个形象的比喻,意思是,针对文件内容,要读100个字节,一次可以读一个字节,共读100次,也可以一次读10个字节,读10次,也可以一次读100个字节,读1次,这取决于自己,像水流一样生生不息.
将数据视为池中的水:
写操作:视为通过水龙头灌水---输出流
读操作:视为通过水龙头接水---输入流
Java标准库的流对象从类型上分为成两个大类:
1).字节流 操作二进制数据的
InputStream 读 FileInputStream
OutputStream 写 FileOutputStream
2).字符流 操作文本文件的
Reader 读 FileReader
Writer 写 FileWriter
这里涉及的类虽然很多,但是规律性很强,核心就是四个操作:
1.打开文件.(构造对象)
2.关闭文件.(close)
3.读文件(read) 针对InputSream / Reader
4.写文件(write) 针对OutputStream / Writer
通过下面代码实例来加深记忆和理解:
1).通过字节流来读取文件:
public class IODemo6 {
public static void main(String[] args) throws IOException {
// 文件必须保证存在
// 创建对象的时候, 使用绝对路径或者相对路径都是可以的 也可以使用File对象
InputStream inputStream = new FileInputStream("d:/text.txt");
// 进行读操作
while (true) {
int b = inputStream.read();
if (b == -1) {
break;
}
System.out.printf("%x\n",(byte)b);
}
// 关闭文件
inputStream.close();
}
}
运行结果
先介绍一下inputstream的read方法
read 无参数版本:一次读一个字节
read一个参数版本:把读到的内容填充到参数的这个字节数组中(此处的参数是个"输出型参数")
返回值是实际读取的字节数
read三个参数版本:和一个参数的版本类似,只不过是往数组的一部分区间里尽可能填充.
read方法的返回值为什么要使用int呢?
这是因为:read读取的是一个字节,按理说,返回一个byte就行了,但是实际上返回的是int,
除了要表示byte里的 0->255 (-128 ->127) 这样的情况之外,还需要表示一个特殊情况 -1
读到 -1 就表示读取文件结束了(读到文件末尾了)
为了提高IO效率,使用read一个参数版本,传入一个字节数组
public class IODemo6 {
public static void main(String[] args) throws IOException {
// 文件必须保证存在
// 创建对象的时候, 使用绝对路径或者相对路径都是可以的 也可以使用File对象
InputStream inputStream = new FileInputStream("d:/text.txt");
while (true) {
// buffe: 缓冲区 存在的意义就是提高IO操作的效率
byte[] buffer = new byte[1024];
// read 返回当前实际读取到的长度
// 第二轮读到的数据会覆盖第一轮读到的数据
// 所以说 读到数据就需要进行处理
int len = inputStream.read(buffer);
System.out.println("len:"+len);
// 等于 -1就结束
if (len == -1) {
break;
}
// 此时读取的结果就放到了 byte数组了
for (int i = 0; i < len; i++) {
System.out.printf("%x\n",buffer[i]);
}
}
// 关闭文件
inputStream.close();
}
}
运行结果:
注意理解这里的read,read会尽可能的把参数传进来的数组给填满,在上述代码创建的字节数组长度是1024,read就会尽可能的读取1024个字节填到数组中,但是实际上,文件剩余长度是有限的,如果剩余长度超过1024,此时1024个字节都会被填满,返回值就是1024了,如果当前剩余长度不足1024,此时有多少就填多少,read方法就会返回当前实际读取的长度.
2).使用字节流写文件
public class IODemo7 {
public static void main(String[] args) {
// 对于outputStream来说.默认情况下,打开一个文件 会先清空这个文件原有的内容
// 如果不想清空,流对象还提供了一个"追加写"对象 通过这个就可以实现不清空内容
// 把新内容追加写到后面
try (OutputStream outputStream = new FileOutputStream("d:/text.txt")) {
outputStream.write(97);
outputStream.write(98);
outputStream.write(99);
outputStream.write(100);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
此时当运行以后,打开这个文件,观察里面内容:
可以观察到,里面的内容确实是想要的结果.
接下来,详细解释上述代码涉及到的知识点:
在上述代码中,并没有使用close方法,close一般来说都是要执行的,如果不执行会怎么样?
进程在内核里,使用PCB这样的数据结构表示,PCB中有一个重要属性:文件描述符表.(相当于一个数组)记录了该进程打开了哪些文件.
每次打开文件操作,都会在文件描述符表中,申请一个位置,把这个信息放进去.
每次关闭文件操作,就会把文件描述符表对应的表项给释放掉
如果没有close,没有及时释放.虽然Java有GC GC操作会在回收这个OutputStream对象的时候去完成这个释放操作,但是GC不一定释放及时.
所以,如果不手动释放, 意味着文件描述符表可能很快就会被占满了(这个数组,不能自动扩容,存在上限) ,如果占满之后,后面再次打开文件,就会打开失败.
文件描述符表最大长度基本上就是几百到几千左右..
所以说,在代码中,必须保证这个close被执行到,那么,如何才能保证这个close被执行到呢?
最推荐的写法就是这个写法:
这个写法虽然没有显式的写close,但是实际上是会执行的,只要try语句块执行完毕,就会自动执行close操作,这个语法,在Java中叫做:try with resource
不是说随便一个对象都可以放到try的括号中,只有实现了Closeable接口的类才可以放到括号中被自动关闭.这个接口提供的方法就是close方法.
观察OutputStream类的源码会发现实现了这个Closeable接口:
如果try()里面定义的对象多于一个,使用分号 (;) 分割
3).使用字符流读文件
public class IODemo8 {
public static void main(String[] args) throws IOException {
try (Reader reader = new FileReader("d:/text.txt")) {
while (true) {
int ch = reader.read();
if (ch == -1) {
break;
}
System.out.println(" "+(char)ch);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果
4).使用字符流写文件
public class IODemo9 {
public static void main(String[] args) {
try (Writer writer = new FileWriter("d:/text.txt")) {
writer.write("hello world");
// 手动刷新缓冲区 确保内容已写入
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果以后,打开上面text.txt观察里面内容:
像上述写操作:
其实是先写到缓冲区中(缓冲区有很多种形态),当写操作执行完毕后,内容可能在缓冲区里,还没有真的进入硬盘,close操作就会出发缓冲区的刷新(刷新操作就会把缓冲区的内容写到硬盘里面),除了close方法以外,flush方法也可以起到刷新缓冲区的效果.
5).Scanner搭配流对象进行使用
这里的System.in 其实就是一个输入流对象.
public class IODemo10 {
public static void main(String[] args) {
try (InputStream inputStream = new FileInputStream("d:/text.txt")) {
Scanner scanner = new Scanner(inputStream);
scanner.next();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Scanner 的close本质上就是要关闭内部包含的这个流对象,但是此时内部的InputStream对象已经被
try() 关闭了,里面的Scanner 不关闭也没事
🐯三、练习
接下来,通过两组典型案例,来增加对IO操作的理解以及掌握.
1.扫描目录,删除文件
实现思路:
1).首先,让用户输入一个扫描目录,然后判断用户输入的路径是不是目录,如果是的话,再让用户输入要删除的文件名.
2).因为文件系统是树形结构,所以我们使用深度优先遍历(递归)完成遍历.
3).通过使用File类的 listFiles() 方法返回值是File数组,可以列出所有用户输入的目录下的所有目录以及文件,这样的话,就可以通过遍历这个数组中的每个元素来进行判断,如果这个元素是目录的话就继续递归,如果不是目录(普通文件)判断是不是要删除的文件.
代码实现:
import java.io.File;
import java.util.Scanner;
/**
* 扫描目录 删除文件
* @author 26568
* @date 2023-01-12 14:19
*/
public class IODemo11 {
private static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
System.out.println("请输入你要搜索的路径");
String basePath = scanner.next();
// 针对用户输入的路径进行判断
File root = new File(basePath);
// 如果不是目录直接结束操作
if (!root.isDirectory()) {
// 路径不存在 或者只是一个普通文件 此时无法进行搜索
System.out.println("输入的路径有误");
return;
}
// 再让用户输入一个要删除的文件
System.out.println("请输入要删除的文件名");
String nameToDelete = scanner.next();
// 使用该方法进行删除文件
scanDir(root,nameToDelete);
}
private static void scanDir(File root, String nameToDelete) {
// 打印当前目录
System.out.println("[scanDir]"+root.getAbsolutePath());
// 1.列出root下的文件和目录
File[] files = root.listFiles();
// 如果为空 说明当前目录没有要删除的文件名
if (files == null) {
// 结束递归
return;
}
// 2.遍历当前files数组 判断是否包含要删除文件名
for (File f:files) {
// 先判断是不是目录
if (f.isDirectory()) {
// 继续递归
scanDir(f,nameToDelete);
} else {
// 是普通文件 判断是否要删除
// 如果包含要删除的文件名
if (f.getName().contains(nameToDelete)) {
System.out.println("确认要删除"+f.getAbsolutePath()+"吗?");
String choice = scanner.next();
if (choice.equals("y") || choice.equals("Y")) {
f.delete();
System.out.println("删除成功");
} else {
System.out.println("删除取消");
}
}
}
}
}
}
运行结果
2.拷贝文件
实现思路:
1).首先,让用户输入要拷贝的文件,然后再输入要拷贝的目的路径,如果输入的不是文件就直接返回, 如果输入的目的路径已经是一个文件了,也直接结束操作.
2).接下来开始拷贝,使用字节流来进行拷贝,每次读取一个字节,然后写入.
代码实现:
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
*
* @author 26568
* @date 2023-01-13 9:47
*/
// 拷贝文件
public class Exercise {
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入你要拷贝的文件");
String root = scanner.next();
System.out.println("请输入目的路径");
String dest = scanner.next();
// 判断输入的路径是否有误
File rootFile = new File(root);
if (!rootFile.isFile()) {
// 如果不是文件
System.out.println("您输入的文件路径有误");
return;
}
File destFile = new File(dest);
if (destFile.isFile()) {
// 如果已经是文件
System.out.println("您输入的目的路径有误");
return;
}
// 进行拷贝
try(InputStream inputStream = new FileInputStream(rootFile);
OutputStream outputStream = new FileOutputStream(destFile)) {
// 一个字节一个字节进行拷贝
while (true) {
int b = inputStream.read();
if (b == -1) {
break;
}
outputStream.write((byte)b);
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果: