💕"跑起来就有意义"💕
作者:Mylvzi
文章主要内容:文件IO讲解
一.与文件相关的基本概念
1.什么是文件
文件从广义上来说就是操作系统对其所持有的硬件设备和软件资源的抽象化表示,但是在日常生活中我们所提到的文件就是指硬盘上存储的数据
,文件I0是指文件的输入(input)和输出(output),实际上就是与硬盘之间的交互,文件的输入就是向硬盘中存储数据,文件的输出就是从硬盘中获取数据
2.什么是硬盘
硬盘就是存储数据的介质
硬盘可以分为两类,机械硬盘(HDD)和固态硬盘(SSD),机械硬盘最先被发明出来,它是由一个磁性的磁盘和一个磁头组成,通过磁头的移动来进行数据的输入与输出,由于磁头的移动也需要消耗资源,机械硬盘更适用于顺序读取的操作,而不适用于随机读取
固态硬盘更为先进一些,其内部不含有机械装置,而是集成程度很高的芯片,固态硬盘的访问速度比机械硬盘更快,更适用于随机读取的情形,效率更高,因而造价也要比机械硬盘更高
需要说明的一点是:尽管固态硬盘的读写速度远远大于机械硬盘,但是还是要比内存慢上不少
二.文件的管理方式
1.文件系统
文件就是硬盘数据的抽象化表示,一个硬盘上有很多的数据,进而就有很多的文件,文件是通过文件系统来进行管理的,其内部是通过N叉树这样的结构来进行文件的管理,树的每一个节点是目录(directory),即我们日常说的文件夹,(目录是更专业的叫法,程序员要有自己的修养~)
2.文件的路径
文件是通过树这种结构来进行管理的,文件的层次结构就代表着文件的路径,文件路径可以分为两类:
- 绝对路径
- 相对路径
绝对路径很容易理解,就是以"D:"或"C:"这样开头的管理方式,反应的是文件从根节点到所在节点的路径表示
相对路径与绝对路径不同,文件相对路径的确定需要先选定一个基准(pivot),根据这个基准文件来确定其他文件的位置,往往有以下两种表示方式:
- "."表示当前目录下
- "…"表示当前目录的上一层目录
比如在我的D盘中有一个名为"演示文本.txt"的文件
绝对路径:D:\新建文件夹\临时文件\演示文本.txt
相对路径:假设基准文件是"临时文件"这个目录,其相对路径可以表示为:“.\演示文本.txt”
3.文件的类型
从编程的角度来看,文件可以分为两类:
- 二进制文件(文件保存的内容是二进制数据,存储数据之中包含 不合法 的字符)
- 文本文件(文件保存的内容是字符串,存储的都是 合法 的字符)
合法字符是指可以通过字符集
/字符码表
查询到的合法字符,常见的字符集有GBK,UTF-8
那如何区分一个文件是二进制文件还是文本文件呢?也很简单,只需要将文件拖入到记事本中打开,如果显示的内容是乱码
,那就是二进制文件(因为二进制文件中有非法字符),如果显示的内容不是乱码
,那就是文本文件.实际上,记事本就是尝试以字符的形式进行文件的打开
我们日常使用的很多文件其实都是二进制型的,比如docx,ppt等
区分文件是二进制还是文本文件对于程序员来说还是很重要的!因为不同类型的文件其操作方式也是不同的(下面会讲到)
三.Java对文件的操作方式
Java对文件的操作方式主要分为两类:
- 对文件系统的操作 -->依赖于File类
- 对文件内容的操作 -->依赖于流对象
1.对文件系统的操作
Java通过File类来管理文件系统,涉及到文件的创建/删除/重命名/获取路径等方法,File类存在于java.io
包中
1.属性
pathSeparator 路径分隔符
// 字符串类型路径分隔符 "/" "\" public static final String pathSeparator
此处要说明一下,在Windows操作系统下,"/“或者”“都可以作为文件的路径分隔符,但是在Linux或"Mac"操作系统下只能使用”/“来作为路径分隔符,所以还是推荐使用”/"作为路径分隔符
2.构造方法
1.根据父目录和孩子文件路径创建File对象
private File(String child, File parent) { }
2.根据文件路径创建File对象(相对/绝对路径均可)
public File(String pathname) { }
3.根据父目录和孩子文件路径创建File对象(父目录使用文件路径表示)
public File(String parent, String child) { }
最常用的构造方法是第二种–通过文件的路径来创建File对象,进行文件系统的操作
举例:在D盘中新创建一个文件名为"test.txt"的文件
// 创建File对象 File file = new File("d:/test.txt");
3.常用方法
File类中有很多常用的方法,需要使用的时候直接查阅api文档即可,以下是几个常用的方法
// 创建File对象 File file = new File("d:/test.txt"); // 1.返回父目录的文件路径 输出d:\ System.out.println(file.getParent()); // 2.返回文件名 文件名 = 前缀 + 扩展名 System.out.println(file.getName()); // 3.获取文件路径 System.out.println(file.getPath());// 相对路径 System.out.println(file.getAbsolutePath());// 绝对路径(相对路径 拼接上 前面的路径) System.out.println(file.getCanonicalPath());// 对绝对路径的简化处理 // 4. System.out.println(file.exists());// 判断文件是否存在 true System.out.println(file.isDirectory());// 判断文件是否是一个目录 false System.out.println(file.isFile());// 判断是否是一个普通文件 true System.out.println(file.createNewFile());// 根据file对象创建一个空文件 创建成功返回false System.out.println(file.delete());// 删除file对象的文件 此时d盘中的test.txt被删除 删除成功返回true // 5.在进程结束之后再执行删除操作 file.deleteOnExit(); Thread.sleep(5000); System.out.println("进程结束!");
说明:
deleteOnExit()方法这种机制其实很常见,他的主要功能就是为了避免程序不正常结束带来数据损失.比如我们经常使用的office系列的软件,在我们进行编辑时,在桌面上其实有一个比较浅颜色的相同类型的文档,称为隐藏文档,隐藏文档的删除就是利用了deleteOnExit这种机制,当我们编辑完毕,点击保存之后隐藏文档就消失了.也就是编辑文档这个进程结束,隐藏文档就会立马消失,如果在中途突然断电,隐藏文档就不会被删除,再次开机的时候可以根据隐藏文档的内容继续编辑
// 根据File对象 列出包含目录(以字符串表示)并打印 File file = new File("d:/"); String[] ret = file.list();// 以字符串数组的形式返回 System.out.println(Arrays.toString(ret));// 打印 // 根据File对象 列出包含目录(以File对象的形式返回) File[] ret2 = file.listFiles();// System.out.println(Arrays.toString(ret2));
根据File对象创建目录
// 创建目录 创建成功返回true File file = new File("d:/java"); boolean ret = file.mkdir(); System.out.println(ret); // 连续创建目录(即使中间目录不存在) File file2 = new File("d:/python/aaa/bbb/ccc"); boolean ret2 = file2.mkdirs(); System.out.println(ret2);
改名 将一个已经存在的文件更名
// 改名 将java改为java2 File file1 = new File("d:/java"); File file2 = new File("d:/java2"); boolean ret = file1.renameTo(file2);// 参数必须是File对象 System.out.println(ret);
2.对文件内容的操作–流对象
一谈到流,我们会想到水流,水流流过桥洞生生不息,我们在这里谈的流对象其实是文件流,文件流就是由大量文件组成的一种集合,我们经常要读取大量的文件,文件从CPU中的输入与输出就类似于水流流入流出桥底
尽管在Java的标准库内部有很多的流对象,但是总的来说可以分为两类:
- 字节流
- 字符流
字节流,每次流动的最小单位是字节(byte),对应着二进制文件的读取
字符流,每次流动的最小单位是字符,对应着文本文件的读取
实际上.字符流其实是对字节流的进一步封装,一个字符对应多少个自己是取决于编码方式的,但无论是那中编码方式,每个字符都是由若干个字符决定的,字符流就是自动将固定范围的字节封装为一个字符,内部存在着一个自动查表的过程
说明:文件内容的输入与输出都是站在CPU的角度,文件从CPU中流出就叫做文件的输出,有文件流入到CPU之中,就是文件的输入.所以无论是哪种流对象,其核心方法都是与文件的输入与输出相关
字符流中的输入输出对象是Reader和Writer对象,字节流中的输入输出对象是InputStream和OutStream对象,这里只要掌握了一种流的一个方法,其他方法就都可以无师自通,下文重点讲解字符流中的Reader对象
1.字符流
研究字符流就是研究与字符流相关的两个输入输出对象Reader和Writer
1.Reader对象
Reader对象主要用于文本文件的读取操作,用于显示文件内容
创建Reader对象
// 创建Reader对象 Reader是一个抽象类不能直接实例化 需要其子类来创建 Reader reader = new FileReader("d:/test.txt");
注意:如果创建失败会抛出异常
read方法
在这里一共有四种参数类型的read方法,第三种不用关注
public int read() throws IOException { char cb[] = new char[1]; if (read(cb, 0, 1) == -1) return -1; else return cb[0]; }
当我们点进去read的源码之后,发现read方法的返回值竟然是int,他不是应该返回一个char类型的数据么?别急,让我们看看注释:
可见,这里之所以使用int来作为返回值,主要是为了能通过 返回 -1这种方式来作为文件读取的结束标志.
这里还有一个小的细节要注意,上文说到,一个字符对应多少个字节取决于字符集,unicode字符集对应的字符是两个字节,utf-8字符集对应的字符是是三个字节,但是0 - 65535只能最多表示两个字节的范围.不是三个字节就无法通过utf-8字符集来表示字符.
这里面其实涉及到java标准可对于对编码方式进行的优化,对于char类型的数据来说,其对应的字符集固定就是unicode字符集,如果是String类型的数据,对应的字符集就是utf-8字符集,主要原因还是在于通过utf-8能够更加容易得识别出连续的字符
char[] c = {'a','b','c'};// 采用unicode编码 String s = c.toString();// 采用utf-8编码
1.无参的read方法
// 创建Reader对象 Reader reader = new FileReader("d:/test.txt"); while (true) { int ret = reader.read(); if(ret == -1) {// 返回 -1 文件读取完毕 System.out.println("文件读取完毕"); break; } char c = (char) ret; System.out.println(c); }
2.一个参数的read方法
读取固定大小的字符,将字符存储到创建的数组cbuf之中,所以cbuf数组又被称为输出型参数,即需要我们提前准备好一个"盘子",读到字符就放到盘子里,直到读满
操作示例
// 创建Reader对象 text内部存储26个英文字母 Reader reader = new FileReader("d:/test.txt"); char[] cbuf = new char[13];// 参数表示cbuf一次最多读取的字符个数 while (true) { int c = reader.read(cbuf); if(c == -1) break; System.out.println(Arrays.toString(cbuf)); }
对于数据量很大的文件,我们就可以采用上述通过while循环的方式分多次读取,保证文件读取完毕.
第三种三个参数的read方法这里不做介绍,就是将cbuf中存储的数据从off位置开始,读取len那么长的范围,此方法不常用
close方法
close 文件的关闭
当我们使用完一个reader对象之后,必须要添加一个close方法来释放资源,否则会引发文件资源泄露 的问题
close方法本质上说释放的是文件描述附表,文件描述符表是PCB(进程控制块)中重要的一个属性,它反映的是进程持有硬件资源和打开文件的数量,打开的文件是通过顺序表这种数据结构来进行管理的.如果我们在使用完一个reader对象之后没有及时去close,那么对应的文件就会持续占有顺序表,但是顺序表的长度是有限的,久而久之就会满了,进而就会造成文件资源泄露,所以,close方法是一个必须要添加的方法,可以将其放到finally之中
Reader reader = new FileReader("d:/test.txt"); try { char[] cbuf = new char[13];// 参数表示cbuf一次最多读取的字符个数 while (true) { int c = reader.read(cbuf); if(c == -1) break; System.out.println(Arrays.toString(cbuf)); } }finally {// 将close放到finally代码块之中 reader.close();// 一定要记得释放资源 }
但这样也不是最"优雅"的结局方案,最优雅的解决方案是使用"try-with-resources"语法
try (Reader reader = new FileReader("d:/test.txt")) { char[] cbuf = new char[13];// 参数表示cbuf一次最多读取的字符个数 while (true) { int c = reader.read(cbuf); if (c == -1) break; System.out.println(Arrays.toString(cbuf)); } }
这样的语法就是保证()中的对象在try代码块中的代码执行完毕之后,自动执行()对象的close方法,注意,要想使用的这样的语法,必须保证()里的对象实现了Closeable接口
2.Writer对象
write方法
使用示例
try (Writer writer = new FileWriter("d:/test.txt")){ // 将test中的内容更改为"hello" writer.write("hello"); }
如果是默认的write方法,则会覆盖掉文件原先存在的内容,如果添加一个参数true,就代表在原有内容之后追加要添加的新内容
2.字节流的基本操作
重点掌握OutputStream类和InputStream类即可
InputStream的使用
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) { throw new RuntimeException(e); }
OutputStream的使用
try (OutputStream outputStream = new FileOutputStream("d:/test.txt",true)){ String s = "你好世界"; outputStream.write(s.getBytes());// 将字符串s转化为对应的编码写入到文件之中 } catch (IOException e) { throw new RuntimeException(e); }
三.流对象之间的转换
文件类型分为二进制文件和文本文件,不同的文件类型需要使用不同的流对象进行输出/输出,这两种流对象之间是可以进行转化的
// 已知文件是二进制文件 读取需要通过字节流进行读取 打印时通过字符打印 try (InputStream inputStream = new FileInputStream("d:/test.txt")) { // 将inputStream作为参数传入 表示scanner对象即将从这个文件中进行读取 Scanner scanner = new Scanner(inputStream); while(scanner.hasNext()) { String s = scanner.next(); System.out.println(s); } } catch (IOException e) { throw new RuntimeException(e); } ;
之前使用Scanner类传入的参数都是System.in,表示从键盘中读取数据,实际上System.in也是一种流对象
Scanner类构造方法的参数就代表要读取数据的位置,scanner不仅可以从键盘上读取数据,也可以从文件,网络中读取数据,是一个常用的方法
try (OutputStream outputStream = new FileOutputStream("d:/test.txt")){ // 已知文件是二进制文件 写入时需要通过字节流对象进行写入 但是不方便 转化为通过字符写入 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println("i love you"); printWriter.flush();// 冲刷缓冲区 将缓冲区中的内容冲刷到硬盘之中 } catch (IOException e) { throw new RuntimeException(e); }
说明:
- 将创建好的outputStream 对象作为参数传入到PrintWriter的构造方法之中,创建出一个PrintWriter对象,就可以通过PrintWriter进行数据的写入,可以使用print/println等方法
- 通过PrintWriter对象写入文件的内容并不会立即存入到文件之中,而是先被存储到内存中的一个特殊区域缓冲区,等一定时间之后再从缓冲区存入到硬盘之中,之所以这么做是因为存内存的速度要远远大于存硬盘的速度,先将一定量的数据存入到内存之中,再转移到硬盘,比读取一个数据就存入到硬盘之中要快的多,能大幅提高性能
四.文件操作系统的一个小应用
需求如下:
根据用户提供的指定目录进行扫描,判断目录下所包含的所有文件是否包含关键字word,如果包含,进行删除,如果不包含,则不删除
注意事项:
- 涉及到对文件系统的操作,应该使用File类
- 要扫描的目录中既包含普通文件又包含目录,如果是目录,要递归扫描其子文件,如果是普通文件,直接判断是否包含关键字即可
代码实现:
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.列出扫描目录下所包含的所有文件 此方法会返回File类型,操作会更加简便(可以利用File类中的所有数据) File[] files = rootPath.listFiles(); if(files == null) { return;// 扫描目录为空 直接返回 } // 依次扫描每个目录 for(File f : files) { // 加个日志 方便观察递归扫描的过程 System.out.println("正在扫描: " + f.getAbsolutePath()); if(f.isFile()) { // 普通文件 -->判断是否包含查询的关键词 checkFileContainsWord(f,word); }else { // 目录 递归扫描其子文件 scanDir(f,word); } } } private static void checkFileContainsWord(File f, String word) { if(!f.getName().contains(word)) { return;// 不包含要删除的关键词 } // 包含要删除的关键词 打印日志 并询问用户是否需要进行删除 System.out.println("正在扫描的文件为: " + f.getName() + ",是否要删除该文件?(Y/N)"); Scanner scanner = new Scanner(System.in); String ret = scanner.next(); if(ret.equals("Y") || ret.equals("y")) { // 执行删除操作 f.delete(); System.out.println("删除" + f.getAbsolutePath() + "成功"); }else { // 不执行删除操作 return; } }