文章目录
- 文件操作和IO
- 文件相关概念
- Java操作文件
- 文件系统操作
- 文件内容操作
- 字节流
- FileOutputStream
- FileInputStream
- 代码演示
- 字符流
- FileWriter
- FileReader
- 代码演示
- 缓冲流
- 转换流
- 案例练习
文件操作和IO
文件相关概念
文件 通常指的是包含用户数据的文件,如文本文件、图像文件、音频文件等。这些文件有具体的扩展名,存储实际的数据内容。
如下图:
上面描述的其实是狭义的文件,更广义的文件可以指代任何可以通过文件系统接口进行操作的资源(“一切皆文件”),在此描述下例如设备文件、管道、套接字等都被视为文件。
但是我们这里涉及的讨论主要还是针对狭义的文件。接下来,我们会介绍两个后续不断会提到的概念:目录 和 路径。
-
目录
- 概念:目录是文件系统的组织结构,用于存储文件和其他目录。目录其实就是文件夹。
- 目录是文件吗? 目录被视为文件,只不过它的内容是目录项而不是用户数据,目录实际上是一个包含目录项的特殊文件。
-
路径
-
概念:用于描述文件系统中从根目录或当前目录到某个特定文件或目录的地址。路径可以用来区分或识别文件,例如某个路径:
C:\Program Files\Java\jdk-17
-
分类:路径分为绝对路径 和 相对路径 两种类型。
-
绝对路径:从根目录开始到目标文件或目录的完整路径。
一个文件的绝对路径是唯一的,在大多数操作系统中,绝对路径总是以根目录(如Windows中的驱动器字母)开始。例如:
C:\Users\dell\text.txt
就是text.txt
文件的绝对路径 -
相对路径:相对路径是相对于当前工作目录的路径,它不从根目录开始。
准确来说,相对路径需要一个基准路径,只不过这个基准路径通常是当前工作目录的路径,正因为如此,相对路径是非唯一的。
-
-
举个例子,详细解释一下相对路径:C:\Users\dell\text.txt
中text.txt
的相对路径是什么,这要取决于基准路径的选择:
.
表示当前所在的目录位置;..
表示上一级目录
-
基准路径是
C:\Users\dell
:- 相对路径:
text.txt
或者.\text.txt
- 解释:从
C:\Users\dell
开始,text.txt
就在当前目录下
- 相对路径:
-
基准路径是
C:\Users
:- 相对路径:
dell\text.txt
或者.\dell\text.txt
- 解释:从
C:\Users
开始,需要进入dell
子目录才能找到text.txt
- 相对路径:
-
基准路径是
C:\
:- 相对路径:
Users\dell\text.txt
或者.\Users\dell\text.txt
- 解释:从
C:\
开始,需要依次进入Users
、dell
才能找到text.txt
- 相对路径:
-
基准路径是
C:\Users\dell\subdir
:- 相对路径:
..\text.txt
- 解释:从
C:\Users\dell\subdir
开始,需要返回上一级目录C:\Users\dell
才能找到text.txt
- 相对路径:
有关路径分隔符使用
/(斜杠)
还是\(反斜杠)
的问题:
/
是通用的,大多数操作系统都将它作为路径分隔符
\
是Windows操作系统特有的,并且通常是默认的(作者笔记本就是Windows操作系统,所以上面的介绍都采用了\
)。Windows。同时,Windows也是支持
/
。
为什么Windows会默认将
\
作为默认路径分隔符呢?这其实是一个历史遗留问题,Windows的前身是DOS,DOS采用
\
作为路径分隔符,所以这个习惯就被保留下来了。
我们平时描述路径时尽量还是使用
/(斜杠)
作为路径分隔符,因为这是通用的,同时在许多现代编程语言中\
作为转义字符,\\
才能表示反斜杠,这是不方便的。
从开发的角度看,文件可以简单分为 文本文件 和 二进制文件。
所有的文件最终都是以二进制形式存储的。在这些文件中,有些文件的二进制数据可以按照特定的字符编码标准(如ASCII、UTF-8、UTF-16等)被解释为字符。当二进制数据恰好符合这些字符编码标准时,这些文件被称为文本文件;剩下的文件中的二进制数据不能直接被解释为字符,或者即使能被解释为字符,这些字符也没有意义,这些文件就是所谓的二进制文件。
两者具体的对比:
文本文件 | 二进制文件 | |
---|---|---|
可读性 | 人类可读,可以直接用记事本查看和编辑 | 人类不可读,需要特定的程序或工具来解析 |
文件大小 | 通常较大,因为字符编码占用空间 | 通常较小,因为直接存储原始数据 |
效率 | 较低,需要更多的字节表示相同的信息 | 较高,因为减少了编码解码的步骤 |
用途 | 源代码文件(如.c 、.java )、日志文件(如.log )、配置文件(如.ini )等 |
编译后的文件(如.class 、.exe )、库文件(如.so )、数据文件(如.bin )等 |
Java操作文件
在介绍文件操作前,我们必须先简单理解几个概念及其它们的关系,这些概念包括:IO、流、
IO 是一个比较广泛的概念,指计算机的输入和输出操作。流 是处理数据传输的一种抽象机制,分为字节流 和 字符流。流实际上是对IO的进一步抽象。
- 字节流是用于处理二进制数据的流。字节流每次读取一个字节,适用于处理图片、音频等二进制文件。字符集不涉及字符编码转换,因此可以处理任何形式的二进制数据。
- 字符流是用于处理字符数据的流。每次读写一个字符(通常是Unicode字符),适用于处理文本文件。字符流自动处理字符编码的转换,使得处理文本数据十分方便。
字节流和字符流并不严格对应二进制文件和文本文件,而是根据数据的处理方式区分的。
例如,要复制一个文本文件,在这个场景下,我们并不关心文本文件的内容,因此直接使用字节流完成复制任务即可。
文件只是IO的一个应用领域,Java中有关文件的操作涉及到两个包java.io
和java.nio
,它们适合不同的应用场景,我们要介绍的是java.io
中的一些常用组件。
文件系统操作
文件系统操作指的是创建文件、删除文件、移动文件、对文件重命名等不涉及文件内容的操作。File
类是java.io
包中唯一代表磁盘文件本身的组件,它定义了一系列文件系统操作方法。因此这一部分围绕 File
类展开。
构造方法
常见构造方法:
构造方法 | 说明 |
---|---|
File(String pathname) | 根据文件路径创建一个File实例,路径可以是绝对路径,也可以是相对路径 |
File(String parent, String child) | 将父路径字符串和子路径字符串组合起来,创建一个File实例 |
File(File parent, String child) | 将抽象的父路径和子路径字符串组合,创建一个File实例 |
- 通过上述构造方法只是实例化了一个代表某个文件的File实例,并不会创建文件。 实例化File对象时,这个文件可以存在也可以不存在
//绝对路径创建File实例
File file1 = new File("C:/Demo/test1.txt");
//相对路径创建File实例
File file2 = new File("./test2.txt");
- 根据相对路径创建的File实例默认选中当前工作目录,当前工作目录是由启动程序的方式决定的。 例如,如果采用IDEA启动,后续通过代码创建的文件会在项目目录中。
常用方法
方法 | 说明 |
---|---|
String getParent() | 返回 File 对象的父目录文件路径 |
String getName() | 返回 File 对象的纯文件名词 |
String getPath() | 返回 File 对象的文件路径 |
String getAbsolutePath() | 返回 File 对象的绝对路径 |
String getCanonicalPath() | 返回 File 对象的修饰过的路径 |
——————————(分隔线) | |
boolean exists() | 判断 File 对象描述的文件是否真实存在 |
boolean isDirectory() | 判断 File 对象代表的文件是否是一个已存在目录 |
boolean isFile() | 判断 File 对象代表的文件是否是一个已存在的普通文件 |
boolean createNewFile() | 根据 File 对象,创建一个文件。成功创建后返回true ,如果文件已经存在,返回false
|
boolean delete() | 根据 File 对象,删除文件或空目录。如果目录中包含文件或其他子目录,delete() 方法将返回 false ,表示删除操作失败。 |
void deleteOnExit() | 根据 File 对象,标注文件将被删除,删除行为会在JVM运行结束时进行 |
File createTempFile(String prefix, String suffix) | 根据prefix 前缀字符串和suffix 后缀字符串创建一个新的空文件,该文件在默认临时文件目录中。需要手动删除 |
——————————(分隔线) | |
String[] list() | 返回 File 对象代表的目录下的所有文件名,如果代表的不是目录,返回null
|
String[] list(FilenameFilter filter) | 返回指定目录中符合 FilenameFilter 过滤条件的所有文件名。 |
File[] listFiles() | 返回 File 对象代表的目录下的所有文件的File对象,如果代表的不是目录,返回null
|
File[] listFiles(FilenameFilter filter) | 返回指定目录中符合 FilenameFilter 过滤条件的所有文件的File对象。 |
boolean mkdir() | 创建 File 对象代表的目录,如果代表的不是目录,返回false ,代表创建失败 |
boolean mkdirs() | 创建 File 对象代表的目录,如果必要,会创建中间目录 |
boolean renameTo(File dest) | 对 File 对象代表的文件重命名,dest 代表新的文件或目录的 File 对象,类似于剪切、粘贴操作 |
boolean canRead() | 判断用户是否对文件有可读权限 |
boolean canWrite() | 判断用户是否对文件有可写权限 |
- 如表格演示,将常用方法分成三部分,演示时也会分成三部分。
【第一部分演示】
public class Demo5 {
public static void main(String[] args) throws IOException {
File file1 = new File("C:/Demo/test1.txt");
File file2 = new File("./test2.txt");
System.out.println("fil1的打印演示:");
System.out.println(file1.getParent());
System.out.println(file1.getName());
System.out.println(file1.getPath());
System.out.println(file1.getAbsolutePath());
System.out.println(file1.getCanonicalPath());
System.out.println();
System.out.println("file2的打印演示:");
System.out.println(file2.getParent());
System.out.println(file2.getName());
System.out.println(file2.getPath());
System.out.println(file2.getAbsolutePath());
System.out.println(file2.getCanonicalPath());
}
}
- 通过打印结果可以看出:
-
getParent()
返回的父路径形式与构造File对象时的参数有关,如file2的返回值是.
-
getName()
返回的就是文件名 -
getPath()
返回的结果也与构造File对象的参数有关 -
getAbsolutePath()
返回绝对路径,但仍可能存在符号链接、相对路径以及.
、..
等 -
getCanonicalPath()
返回规范路径,消除了符号链接、相对路径、.
、..
等,同时如果文件不存在或路径无效,可能会抛出IOException
异常。
-
【第二部分演示】
public class Demo6 {
public static void main(String[] args) throws IOException {
File file = new File("C:/Demo/test.txt");
//判断是否真实存在
System.out.println(file.exists());
//创建出来
file.createNewFile();
//判断类型
System.out.println(file.isFile());
System.out.println(file.isDirectory());
//删除并判断是否成功
file.delete();
System.out.println(file.exists());
}
}
delete()
方法只能删除普通文件和空目录,如果不是空目录,将无法删除。 如果想要删除一个非空目录,必须递归逐个删除。
private static void deleteDir(File dir) {
if(!dir.isDirectory()) {
return;
}
File[] files = dir.listFiles();
//空目录
if(files == null) {
dir.delete();
}else {
for(int i = 0; i < files.length; i++) {
if(files[i].isFile()) {
files[i].delete();
}else {
deleteDir(files[i]);
}
}
}
//删除自己
dir.delete();
}
deleteOnExit()
方法 和 createTempFile(String prefix, String suffix)
通常配合使用,以确保临时文件在程序退出时自动删除。
public class Demo8 {
public static void main(String[] args) throws IOException {
//创建临时文件
File tempFile = File.createTempFile("test", ".txt");
//查看是否创建成功
System.out.println(tempFile.exists());
//设置JVM运行结束后删除
tempFile.deleteOnExit();
//判断JVM结束前,deleteOnExit()后,是否存在
System.out.println(tempFile.exists());
}
}
【第三部分演示】
list()
、listFiles()
以及它们的重载版本都不会递归地返回子目录的内容,只返回当前目录下的文件和子目录的列表。
list()
和listFiles()
的带参数的重载版本list(FilenameFilter filter)
和listFiles(FilenameFilter filter)
中的FilenameFilter
是一个函数式接口,称为文件过滤器。只有一个抽象方法boolean accept(File dir, String name)
,用来指定文件名的过滤规则,符合条件的(规则下返回true
)文件名将被保留,不符合过滤条件的(规则下返回false
)的文件将被过滤掉,带FilenameFilter
文件过滤器类型参数的list()
或listFiles()
方法最终会返回符合过滤条件的所有文件名或者文件的File
对象。
public class Demo9 {
public static void main(String[] args) {
File file = new File("C:/Demo");
//使用没有文件过滤器的list方法返回C:/Demo目录下的所有文件名
String[] files1 = file.list();
//使用带有文件过滤器的list方法过滤出名字包含"test"的文件名
String[] files2 = file.list((dir, name) -> name.contains("test"));
//打印查看效果
System.out.println(Arrays.toString(files1));
System.out.println(Arrays.toString(files2));
}
}
前面的删除非空目录的代码中其实已经包含了遍历目录中所有文件的操作,就是利用listFiles()
拿到所有的文件对象数组,然后遍历对象数组,如果是目录,就递归遍历这个目录,整体就是一个递归方法,这里不再演示。
public class Demo10 {
public static void main(String[] args) {
File dir = new File("C:/Demo/newSubDemo1");//这个File实例代表的文件的中间目录Demo已经存在
File dirs = new File("C:/Demo/newSubDemo2/newSubDemo3");//这个File实例代表的文件的中间目录newSubDemo2不存在
System.out.println("Demo目录是否存在:" + dir.getParentFile().exists());
System.out.println("newSubDemo2目录是否存在:" + dirs.getParentFile().exists());
System.out.println("newSubDemo1目录是否存在:" + dir.exists());
System.out.println("newSubDemo3目录是否存在:" + dirs.exists());
System.out.println("使用mkdir创建中间目录均存在的目录,是否成功:" + dir.mkdir());
System.out.println("使用mkdir创建中间目录不存在的目录,是否成功:" + dirs.mkdir());
System.out.println("使用mkdirs创建中间目录不存在的目录,是否成功:" + dirs.mkdirs());
}
}
- 上述代码验证了:
mkdir()
和mkdirs()
方法均能创建目录,但mkdir()
方法不能创建中间目录不存在的目录,而mkdirs()
方法在必要时能够创建中间目录。
public class Demo11 {
public static void main(String[] args) {
File srcFile = new File("C:/Demo/demo1.txt");
File dstFile = new File("C:/Demo/newDemo1.txt");
System.out.println(srcFile.exists());
System.out.println(dstFile.exists());
System.out.println(srcFile.renameTo(dstFile));
}
}
-
renameTo(File dest)
方法的调用者代表的文件 和 参数代表的文件的关系:调用者:表示要被重命名的源文件或目录。
参数:表示新的文件名或者路径。
-
如果调用者代表的文件不存在,则方法直接返回
false
-
如果参数代表的文件已经存在,在Windows系统下不会覆盖已有文件,即方法返回
false
-
明确
renameTo()
方法不只是重命名,它有类似于剪切粘贴的功能
文件内容操作
文件内容操作即对文件的内容进行操作,包括读写文件。对文件内容的操作是通过流实现的,可以通过字节流 或 字符流操作文件内容,同时使用缓冲流提高读写效率,还可以使用转换流实现字节流和字符流的转换。因此我们将讨论java.io
中与这四种流相关的组件。
在开始介绍前,我们得先理解什么是输入什么是输出,输入和输出是站在CPU的角度考虑的,而不是站在我们的角度,因此:
- 读文件是输入
- 写文件是输出
字节流
InputStream
和 OutputStream
类是 java.io
包中与字节流相关的所有组件的基类。这两个类是抽象类,提供了处理字节流的基本方法和框架。与文件相关的字节流类主要是 FileInputStream
和 FileOutputStream
。这两个类分别用于从文件中读取字节和将字节写入文件。
为了方便演示,我们会在介绍完FileInputStream
和FileOutputStream
后一起演示。
FileOutputStream
常用构造方法
构造方法 | 说明 |
---|---|
FileOutputStream(String name) | 根据字符串构造,默认的覆盖写入模式 |
FileOutputStream(String name, boolean append) | 根据字符串构造,可以指定模式:true (追加模式);false (覆盖写入模式) |
FileOutputStream(File file) | 根据File对象构造,默认的覆盖写入模式 |
FileOutputStream(File file, boolean append) | 根据File对象构造,可以指定模式:true (追加模式);false (覆盖写入模式) |
- 如果用于构造
FileOutputStream
的字符串或File
对象所指向的文件不存在,那么在第一次尝试写入时,Java 会自动创建这个文件。 - 如果文件已经存在,并且是覆盖写入模式,那么文件中的原有内容将被删除,新的内容将从头开始写入;如果是追加模式,那么新写入的数据将会被添加到文件的末尾,原有的内容将被保留。
- 如果试图使用
FileOutputStream
写入一个目录而不是一个文件,Java 将抛出一个FileNotFoundException
异常。
常用方法
方法 | 说明 |
---|---|
void write(int b) | 写入最低的8位(即最低的一个字节) |
void write(byte b[]) | 将 b 字节数组中的数据全部写入 |
void write(byte b[], int off, intlen) | 将 b 字节数组中从 off 开始的 len 个数据写入 |
void close() | 关闭字节流 |
void flush() | 立即刷新输出流,将缓冲区中的数据立即写入 |
-
完成文件操作后一定要调用
close()
方法释放资源,否则可能会出现文件资源泄露的问题。 -
实际情况中很少使用
flush()
方法,flush()
方法一般在close()
时自动调用。 -
FileOutputStream
类(包括后面的FileInputStream
等类)代表字节流,但为什么write(int b)
需要一个int
类型的参数?-
表示范围和返回值
采用
int
类型可以确保方法能够处理更大的范围,尽管write(int b)
只会写入最低位的一个字节。同时,某些情况下write
方法可能需要返回值来表示写入操作的结果,尽管实际上write
方法没有返回值,但这种设计为未来的扩展留下了空间 -
历史遗留
Java的I/O流设计借鉴了C语言的I/O库。C语言习惯使用
int
类型。
-
-
write(byte b[])
比write(int b)
更常用,因为前者的字节数组一次可以写入多个字节,减少了系统调用次数,提高了写入效率;字节数组作为缓存,批量写入数据,减少了文件I/O次数,提高了I/O操作的性能;处理更复杂的数据,例如从网络接收的数据通常都是以字节数组的方式存在的。
浅谈文件资源泄露问题:
每个进程都会有一个PCB来描述进程中的某些属性,其中就包含文件描述符表。每当打开一个文件,都会申请一个表项,如果我们打开了大量的文件但是不关闭释放资源时,文件描述符表就会爆满进而发生错误,即出现了严重的文件资源泄露问题。
tip: 文件描述符表不会自动扩容。这是因为:
- 操作系统有资源限制,不允许单个进程申请过多的资源。
- 文件描述符表是操作系统内核的一部分,内核空间的内存管理十分严格,不允许随意扩展。
- 另外,如果允许一个文件描述符表过大,那么整个表的查找和管理性能就会大打折扣。
FileInputStream
常用构造方法
构造方法 | 说明 |
---|---|
FileInputStream(String name) | 根据文件路径构造文件输入流 |
FileInputStream(File file) | 根据 File 对象构造文件输入流 |
- 如果所指向的文件不存在 或者 文件存在但是一个目录,Java将会抛出
FileNotFoundException
异常
常用方法
方法 | 说明 |
---|---|
int read() | 从输入流中读取一个字节的数据,返回 -1 表示读完 |
int read(byte b[]) | 从输入流中最多读取 b.length 个字节的数据到字节数组b中,返回实际读取的数量, -1 代表读完 |
int read(byte b[], int off, int len) | 从输入流中读取最多 len 个字节的数据,读到的数据存放到字符数组b中,从off位置开始存,方法返回实际读取的数量,-1 代表读完 |
void close() | 关闭字节流 |
-
有参数的
read
方法读到的数据都存放在参数byte b[]
字符数组中,即b
数组既作为参数,又用于“返回值”,这种参数我们称之为 输出型参数。之所以可以这么做,是因为数组类型实际上是一个引用类型。可以这么理解输出型参数:字符数组想象成一个饭盒,
read
方法想象成餐厅,我们将饭盒给餐厅(打饭阿姨),餐厅就会还给我们一个盛满饭的饭盒。 -
一次读取多个字节的
read
方法会更常用,原因是字节数组可以作为缓存数组,同上介绍。 -
读操作执行完毕后及时
close()
代码演示
Java 7引入了
try-with-resources
语法,旨在简化资源管理,特别是对于那些必须显式关闭以防止资源泄漏的对象,如我们接下来要讲的文件输入输出流。
try-with-resources
语法特性:
使用方法:
try
后的括号内实例化资源对象,仍可以使用catch
捕获异常以及finally
语句。同时,try
括号内可以声明多个资源,每个资源之间用分号隔开。try (ResourceType resource = new ResourceType()) { // 使用资源的代码 } catch (ExceptionType1 e1) { // 处理异常 } finally { // 可选的finally块 }
使用条件:
- 资源对象必须在
try
括号内初始化(创建)- 必须实现
AutoCloseable
接口(意味着包括所有实现了Closeable
接口的类)使用优势:自动关闭资源而不需要手动调用
close
方法,资源(对象)会在try
代码块结束时自动关闭,不论是否发生异常。这减少了由于关闭资源而造成的潜在错误或内存泄漏风险,使得程序员可以更专注于业务逻辑。
public class Demo14 {
public static void main(String[] args) throws FileNotFoundException {
//写文件
try(FileOutputStream outputStream = new FileOutputStream("D:/DemoFile/INNER/demo.txt")) {
outputStream.write(97);
outputStream.write(98);
outputStream.write('B');
outputStream.write(new byte[]{'a', 'b', 'c'});
outputStream.write('难');
} catch (IOException e) {
e.printStackTrace();
}
//读文件
try(FileInputStream inputStream = new FileInputStream("D:/DemoFile/INNER/demo.txt")) {
while(true) {
byte[] buf = new byte[1024]