1. 文件
文件有狭义的文件和广义的文件:
- 狭义的文件 ------ 保存在硬盘上的文件
- 广义的文件 ------ 操作系统进行资源管理的一种机制,很多的软件/硬件资源。
这里我们要谈的是关于狭义的文件(file),println 和 scanner 都是针对文件的,一个是控制台的标准输出,一个是控制台的标准输入。
就像这样的,就是一个文件 :
而像这种,文件夹 也是一种文件
,文件夹是一种通俗的说法,也叫目录(directory),是专业术语。
针对硬盘 这种持久化存储的I/O 设备,当我们想要进行数据保存时, 往往不是保存成⼀个整体,而是独立成⼀个个的单位进行保存,这个独立的单位就被抽象成文件的概念,就类似办公桌上的⼀份份真实的文件⼀般。(我们的电脑一般都是 固态硬盘ssd)
文件除了有数据内容之外,还有⼀部分信息,例如文件名、文件类型、⽂件大小等并不作为文件的数据文存在,我们把这部分信息可以视为文件的元信息 。
1.1 树型结构和目录
一台计算机中,能够保存的文件是很多的(不允许直接操作硬盘,而是以文件的形式来间接操作),那么如何组织这些文件呢?
------ 就是按照层级结构进行组织,也就是我们数据结构中学习过的树形结构。这样,⼀种专门用来存放管理信息的特殊文件 诞生了,也就是我们平时所谓文件夹(folder)或者目录 (directory)的概念。

示例:想要找到这个文件
,它的树型结构(N叉树):

1.2 文件路径(Path)
从上述图中可知,从树根开始,到最终的目标文件,中间需要经过许多的目录/文件夹。
而把这些目录记录下来,就能构成路径。在文件系统中就是通过路径定位我们的⼀个唯⼀的文件。
一般使用**/ 斜杆** 来分割路径中的多级目录。在主流操作系统中,都是使用 / 来分割的,在Windows中,/ 斜杠和 \ 反斜杠 都支持,默认是 \ ,不过我们在表示路径时,还是建议写成 / ,因为**\ 在字符串中需要转义**。
路径又分为 绝对路径和相对路劲。
- 绝对路径:从盘符(根节点)开始,逐级表示出来,这种描述路径的方式就称为文件的绝对路径。
- D:\Java code\java\JavaEE\J260418\J260418.iml
- 相对路径:从任意节点出发,进行路径的描述,这种描述方式就被称为相对路径,相对于当前所在结点的⼀条路径。
- ./ J260418.iml 或者 **../**J260418.iml
- . 表示当前所在的目录位置/当前节点,.. 表示当前路径的上一层路径/当前节点父节点
关于相对路径,需要明确一个 "基准路径"。
- 假设基准为 D:\Java code\java\JavaEE\J260418,那么目标文件相对路径就是 ./ J260418.iml。
- 假设基准为 D:\Java code\java\JavaEE,那么目标文件的相对路径就是 ./ J260418/J260418.iml
- 假设基准为 D:\Java code\java\JavaEE\J260412 ,那么目标文件的相对路径就是 ../ J260418/J260418.iml
- 假设基准为 D:\Java code\java\JavaEE\J260412\src,那么目标文件的相对路径就是**../../** J260418/J260418.iml
如果在代码中写一个相对路径,那么基准路径是谁?
------ 在 idea 中直接运行,基准路径就是项目的目录:
也就是:D:\Java code\java\JavaEE\J260502

1.3 文件种类
根据文件保存的数据不同,一般划分为文本文件和二进制文件,分别指代保存被字符集编码的文本和按照标准格式保存的非被字符集编码过的文件。
这样划分有一个大前提,所有的文件都是二进制的文件 ,只不过有一些特殊的二进制文件,其中的数据根据编码方式(utf8mb4或者gbk等)刚好能构成字符(不仅仅是ASCII码),二进制的数据恰好都在码表中能够查到并且翻译过来的字符能够构成有意义的信息,就是文本文件。
如果先判断某个文件是否是文本文件,可以直接使用记事本打开。打开之后不是乱码,能看懂就是文本文件,否则为二进制文件。记事本就是在按照文本的方式来打开的,自动进行查码表并翻译。
图片,音频,视频,.docx(word文档)、可执行程序,这些都是二进制文件,txt纯文本、.java、.c 都是典型的文本文件。

2. Java中操作文件
Java标准库中提供了一系列的类操作文件。包含
- 1)文件系统操作:创建文件,删除文件,重命名,创建目录等
- 2)文件内容操作:针对一个文件中的内容进行读和写
2.1 File类
Java 中通过 java.io.File 类 来对⼀个文件(包括目录)进行抽象的描述,File类是用于操作文件系统的,也就是用于文件的创建/删除等。
注意:有 File 对象, 并不代表真实存在该文件。
File 类中的常见属性、构造方法和方法:
属性
|---------------|---------------|-----------------------------|
| 修饰符及类型 | 属性 | 说明 |
| static String | pathSeparator | 依赖于系统的路径分隔符,String类型的表示 |
| static char | pathSeparator | 依赖于系统的路径分隔符,char类型的表示 |
- pathSeparator,即 / ,路径中目录之间的分隔符。
构造方法
|-----------------------------------|------------------------------------------|
| 签名 | 说明 |
| File(File parent,String child) | 根据父目录+孩子文件路径,创建⼀个新的 File 实例 |
| File(String pathname) | 根据文件路径创建⼀个新的 File 实例,路径可以是绝对路径或者相对路径 |
| File(String parent, String child) | 根据父目录+孩子文件路径,创建⼀个新的 File 实 例,父目录用路径表示 |
方法
|------------|---------------------|--------------------------------------------------------|
| 修饰符及返回值类型 | 方法签名 | 说明 |
| String | getParent() | 返回File对象的父目录⽂件路径 |
| String | getName() | 返回FIle对象的纯⽂件名称 |
| String | getPath() | 返回File对象的⽂件路径,可能是绝对路径或者相对路径,取决于用户如何写 |
| String | getAbsolutePath() | 返回File对象的绝对路径 |
| String | getCanonicalPath() | 返回File对象的修饰过的绝对路径,会抛出一个IOException异常 |
| boolean | exists() | 判断File对象描述的⽂件是否真实存在 |
| boolean | isDirectory() | 判断File对象代表的文件是否是⼀个目录 |
| boolean | isFile() | 判断File对象代表的⽂件是否是⼀ 个普通⽂件 |
| boolean | createNewFile() | 根据File对象,自动创建⼀个空文件。成功创建后返回true |
| boolean | delete() | 根据File对象,删除该⽂件。成功 删除后返回true |
| void | deleteOnExit() | 根据File对象,标注⽂件将被删 除,删除动作会到JVM运⾏结束时 才会进行,即不立即删除,进程结束时删除。 |
| String[] | list() | 返回File对象代表的目录下的所有⽂件名 |
| File[] | listFiles() | 返回File对象代表的目录下的所有⽂件,以File对象表示 |
| boolean | mkdir() | 创建File对象代表的⽬录 |
| boolean | mkdirs() | 创建File对象代表的⽬录,如果必要,会创建中间⽬录 |
| boolean | renameTo(File dest) | 进行⽂件改名,也可以视为我们平时的剪切、粘贴操作 |
| boolean | canRead() | 判断用户是否对文件有可读权限 |
| boolean | canWrite() | 判断用户是否对文件有可写权限 |
示例1:获取 test.txt 文件的父目录,名字及路径:


注意 ,getPath() 方法,构造 File 对象时传入的是绝对路径,那么返回的就是绝对路径,相对路径一样。而getCanonicalFile() 方法,会抛出一个文件操作的异常 IOException,它的作用是对绝对路径进行修饰,即绝对路径的简化版本,把路径中间的 . 或者**..**去掉。
在上述的例子中不明显,可以看以下的例子:


示例2:查看文件是否存在,查看文件是一个普通文件还是目录:

结果都是 false 的原因很简单,因为 test.txt 它不存在,我们根本没有创建出这一个文件:

将其作为一个文件创建出来后再次运行,就能得到结果:

如果将其作为一个目录创建出来,再次运行就能得到不同的结果:注意正常目录名是没有后缀的,只是这个名字需要:

除了上述自己手动创建之外,还可以利用 createNewFile() 方法创建,它创建的是一个文件,同样会抛出 IOException 异常:


自动创建出了 test.txt 文件:

示例3:查看文件是否被删除
将上述创建的文件删除:
查看结果,确实已经删除掉了:

当我们再次运行起来,发现结果是 false,这是因为我们前面一次已经删除掉了文件,此时并没有存在的文件可以删除了:

如果 test.txt 文件不想被立即删除掉,而是等待进程结束再删除掉,那么使用 deleteOnExit() 方法(先将 test.txt 文件重新创建出来):

示例4:查看 File 对象代表的目录下所有的文件名(包含目录和普通文件),list() 是针对目录的,针对文件是无法进行list的

查看结果:


还有 listFile() 方法,该方法的效果是一样的,只是返回的结果以File对象表示的,而不是一个字符串对象,包含更多的操作:


示例5:创建目录,其中 mkdir() 创建目录,而 mkdirs() 创建多级目录

运行结果:


当然,再次运行的结果和文件的一样,该目录已经存在了,运行结果为 false。
创建 ./test/111/222/333 这样的多级目录:




示例6:修改目录的名字/重命名



从操作系统的角度来看,重命名和移动操作本质上是一样的 ,也就是说,renameTo 方法 除了可以重命名,也可以指定此目录的位置,即 移动/剪切 这个目录到别的位置:



这个移动/剪切 文件/目录的操作非常快,是O(1);如果要 复制 文件/目录,复杂度就是O(n),要遍历文件/目录中的数据,再写入一个新的 文件/目录。
2.2 数据流
Java中,针对文件内容操作,主要通过一组 "流对象" 来实现。
关于 流,可以把它看作 "水流",如果有 100ml 的水,有无数种接水的方式:
- 1次把100ml的水接完
- 分2次,一次接50ml的水
- ..................
而计算机中的 "流",和水流相似,从文件读取 100字节 的数据,有无数种读取数据的方式:
- 1次把100字节都读完
- 分2次,1次读50字节
- ..................

因此,计算机中针对读写文件,也是使用 "流"(Stream),Java中提供了一组类(有几十个),来表示流。针对这么多的类,分为两个大的类别:
- 字节流 :读写文件,以字节为单位,是针对二进制文件使用的。
- InputStream 类 表示 输入类
- OutputStream 类 表示 输出类
- 字符流:读写文件,以字符为单位,是针对文本文件使用的。
- Reader 类 表示 输入类
- Writer 类 表示 输出类
其他的类都是从这四个类中扩展出去的。
关于输入和输出,即读和写,以 CPU 为基准,数据的流向:
- 从 硬盘 到 CPU ------> 输入 (以CPU为基准,迎面而来的是输入/读)
- 从 CPU 到 硬盘 ------> 输出 (以CPU为基准,离去的是输出/写)
- 所以,输入就表示从文件中读数据;输出就表示从文件中写数据。
2.2.1 InputStream
字节流读方法:
|-----------|----------------------------------|-----------------------------------------------------------------------------------------------|
| 修饰符及返回值类型 | 方法签名 | 说明 |
| int | read() | 读取⼀个字节的数据,返回-1代表已经完全读完了, 会抛出 IOException 异常 |
| int | read(byte[] b) | 最多读取 b.length字节的数据到 b 中,即会尽可能把数组填满,最终返回的是实际读到的数量;-1 代表已经读完了, 会抛出 IOException 异常 |
| int | read(byte[] b,int off,int len) | 最多读取 len-off 字节的数据到 b 中,从 b的指定位置 off 开始放置,返回实际读到的数量;-1代表已经读完了, 会抛出 IOException 异常 |
| void | close() | 关闭字节流,会抛出 IOException 异常 |
- 上述的 read 方法中,调用一次,读取一个字节,但是返回的值类型却是 int ,而不是 byte,这是因为需要使用 -1 这样的整数来表示读取完毕



InputStream 是一个抽象类,不能直接进行实例化对象

需要借助它的实现类来完成实例化,关于 InputStream 的实现类有很多,基本可以认为不同的输⼊设备都可以对应⼀个 InputStream 类,我们现在只关心从文件中读取 ,所以使用 FileInputStream。

FileInputStream 构造方法:
|------------------------------|--------------------------------------------------------------------------|
| 签名 | 说明 |
| FileInputStream(File file) | 利用 File 构造文件输入流,会抛出FileNotFoundException 异常,如果文件不存在,就会抛出这个异常 |
| FileInputStream(String name) | 利用文件路径构造文件输入流,可以是绝对路径或者相对路径。会抛出FileNotFoundException 异常,如果文件不存在,就会抛出这个异常 |
这里的创建对象操作,一旦成功,就相当于 "打开文件",类似C语言的 fopen:

先打开文件,然后才读写,这是操作系统定义的流程,认为打开操作,就是根据文件路径定位到对应的硬盘空间,且读写完文件后,要手动关闭文件,即释放资源,防止 "文件资源泄露"。
- 打开了 IO 流 / 文件,用完不关闭,系统一直占着这个文件、内存、端口不放,即霸占系统资源不归还,这就叫文件资源泄露。

注意,FileNotFoundException 是 IOException 的子类,同时抛异常时,就会变成 IOException:
还有一点,就算我们记得打开文件,操作完文件之后关闭文件,但是最终可能执行不到 关闭文件的操作,因为:在打开和关闭文件之间,对于文件的操作,可能有代码逻辑 return 了,或者抛出了异常,那么程序就直接返回或者由于异常中断了,而无法关闭资源。
为了避免上述的问题,我们可以将文件内容操作的所有逻辑放在try-catch-finally代码块中,保证无论如何最终都能关闭资源。

除了 try-finally 的方式,还有另一个方式,而且更常用,就是 try-with-resources 语法,括号里放的是实现了 AutoCloseable 接口(或它的子接口 Closeable)的资源对象:
java
try(实现AutoCloseable或它的子接口Closeable的对象) {
}catch() {
}..................
放在 try() 里的资源,不管代码正常结束还是中途报错,JVM 都会自动调用AutoCloseable(或它的子接口 Closeable)的 close() 方法关闭资源 ,从根源上解决了文件 / 数据库 / 网络资源泄露的问题。
而 FileInputStream 就继承了 Closeable 接口 ,可以使用上述的语法:

打开文件,**read()**读文件内容,一次读一个字节:

运行起来后,发现抛出了文件不存在的异常,也就是说我们并没有 test.txt 文件:
现在将这个文件创建出来:

文件中的内容就是一个 hello:
此时再次运行程序,得到结果:

将 hello 按照字节一个个读取出来,分别打印,且每个字节的数据范围是0-255,我们可以利用上述打印出来的数据到码表中对应找到其对应的字符,由于hello是纯英文,那就去 ASCII码表中找对应的字符:
如果文件的内容是 "你好" 这两个汉字:
再次运行起来,发现这两个汉字分别对应了3个字节:
也就是说,是按照 UTF-8 编码方式编码的,因此可以去查 UTF-8码表去找对应的字符:

因此,我们将读取到的结果按照十六进制的方式打印:

得出的结果就可以对照上述的码表:
read(byte[] b) 一次读多个字节,读取到的数据放到参数 b 中
该读操作会尽可能把字节数组填满,如果填不满,能填几个就是几个,最终返回的结果是实际读到的字节个数,即实际往字节数组中填了几个字节。


在这个代码中,是使用参数直接作为方法的返回值 的,像data 这样的参数,就叫做 "输出型参数 ":


-
参数 data 数组本身充当了"输出"的角色------读取到的字节内容被直接填进了这个数组里,相当于通过参数把数据"返回"给调用方
-
真正的"产物":读取到的文件内容,存放在 b 这个参数数组 里。
-
方法签名上的返回值:只是一个 int,只是告诉你"读了多少个字节",即参数数组长度信息。
-
也就是说,数据是通过修改参数对象(数组)传出来的,不是通过 return 传出来的。这里返回的 int 只是一个附带信息,核心结果(读取的字节)是通过参数带出去的。
-
调用方是如何获取数据的:

-
并没有写 data = inputStream.read(...) 来获得一个包含数据的新数组,而是传一个空容器进去,方法把它装满。容器本身(data)就是调用方和 read 方法之间的"共享白板",方法通过修改这个参数的内容来传递核心结果,而不是通过 return 一个新对象。
-
这种模式就叫 输出参数,即 通过参数来"返回值"。
-
inputStream.read(data) 把读取的文件内容"返回"到了 data 这个参数里,int n 只是记录读了多少。
其实输出型参数,本质上还是语法上制约了发挥,在Java中,要求一个方法只能有一个返回值,如果希望返回多个数据,就只能通过参数来凑了,就像上述 read(byte[] b) 方法,就是希望同时返回 长度 和 内容/核心数组的。


如果字节数组设小一点的话,每次都可以填满,直到文件中没有数据了:


read(byte[] b,int off,int len) 指定从off偏移量开始读文件中的字节,读取最多len-off个,最终返回实际读取到的个数
2.2.2 OutputStream
字节流写方法:
|-----------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 修饰符及返回值类型 | 方法签名 | 说明 |
| void | write(int b) | 写入要给的字节数据,会抛出 IOException 异常 |
| void | write(byte[] b) | 将 b 这个字节数组中的数据全部写入 os 中,会抛出 IOException 异常 |
| int | write(byte[] b,int off,int len) | 将 b 这个字节数组中从 off 开始的数据写⼊ os 中,⼀共写 len 个,会抛出 IOException 异常 |
| void | close() | 关闭字节流,会抛出 IOException 异常 |
| void | flush() | 我们知道 I/O 的速度是很慢的,所以,大多的 OutputStream 为了减少设备操作的次数,在写数据的时候都会将数据先暂时写入内存的⼀个指定区域里,直到该区域 满了或者其他指定条件时才真正将 数据写入设备中,这个区域⼀般称 为缓冲区。但造成⼀个结果,就是我们写的数据,很可能会遗留⼀部分在缓冲区中。需要在最后或者合适的位置,调用 flush(刷新)操作,将数据刷到设备中。 |
和 InputStream 一样,OutputStream 也是一个接口,不能直接实例化对象:

需要借助它的实现类 FileOutputStream 来完成实例化。

FileOutputStream 构造方法
|----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 签名 | 说明 |
| FileOutputStream(File file) | 利用 File 构造文件输出流,会抛出FileNotFoundException 异常,如果文件不存在,就会抛出这个异常 |
| FileOutputStream(String name) | 利用文件路径构造文件输出流,可以是绝对路径或者相对路径。会抛出FileNotFoundException 异常,如果文件不存在,就会抛出这个异常 |
| FileOutputStream(String name,boolean append) | 利用文件路径构造文件输出流,通过 append 参数控制 追加(true)或覆盖(false) ,即是否在文件原有内容的基础上追加,还是覆盖原有的内容,只保留新写入的内容。会抛出 FileNotFoundException 异常,如果文件不存在,就会抛出这个异常。上述两个构造中,默认 append 是 false,即新写入的内容会覆盖之前文件中的内容。 |
write(int b) 一次写一个字节到文件中

此时我们并没有 output.txt 这个文件,但是当程序运行起来后,会发现这个文件被创建出来了,且我们写入的内容也在里面:


这说明,对于 OutputStream 来说,默认情况下会尝试创建不存在的文件。而且多次运行,发现 output.txt 文件中还是只有,abc 三个内容,这说明了默认情况下,是会清除上次的文件内容的,重新打开文件的一瞬间,上次的文件的内容就清空了。


如果想要在原有文件内容上进行追加,需要将 OutputStream 构造方法的 append 参数设置为 true,那么多次运行,就不会覆盖之前的文件内容:


write(byte[] b) 一次写若干个字节到文件中


2.2.3 Reader
字符流读方法:
|-----------|-------------------------------------|-------------------------------------------------------------------------------------------------------|
| 修饰符及返回值类型 | 方法签名 | 说明 |
| int | read() | 读取⼀个字符的数据,返回-1代表已经完全读完了, 会抛出 IOException 异常 |
| int | read(char[] cbuf) | 最多读取 cbuf.length字符的数据到 cbuf 中,即会尽可能把数组填满,最终返回的是实际读到的数量;-1 代表已经读完了, 会抛出 IOException 异常 |
| int | read(char[] cbuf,int off,int len) | 最多读取 len-off 字符的数据到 b 中,从 b的指定位置 off 开始放置,返回实际读到的数量;-1代表已经读完了, 会抛出 IOException 异常 |
| int | read(charBuffer cbuf) | charBuffer 相当于对 char[] 进行了封装 ,从缓冲区的当前位置 开始写入;返回实际读到的字符数量;-1 代表已经读完了,会抛出 IOException 异常。 |
| void | close() | 关闭字符流,会抛出 IOException 异常 |
- Reader.read() 返回 int 的原因与 InputStream.read() 类似:
- char 类型是无符号 16 位,范围 0~65535,无法表示负数 -1。
- 因此返回 int,用 0~65535 表示读取的字符,-1 表示已到达流末尾。
同样的 Reader 是一个抽象类,需要借助它的实现类 FileReader 来实例化对象:



FileReader 构造方法
|-----------------------------|--------------------------------------------------------------------------|
| 签名 | 说明 |
| FileReader(File file) | 利用 File 构造文件输入流,会抛出FileNotFoundException 异常,如果文件不存在,就会抛出这个异常 |
| FileReader(String fileName) | 利用文件路径构造文件输入流,可以是绝对路径或者相对路径。会抛出FileNotFoundException 异常,如果文件不存在,就会抛出这个异常 |
read() 一次读一个字符

查看运行结果,从文件中读取到了两个 字符char:

Java 的 char 类型是一个无符号 16 位整数,范围 0 ~ 65535,在内存中正好占2 个字节( Unicode编码方式**)。也就是说,在字符流这里一个字符是占2个字节,而我们在使用字节流读取时,一个字符是占3个字节。**
其实上述两种都是对的,字节流读到的是文件中的原始数据,在硬盘上保存文件的时候,"你好" 就是6个字节,是 utf8 编码;而字符流在读的时候,就会根据文件的内容编码格式进行解析,read() 一次,就会读到3个字节(按照 utf8 解析),返回的时候,针对3个字节进行转码,拿这3个字节查一下 utf8 码表,是'你' 汉字,之后 read() 把这个汉字在 Unicode 的编码值返回到 char 变量中,就变成 2 个字节了。
read(char[] cbuf) 一次读多个字符


2.2.4 Writer
字符流写方法:
|-----------|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 修饰符及返回值类型 | 方法签名 | 说明 |
| void | write(int c) | 写入要给的字符数据,会抛出 IOException 异常 |
| void | write(char[] cbuf) | 将 cbuf 这个字符数组中的数据全部写入 os 中,会抛出 IOException 异常 |
| int | write(char[] cbuf,int off,int len) | 将 cbuf 这个字符数组中从 off 开始的数据写⼊ os 中,⼀共写 len 个,会抛出 IOException 异常 |
| void | close() | 关闭字符流,会抛出 IOException 异常 |
| void | flush() | 我们知道 I/O 的速度是很慢的,所以,大多的 OutputStream 为了减少设备操作的次数,在写数据的时候都会将数据先暂时写入内存的⼀个指定区域里,直到该区域 满了或者其他指定条件时才真正将 数据写入设备中,这个区域⼀般称 为缓冲区。但造成⼀个结果,就是我们写的数据,很可能会遗留⼀部分在缓冲区中。需要在最后或者合适的位置,调用 flush(刷新)操作,将数据刷到设备中。 |
- write 方法的参数不止可以是字符,还可以是一个字符串,即可以写入一个字符串
Writer 是一个抽象类,需要借助它的实现类 FileWriter 来实例化对象:


FileWriter 构造方法
|----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 签名 | 说明 |
| FileWriter(File file) | 利用 File 构造文件输出流,会抛出FileNotFoundException 异常,如果文件不存在,就会抛出这个异常 |
| FileWriter(String name) | 利用文件路径构造文件输出流,可以是绝对路径或者相对路径。会抛出FileNotFoundException 异常,如果文件不存在,就会抛出这个异常 |
| FileWriter(String name,boolean append) | 利用文件路径构造文件输出流,通过 append 参数控制 追加(true)或覆盖(false) ,即是否在文件原有内容的基础上追加,还是覆盖原有的内容,只保留新写入的内容。会抛出 FileNotFoundException 异常,如果文件不存在,就会抛出这个异常。上述两个构造中,默认 append 是 false,即新写入的内容会覆盖之前文件中的内容。 |
write(int b/String str/....) 一次写一个字符到文件中


它与 OutputStream 一样,会创建一个文件,且默认会覆盖原有文件中的内容。


write(char[] cbuf) 一次写多个字符到文件中


到这里关于文件内容操作的内容就结束了。
3.总结
- 使用 FIle 类进行文件系统的操作,即创建/删除文件或者目录的操作。
- 使用流来进行文件内容的操作,即读和写。
- 流对象的使用流程:先打开,再读写,最后关闭
- 流对象的选择:先区分文件是文本文件还是二进制文件,然后区分是读还是写
关于缓冲区:通常是一段内存空间,用于提高程序效率。直接读写硬盘比较低效,因此有时候希望降低读写文件的次数,因此,在进行 IO 操作的时候,把要写的数据先放到缓冲区里临时存储,积攒一波,之后再一起写,或者读的时候,也不是一个个的读,是一次读一批数据到缓冲区中,再慢慢解析。
而当前 IO 流对象 read 和 write 方法,是属于直接读写文件,想要提高效率,就要手动创建一个缓冲区,减少 read 和 write 的次数,或者使用标准库提供的**"缓冲区流" BufferedStream** ,就是将 InputStream 之类的对象再套上一层,即变成 BufferedInputStream等。
示例:
4.练习
示例1:扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件
- 首先需要做的是输入指定要搜索的目录路径,保证输入该路径最终指向的是一个目录,否则直接返回;
- 然后输入指定关键字,表示在此目录下搜索到的要删除的普通文件的名字中包含这个关键字
- 接着有了上述的目录以及关键字,就可以开始对目录进行遍历扫描,在 scanDir() 方法中写这段逻辑:遍历寻找包含关键字的文件,每找到一个符合条件的文件,询问用户是否进行删除。也要保证此文件是一个普通文件而不是一个目录,如果发现符合条件的文件是一个目录,那么递归调用 scanDir()方法继续遍历当前的目录。
- 删除普通文件**deleteFile()**方法的逻辑:首先打印一下这个普通文件的完整路径/绝对路径,同时询问用户是否需要删除掉这个路径下的该文件。
java
public class Test {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要搜索的目录:");
String rootDir = scannner.next();
File rootFile = new File(rootDir);
if(!rootFile.isDirectory()) {
System.out.println("输入的不是目录!");
return;
}
System.out.println("请输入要删除的文件包含的关键字:");
String keyword = scanner.next();
//扫描当前目录,寻找要删除的目标文件
scanDir(rootFile,keyword);
}
private static void scanDir(File rootFile,String keyword) {
//1.列出当前目录的所有文件内容
File[] files = rootFile.listFile();
if(files == null) {
System.out.println("当前目录为空!");
return;
}
//2.遍历目录
for(File file : files) {
//3.判断当前文件是普通文件还是目录
if(file.isFile()) {
//4.如果是普通文件,则判断文件名是否包含关键字并选择是否删除
deleteFile(file,keyword);
}else {
//5.如果是目录,递归调用本方法继续遍历当前目录
scanDir(file,keyword);
}
}
}
private static void deleteFile(File file,String keyword) {
if(file.getName().contains(keyword)) {
System.out.println("发现目标文件:" + file.getAbsoluteFile() + "包含关键字,是否删除(y/n):");
Scanner scanner = new Scanner(System.in);
String input = scanner.next();
if(input.equalsIgnoreCase("y")) {
file.delete();
System.out.println("文件已删除!");
}
}
}
}
示例:要在 D:\database 该目录下,选择删除包含 "ab" 关键字的普通文件:

运行程序结果:

打开此目录,包含关键字 "ab" 的文件确实被删除了:

上述的递归遍历过程,其实就类似于二叉树遍历,递归遍历左子树和右子树;只不过此处不是二叉树,而是 N叉树,通过 for 循环,把每个 叉(目录) 都进行递归遍历。
示例2:进行普通文件的复制
复制就是把文件中每个字节都读出来,写入另一个文件中,即进行读和写操作。
- 首先需要知道 源文件 的路径以及 目标文件 的路径,要保证源文件一定存在且是一个文件,而目标文件,它可以不存在(写操作会自动创建出一个文件),但是它的目录必须存在,即它的路径必须存在。
- 上述的前提条件没问题后,开始复制文件的操作:就是将 源文件的内容 读出来InputStream,然后再将源文件中的内容 写入目标文件中OutputStream,主要是读到多少写多少。
java
public class Test {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入源文件的路径:");
String srcPath = scanner.next();
System.out.println("请输入目标文件的路径:");
String destPath = scanner.next();
File srcFile = new File(srcPath);
if(!srcFile.isFile()) {
System.out.println("源文件不存在或不是文件");
return;
}
File destFile = new File(destPath);
//目标文件可以不存在,但是它的目录必须存在
if(!destFile.getParentFile().isDirectory()) {
System.out.println("目标文件所在的目录不存在!");
return;
}
//开始复制文件操作
try(InputStream inputStream = new FileInputStream(srcFile);
OutputStream outputStream = new FileOutputStream(destFile)) {
while(true) {
//读取源文件的内容
byte[] buf = new byte[1024];
int data = inputStream.read(buf);
if(data == -1) {
break;
}
//将读取到的数据写入目标文件
//buf数组不一定被填满,按照实际读到的个数data写入目标文件中
outputStream.write(buf,0,data);
}
}catch (IOException e) {
throw new RuntimeException(e);
}
}
}
示例:源文件是一张图片,它的文件名为people.jpg,它的路径为 D:\Document\picture\people.jpg,要复制这张图片,即目标文件,它也在 D:\Document\picture 这个目录下,目标文件文件名为作为目标文件 people2.jpg:

运行程序结果为:


示例3:扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)
- 首先需要做的是输入指定要搜索的目录路径,保证输入该路径最终指向的是一个目录,否则直接返回;
- 然后输入指定关键字,表示要搜索该目录下,文件包含关键字或者是文件内容包含关键字的所有文件
- 接着开始扫描目录scanDir() ,遍历目录下的所有内容筛选出是普通文件的文件,然后进一步判断该文件是否包含关键字,或者该文件的内容是否包含关键字findFile();如果遍历的是一个目录,递归继续遍历。
- 关于 findFIle() 方法的逻辑:有两种情况:
- 如果是 文件/文件名 包含关键字,那么直接打印该文件,然后返回。
- 如果是 文件的内容 包含关键字,那么读取该文件的内容,然后借助 StringBuilder 类来存放 读到的内容,最后判断StringBuilder中是否包含关键字,如果包含,则打印该文件,然后返回。
- 也就是说,包含关键字的可能是文件名,或者文件名不包含但是内容包含。
java
public class Test {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要搜索的目录:");
String rootPath = scanner.next();
File rootFile = new File(rootPath);
if(!rootFile.isDirectory()) {
System.out.println("输入的路劲不是目录!");
return;
}
System.out.println("请输入要搜索的关键字:");
String keyword = scanner.next();
//扫描目录
scanDir(rootFile,keyword);
}
private static void scanDir(File rootFile,String keyword) {
//列出当前目录的所有内容
File[] files = rootFile.listFile();
if(files == null) {
System.out.println("当前目录为空!");
return;
}
//遍历目录下的所有文件
for(File file : files) {
if(file.isFile()) {
//是普通文件,找出所有包含关键字的文件或者文件的内容包含关键字的文件
findFile(file,keyword);
}else {
scanDir(file,keyword);
}
}
}
private static void findFile(File file,String keyword) {
//1.如果文件包含关键字,打印该文件
if(file.getName().contains(keyword)) {
System.out.println("找到目标文件:" + file.getAbsoluteFile());
return;
}
//2.如果是文件的内容包含关键字,打印该文件
//由于keyword是字符串,按照字符流的方法来处理,即读文件内容
StringBuilder stringBuilder = new StringBuilder();//接收文件内容
try(Reader reader = new FileReader(file)) {
while(true) {
char[] buf = new char[1024];
int data = reader.read(buf);
if(data == -1) {
break;
}
stringBuilder.append(buf);
}
}catch (IOException e) {
throw new RuntimeException(e);
}
//判断stringBuilder中是否包含关键字
if(stringBuilder.indexOf(keyword) >= 0) {
System.out.println("文件内容包含关键字:" + file.getAbsoluteFile());
}
}
}
示例:目录 D:\database 下,能够搜索到包含 "sun" 的文件:

程序运行结果:

如果是文件的内容包含关键字 "sun":

程序运行结果:
