目录
1、认识文件
1.1、路径
1.1.1 、绝对路径
1.1.2、相对路径
1.2、文本文件 vs 二进制文件
2、文件系统操作
3、文件内容操作
3.1、字节流用来输入的抽象类InputStream的方法使用
3.1.1、FileInputStream类的构造方法
3.1.2、字节流读操作
3.1.3、字节流写操作
3.2、 字符流的读写操作
4、文件操作的案例
1、认识文件
我们平时谈到的"文件",指的都是硬盘上的文件。我们在进行数据保存的时候,往往不是保存成一个整体,而是独立成一个个单位进行保存,这个独立的单位就被抽象成文件的概念,就类似办公桌上的一份一份真是的文件。
硬盘(外存)和内存相比
- 速度:内存比硬盘快很多
- 空间:内存空间比硬盘小
- 成本:内存比硬盘贵
- 持久化:内存掉电后数据会丢失,外存掉电后数据还在。
🎉文件的元信息:文件除了有数据内容之外,还有一部分信息,例如文件名,文件类型、文件大小等并不作为文件的数据而存在,我们把这部分信息可以视为文件的元信息。
我们之前博客中的代码,绝大部分是围绕内存展开的(JavaSE和数据结构),定义一个变量其实就是在内存上申请空间。MySQL主要就是操作硬盘。我们这个博客中的文件IO也是操作硬盘。
1.1、路径
🎃路径是文件系统上一个文件/目录(文件夹)的具体位置。
✨文件系统是以树形结构来组织文件和目录——这个树形结构为(N叉树) 。
🎉文件路径:就是从数根结点出发,沿着树杈,一路往下走,到达目标文件,此时这中间经过的内容就是文件路径。
📙 实际表示路径,是通过一个字符串表示,每个目录之间使用\\或者/来分割
- 上边的图片中我们看到每个目录之间是使用\表示的,反斜杠(\)只是在Windows中适用,我们在写代码的时候需要写成\\,用转义字符将\转换。所以我们在写代码的时候,还是比较建议使用、。
- 上述看见两个图中一个路径存在此电脑,一个不存在此电脑。存在此电脑的是因为我们是按照层级打开目录,所以电脑上会显示。另外一个不存在此电脑的是因为,我们想要找(或者是表示)一个文件的时候,默认是从盘符开始的。表示路径的时候,可以把"此电脑"省略,直接从盘符开始表示。
1.1.1 、绝对路径
从盘符开始,一层一层往下找,这个过程,得到的路径,就是绝对路径。
1.1.2、相对路径
从给定的某个目录出发,一层一层往下诏,这个过程得到的路径,就是相对路径。
. 在相对路径中,表示当前目录;
.. 在相对路径中,表示的是当前目录的上级目录。
❗❗❗注意:
- 相对路径,一定要明确工作目录(基准目录)是什么。
- 文件系统上任何一个文件,对应的路径是唯一的,不会存在两个路径相同,但是文件不同的情况。在Linux上可能存在一个文件,有两个不同的路径能找到它;但是在Windows上不存在,Windows上可以认为,路径和文件是一一对应的,路径就相当于一个文件的"身份标识"。
1.2、文本文件 vs 二进制文件
1️⃣文本文件:存储的是文本(文本文件的内容都是由ASCII表字符构成)。文本文件里存储的数据,就是遵守ASCII或者其他字符集编码(例如:utf8),所得到的文件,本质上存的是字符(不仅仅是char)。
2️⃣二进制文件:存储的是二进制数据,(存储不受任何字符集的限制)
✨判定一个文件是文本文件还是二进制文件的方法。
- 直接使用记事本打开某个文件,如果打开之后的内容你能够看懂,这个文件就是文本文件;如果你看不懂,内容乱糟糟的,这个文件就是二进制文件。(因为记事本是默认按照文本的形式来解析显示的,解析成功就是文本文件,解释失败就是二进制文件)
- 文本文件:文件后缀为.txt,.java,.c
- 二进制文件:文件后缀为 .class ,.exe,.jpg,.mp3
2、文件系统操作
Java标准库给我们提供了File这个类,Flie对象是对硬盘上的一个文件的"抽象"表示。文件是存储在硬盘上的,直接通过代码操作硬盘,不太方便,就在内存中创建一个对象,操作这个内存中的对象,就可以间接的影响到硬盘的文件情况了。
1️⃣构造File对象
构造的过程中,可以使用绝对路径或者相对路径进行初始化,这个路径指向的文件,可以是真实存在的,也可以是不存在的。
public class IODemo1 {
public static void main(String[] args) {
//初始化这个对象的时候,这个文件可以真实存在也可以不存在。
File file = new File("d:/dog.jpg");
}
}
2️⃣File类提供的一些方法
写一些代码来了解这些方法的使用
🎉观察get系列方法的特点和差异
public class IODemo1 { public static void main(String[] args) throws IOException { //初始化这个对象的时候,这个文件可以真实存在也可以不存在。 File file = new File("d:/dog.jpg"); //获取File对象的父目录文件路径 System.out.println(file.getParent()); //获取File对象的文件名 System.out.println(file.getName()); //获取File对象的全路径 System.out.println(file.getPath()); //获取File对象的绝对路径 System.out.println(file.getAbsoluteFile()); //获取File对象的修饰过的绝对路径 System.out.println(file.getCanonicalFile()); } }
🎉普通文件的创建、删除。
public class IODemo2 { public static void main(String[] args) throws IOException { //初始化的时候,这个文件目录的写法,是相对路径的一种写发,./通常可以省略 File file = new File("hello_world.txt"); //判断这个文件是否真实存在 System.out.println(file.exists()); //false //判断File对象代表的文件是否是一个目录 System.out.println(file.isDirectory()); //false //判断File对象代表的文件是否是一个普通文件 System.out.println(file.isFile()); //false //创建文件 file.createNewFile(); System.out.println(file.exists()); //true System.out.println(file.isDirectory()); //false System.out.println(file.isFile()); //true } }
❗❗❗注意:IEDA工作目录就是项目所在目录,写相对路径时,就是以system_code这一级为基准,来展开的。
🎉创建目录
public class IODemo3 { public static void main(String[] args) { File file = new File("text-dir/aaa/bbb"); //只能创建一级目录 file.mkdir(); //可以创建多级目录 file.mkdirs(); } }
🎉列出一个目录下包含那些内容
public class IODemo5 { public static void main(String[] args) { File file = new File("text-dir"); String[] results1 = file.list(); //数组的打印,需要使用数组的工具类调用toString方法进行打印,才能打印出数组当中的内容,要不然打印的是数组的hash值 System.out.println(Arrays.toString(results1)); File[] results2 = file.listFiles(); System.out.println(Arrays.toString(results2)); } }
🎉针对文件或目录重命名
public class IODemo6 { public static void main(String[] args) { File src = new File("./text-dir"); File dest = new File("./test222"); src.renameTo(dest); } }
3、文件内容操作
- 针对文本文件,提供了一组类,统称为"字符流"(典型代表,Reader,Writer),字符流读写的基本单位是字符(根据字符集来确定一个字符占几个字节),字符流每次读写最少是一个字符。
- 针对二进制文件,提供了一组类,统称为"字节流"(典型代表,InputStream,OutputStream),字节流读写的基本单位是字节(8个bit位),字节流每次读写最少是一个字节。
每种流对象,又分为两种
- 一种是用来输入的:Reader,InputStream
- 一种是用来输出的:Writer,OutputStream
3.1、字节流用来输入的抽象类InputStream的方法使用
InputStream只是一个抽象类,要使用这个类,需要实例化实现了这个抽象类的子类。InputStream类使用来进行输入的,它的实现类很多,基本上可以认为不同的输入设备都可以对应一个InputStream类,不仅仅是读写硬盘文件,还可以是读写网络编程,或者是读写网卡。本章博客只是针对硬盘文件的输入,所以这里我们只需要实例化FileInputStream类即可。
3.1.1、FileInputStream类的构造方法
public class IODemo7 {
public static void main(String[] args) throws IOException {
//这个过程就相当于文件打开操作
//让当前的inputStream变量和硬盘上的文件关联起来
InputStream inputStream = new FileInputStream("D:/test.txt");
//释放资源
//释放资源这部操作非常重要,不能省略
inputStream.close();
}
}
- 上述代码中的资源释放的操作非常重要,千万不要忘记!因为Java有垃圾回收机制,可以帮助我们将使用过的资源释放掉,但是在文件操作这里垃圾回收机制并不能帮我们把资源释放掉。需要我们手动释放。这里说的文件操作的时候需要释放资源,释放的资源主要是文件描述符表。
- 文件描述符表:记载了当前进程打开了那些文件,每次打开一个文件,就会在这个表里,申请到一个位置,这个表可以当成一个数组,数组下标就是文件描述符,数组元素就是这个文件在内核中的结构体的表示。但是这个表的长度是有限制的,不能无休止的打开文件,但是又不释放。一旦文件表述符表满了,继续打开就会打开失败,这就导致了文件资源泄露。
上述的这种写法虽然写到了将文件释放,但是这样写,在执行的时候,中间出现一些问题,比如return或者抛异常,就会导致close执行不到了。所以为了保证close一定被执行到,所以这里我们了解一下比较稳妥的写法。
1️⃣第一种写法:这种写法虽然可以保证一定执行到close,但是这种写法并不好看。
public class IODemo7 { public static void main(String[] args) throws IOException { InputStream inputStream = null; try{ inputStream = new FileInputStream("D:/test.txt"); } finally{ //释放资源 inputStream.close(); } } }
2️⃣第二种写法try with resources:带有资源的try操作,会在try代码块结束时,自动执行close关闭操作。
public class IODemo7 { public static void main(String[] args) throws IOException { try(InputStream inputStream = new FileInputStream("D:/test.txt")){ } } }
之所以上述的写法能够释放资源是因为InputStream实现了一个特定的接口Closeable
3.1.2、字节流读操作
InputStream类中提供了3种读操作的方法。
先来了解一下无参的read方法.
public class IODemo7 {
public static void main(String[] args) throws IOException {
try(InputStream inputStream = new FileInputStream("D:/test.txt")){
//读文件
//无参数的read,相当于一次读一个字节,但是返回类型是int,当读到末尾返回-1
while(true){
int b = inputStream.read();
if(b==-1){
//读到末尾了,结束循环
break;
}
System.out.println(b);
}
}
}
}
❓❓❓上述代码中read方法读取的是二进制文件中的内容,读取出来的数据应该是字节,应该使用byte来接收,为什么使用int类型的变量来接收??
❗❗❗因为源码当中的注释写到read返回的是一个0~255之间的数字,但是如果文件读取完毕,再次读取就会返回-1,这个时候-1没有在这个范围中,所以就要使用int来接收,但是说到这里有的人会说到short也可以,但是应为内存对齐的原因,short并没有int快。
✨再来看一下上述程序的执行结果与test.txt文件中的内容之间的关系。
1️⃣二进制文本中内容为英文
2️⃣如果test文件中将内容编辑为中文出现的结果会是什么(test文件中输入中文“世界你好”)
可以看到上述控制台上输出的10进制数和我们查到的不一样,这时因为我们查到的是把3个字节放在一起,搞了一个大10进制,和咱们控制台上3个字节分开打印是不一样的,我们通过控制它将汉字以16进制打印出来观察结果。
//按照16进制打印 System.out.printf("%x\n",b);
✨读操作的总结:
- read():无参数版本,返回该字节的内容
- read(byte[ ] buffer,int offset):这个版本,读到的内容放到参数buffer数组中,此时返回值是读到的字节数。
- read(byte[ ] buffer,int offset ,int length):这个版本也是读到buffer数组中,但是不是从头放,是从offset位置放,最多放length长度,返回值也是实际读的长度。
3.1.3、字节流写操作
可以看到写操作也有多种方法。这里我们来了解每次写一个的字符的方法。
代码示例
public class IODemo8 {
public static void main(String[] args) {
try(OutputStream outputStream = new FileOutputStream("d:/test.txt")){
outputStream.write(97);
outputStream.write(101);
outputStream.write(100);
}catch(IOException e){
e.printStackTrace();
}
}
}
写操作的执行结果我们要在对应的二进制文件中寻找。
✨总结:
- read和write还可以一次读写多个字节,使用byte[]来表示。
- read会尽可能把byte[]填满。如果读到文件内容末尾,返回-1
- write会把byte[]所有数据都写入文件
3.2、 字符流的读写操作
字符流文件读写操作和字节流核心逻辑一样,使用Reader,Writer这两个抽象类的子类的构造方法来打开文件(FileReader,FileWriter).read方法来读(一次读一个char或者char[]);write方法来写(一次写一个char或者char[]或者String),close释放资源。
public class IODemo9 {
public static void main(String[] args) {
try(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);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这个代码和上面的字节流读取代码逻辑基本相似,就是在打印的时候,将int类型的c强制类型转换为char类型,这样每次读取打印的内容就是字符。
4、文件操作的案例
遍历目录,在里面的文件内容中查找。
在你的电脑上,有很多目录,目录里面有很多文件,每个文件里有很多内容,假设某些文件中,包含"hello"关键词。下面的程序就是找出那些文件,是包含这个关键词的。
我们这里的实现方式是一个简单粗暴的方式:
- 先以递归的方式遍历目录。比如给定一个d:/去递归的把这里包含的所有文件都列出来。
- 每次找到一个文件,都打开,并读取文件内容(这个时候就得到一串字符)
- 再判定要查询的词,是否在上述文件内容中存在,如果存在,即为所求。
package io;
import java.io.*;
import java.util.Scanner;
public class IODemo10 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
//1.先让用户指定一个要搜索的根目录
System.out.println("请输入要扫描的根目录:");
//将用户输入的内容构造常File对象
File rootDir = new File(scanner.next());
//判断一下输入的路径名表示的文件是否为一个目录
if(!rootDir.isDirectory()){
System.out.println("输入有误,你输入的目录不存在!");
return;
}
//2.让用户输入一个要查的词
System.out.println("请输入要查询的词:");
String word = scanner.next();
//3.递归的进行目录或者文件的遍历
scanDir(rootDir,word);
}
private static void scanDir(File rootDir, String word) {
//列出当前rootDir中的内容,没有内容,直接递归结束
File[] files = rootDir.listFiles();//返回rootDir对象(d盘)下的所有文件
if(files == null){
//当前rootDir是一个控的目录,这里啥都没有。
//就没有必要在递归了,直接返回就行。
return;
}
//目录中有内容,就遍历目录中的每个元素
for(File f:files){//通过这个for循环遍历这个文件数组
//设置一个日志
System.out.println("当前搜索到:"+f.getAbsoluteFile());//打印文件的绝对路径
if(f.isFile()){
//判断是普通文件
//打开文件,读取内容,比较看是否包含上述关键词
String content = readFile(f);
//判断看读到的文件内容中是否包含关键字
if(content.contains(word)){
//文件中有要查中的关键字,则输出这个文件的绝对路径
System.out.println(f.getAbsoluteFile()+"包含要查找的关键字");
}
}else if(f.isDirectory()){
//判断是目录
scanDir(f,word);//此处的递归就是以但钱f这个目录作为起点,搜索t目录里面的内容。
}else{
//不是普通文件,也不是目录文件直接跳过,以Linux为例,文件种类有很多,普通文件,目录文件,管道文件...
continue;
}
}
}
//读取文件的整个内容,返回出来
private static String readFile(File f) {
//使用字符流来读取,由于咱们匹配的是字符串,此处只能按照字符流处理,才是有意义的。
StringBuilder stringBuilder = new StringBuilder();
try(Reader reader = new FileReader(f)){
//一次读一个字符,把读到的结果给拼装到StringBuffer中,同意转成String
while(true){
int c = reader.read();
if(c == -1){
break;
}
//再进行字符串拼接的时候,需要将刚刚读取到的c数据强转成char类型,c的类型本身是一个int,为了防止超出char的范围
stringBuilder.append((char)c);
}
} catch (IOException e) {
e.printStackTrace();
}
return stringBuilder.toString();
}
}