一:认识文件
针对硬盘这种持久化存储的 I / O 设备,当我们想要进行数据保存时,往往不是保存成一个整体,而是独立成一个个的单位进行保存,这个独立的单位就被抽象成文件的概念,就类似办公桌上的一份份真实的文件一般。
文件除了有数据内容之外,还有一部分信息,例如文件名、文件类型、文件大小等并不作为文件的数据而存在,我们把这部分信息可以视为文件的元信息。
同时,随着文件越来越多,我们就需要对文件进行管理,如何进行文件的组织呢,一种合乎自然的想法出现了,就是按照层级结构进行组织 —— 也就是我们数据结构中学习过的树形结构。这样,一种专门用来存放管理信息的特殊文件诞生了,也就是我们平时所谓文件夹或者目录的概念。
1.1 文件路径
如何在文件系统中如何定位我们的一个唯一的文件就成为当前要解决的问题,这其实很简单,因为从树型结构的角度来看,树中的每个结点都可以被一条从根开始,一直到达的结点的路径所描述,而这种描述方式就被称为文件的绝对路径。
除了可以从根开始进行路径的描述,我们还可以从任意结点出发进行路径的描述,而这种描述方式就被称为相对路径,相对于当前所在结点的一条路径。
文件的相对路径是以工作目录为基准的,每个程序运行的时候都有一个工作目录,IDEA 的工作目录默认为当前项目所在的目录
1.2 文件的分类
文件根据其保存数据的不同,被分为不同的类型,我们一般简单的划分为文本文件和二进制文件,分别指代保存被字符集编码的文本和按照标准格式保存的非被字符集编码过的文件。
1.3 文件管理
Windows 操作系统上,会按照文件名中的后缀来确定文件类型以及该类型文件的默认打开程序。但这个习俗并不是通用的,在 OSX、Unix、Linux 等操作系统上,就没有这样的习惯,一般不对文件类型做如此精确地分类。
文件由于被操作系统进行了管理,所以根据不同的用户,会赋予用户不同的对待该文件的权限,一般地可以认为有可读、可写、可执行权限。
Windows 操作系统上,还有一类文件比较特殊,就是平时我们看到的快捷方式,这种文件只是对真实文件的一种引用而已。其他操作系统上也有类似的概念,例如软链接等。
最后,很多操作系统为了实现接口的统一性,将所有的 I / O 设备都抽象成了文件的概念,使用这一理念最为知名的就是 Unix、Linux 操作系统 —— 万物皆文件。
二: Java 中操作文件
Java 中通过 java.io.File 类来对一个文件进行抽象的描述。注意,有 File 对象,并不代表真实存在该文件,下面我们来看看 File 类中的常见属性、构造方法和方法:
- 属性
修饰符及类型 | 属性 | 说明 |
---|---|---|
static | String | pathSeparator ( 路径分隔字符串 ) |
static | char | pathSeparatorChar ( 路径分隔字符 ) |
- 构造方法
方法签名 | 说明 |
---|---|
File(File parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例 |
File(String pathname) | 根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者相对路径 |
File(String parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例,父目录用路径表示 |
import java.io.File;
public class FileConstructorExample {
public static void main(String[] args) {
// 使用父目录 + 孩子文件路径创建一个新的 File 实例
File file1 = new File("C:/myfolder", "myfile.txt"); // C:/myfolder/myfile.txt
// 使用文件路径创建一个新的 File 实例
File file2 = new File("C:/myfolder/myfile.txt"); // C:/myfolder/myfile.txt
// 使用父目录 + 孩子文件路径创建一个新的 File 实例,父目录用路径表示
File parentDir = new File("C:/myfolder");
File file3 = new File(parentDir, "myfile.txt"); // C:/myfolder/myfile.txt
System.out.println("file1 路径:" + file1.getPath());
System.out.println("file2 路径:" + file2.getPath());
System.out.println("file3 路径:" + file3.getPath());
}
}
- 常用方法:
修饰符及返回值类型 | 方法签名 | 说明 |
---|---|---|
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 |
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() | 判断用户是否对文件有可写权限 |
以下是这些方法的使用示例代码:
import java.io.File;
import java.io.IOException;
public class FileExample {
public static void main(String[] args) {
// 创建 File 对象
File file = new File("C:/Users/86198/Desktop/test/test1.txt");
// 输出文件的父目录路径
System.out.println("父目录路径: " + file.getParent()); // 父目录路径: C:/Users/86198/Desktop/test
// 输出文件的名称
System.out.println("文件名称: " + file.getName()); // 文件名称: test1.txt
// 输出文件的路径
System.out.println("文件路径: " + file.getPath()); // 文件路径: C:/Users/86198/Desktop/test/test1.txt
// 输出文件的绝对路径
System.out.println("绝对路径: " + file.getAbsolutePath()); // 绝对路径: C:/Users/86198/Desktop/test/test1.txt
// 输出文件的修饰绝对路径
try {
System.out.println("规范路径: " + file.getCanonicalPath()); // 规范路径: C:/Users/86198/Desktop/test/test1.txt
} catch (IOException e) {
e.printStackTrace();
}
// 判断文件是否存在
System.out.println("文件是否存在: " + file.exists()); // 文件是否存在: true
// 判断是否是目录
System.out.println("是否是目录: " + file.isDirectory()); // 是否是目录: false
// 判断是否是普通文件
System.out.println("是否是普通文件: " + file.isFile()); // 是否是普通文件: true
// 判断是否可读
System.out.println("是否可读: " + file.canRead()); // 是否可读: true
// 判断是否可写
System.out.println("是否可写: " + file.canWrite()); // 是否可写: true
// 创建一个新文件
File newFile = new File("C:/Users/86198/Desktop/test/newFile.txt");
try {
if (newFile.createNewFile()) {
System.out.println("新文件已创建: " + newFile.getPath()); // 新文件已创建: C:/Users/86198/Desktop/test/newFile.txt
} else {
System.out.println("新文件创建失败,文件已存在: " + newFile.getPath()); // 新文件创建失败,文件已存在: C:/Users/86198/Desktop/test/newFile.txt
}
} catch (IOException e) {
e.printStackTrace();
}
// 删除文件
if (newFile.delete()) {
System.out.println("新文件已删除: " + newFile.getPath()); // 新文件已删除: C:/Users/86198/Desktop/test/newFile.txt
} else {
System.out.println("新文件删除失败: " + newFile.getPath()); // 新文件删除失败: C:/Users/86198/Desktop/test/newFile.txt
}
// 列出当前目录下的所有文件名
File dir = new File("C:/Users/86198/Desktop/test");
if (dir.isDirectory()) {
System.out.println("目录中的文件名列表: "); // 目录中的文件名列表:
String[] fileList = dir.list();
if (fileList != null) {
for (String fileName : fileList) {
System.out.println(fileName); // test1.txt, test2.txt 等
}
}
}
// 列出当前目录下的所有文件对象
System.out.println("目录中的文件对象列表: "); // 目录中的文件对象列表:
File[] fileArray = dir.listFiles();
if (fileArray != null) {
for (File f : fileArray) {
System.out.println(f.getPath()); // C:/Users/86198/Desktop/test/test1.txt 等
}
}
// 创建一个目录
File newDir = new File("C:/Users/86198/Desktop/test/newDir");
if (newDir.mkdir()) {
System.out.println("目录已创建: " + newDir.getPath()); // 目录已创建: C:/Users/86198/Desktop/test/newDir
} else {
System.out.println("目录创建失败: " + newDir.getPath()); // 目录创建失败: C:/Users/86198/Desktop/test/newDir
}
// 删除新建的目录
if (newDir.delete()) {
System.out.println("目录已删除: " + newDir.getPath()); // 目录已删除: C:/Users/86198/Desktop/test/newDir
} else {
System.out.println("目录删除失败: " + newDir.getPath()); // 目录删除失败: C:/Users/86198/Desktop/test/newDir
}
// 进行文件重命名(剪切/粘贴操作)
File renameFile = new File("C:/Users/86198/Desktop/test/test2.txt");
if (file.renameTo(renameFile)) {
System.out.println("文件已重命名为: " + renameFile.getPath()); // 文件已重命名为: C:/Users/86198/Desktop/test/test2.txt
} else {
System.out.println("文件重命名失败"); // 文件重命名失败
}
}
}
三:文件内容的读写——数据流
对于流的分类我们有两种分法:
- 按流的方向分类:输入流、输出流
- 按数据处理单位:字节流、字符流
现在我们理解一下什么是输入,什么是输出,什么是字节流,什么是字符流:
类别 | 描述 |
---|---|
字节流 | 以字节(8 位)为单位处理数据,适合处理二进制文件,如图片、音频、视频等。 |
字符流 | 以字符(16 位)为单位处理数据,适合处理文本文件,如 .txt 文件等。 |
输入 | 程序从外部数据源(如文件、键盘、网络等)读取数据到程序内部的过程称为输入。 |
输出 | 程序将数据从内部写入到外部数据目标(如文件、屏幕、网络等)的过程称为输出。 |
3.1 字节输入流( InputStream )
InputStream 是 Java 中用于读取字节数据的抽象类,常用的子类包括 FileInputStream。它主要用于读取二进制数据,但也可以读取文本文件,FileInputStream 常用方法如下:
方法 | 描述 |
---|---|
int read() | 读取单个字节,返回值为 0 - 255 的字节值。如果返回 -1,表示读取到文件末尾。 |
int read(byte[] b) | 读取多个字节,存储到字节数组 b 中,返回实际读取的字节数。如果返回 -1,表示文件结束。 |
int read(byte[] b, int off, int len) | 该方法从字符输入流中最多读取 len 个字节,并将它们存储到字符数组 b 中,从数组的 off 位置开始存储。 |
void close() | 关闭输入流,释放资源。 |
文件的内容如图所示:
public class InputStreamExample {
public static void main(String[] args) {
String filePath = "C:/Users/86198/Desktop/演示文件.txt";
try (FileInputStream inputStream = new FileInputStream(filePath)) {
// 1. 使用 read() 读取单个字节
System.out.println("使用 read() 读取单个字节:");
int data;
while ((data = inputStream.read()) != -1) {
System.out.print((char) data); // 输出字节对应的字符
//输出 abcdefg1234567
}
System.out.println(); // 换行
// 2. 使用 read(byte[] b) 读取多个字节
byte[] buffer = new byte[8]; // 创建 8 字节大小的数组
try (FileInputStream inputStream2 = new FileInputStream(filePath)) {
System.out.println("使用 read(byte[] b) 读取多个字节:");
int bytesRead = inputStream2.read(buffer); // 读取最多 8 个字节
for (int i = 0; i < bytesRead; i++) {
System.out.print((char) buffer[i]); // 输出字节对应的字符
//输出 abcdefg1
}
System.out.println();
}
// 3. 使用 read(byte[] b, int off, int len) 从偏移量开始读取
byte[] buffer2 = new byte[8]; // 再创建一个 8 字节大小的数组
try (FileInputStream inputStream3 = new FileInputStream(filePath)) {
System.out.println("使用 read(byte[] b, int off, int len) 从偏移量开始读取:");
int bytesRead = inputStream3.read(buffer2, 2, 6); // 从 buffer2 数组的索引为 2 (从 0 开始)的地方开始存储读取到的数据,最多读取 6 个字节。
for (int i = 2; i < 2 + bytesRead; i++) {
System.out.print((char) buffer2[i]); // 输出字节对应的字符
//输出 abcdef
}
System.out.println();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
可以这样理解,字符流和字节流都是读取或修改文件中的内容,但是字节流是按照一个字节一个字节的方式读的,而字符流是按照两个字节两个字节的方式读的。这里可能有一个误区,Java 的字符不是 2 字节的吗,为什么我们可以读取一个字节赋值给他,注意,这和 Java 的 Unicode 编码没有什么太大关系,你只需要知道字节流每次读取一个字节就可以了,当字节流遇到中文就可能出现乱码。
3.2 字节输出流( OutputStream )
OutputStream 是 Java 中用于写入字节数据的抽象类,常用的子类包括 FileOutputStream。它主要用于写入二进制数据,也可以写入文本文件,FileOutputStream 的常用方法如下:
方法 | 描述 |
---|---|
void write(int b) | 写入一个字节。 |
void write(byte[] b) | 写入字节数组 b 中的所有字节。 |
void write(byte[] b, int off, int len) | 从字节数组 b 的 off 位置开始,写入 len 个字节。 |
void close() | 关闭输出流,释放资源。 |
public class OutputStreamExample {
public static void main(String[] args) {
String filePath = "C:/Users/86198/Desktop/演示文件.txt";
// 1. 使用 write(int b) 写入单个字节
try (FileOutputStream outputStream = new FileOutputStream(filePath)) {
System.out.println("使用 write(int b) 写入单个字节:");
outputStream.write('A'); // 写入单个字符 'A'
System.out.println("写入 A 成功");
} catch (IOException e) {
e.printStackTrace();
}
// 2. 使用 write(byte[] b) 写入字节数组
String content = "Hello123";
byte[] contentBytes = content.getBytes(); // 将字符串转换为字节数组
try (FileOutputStream outputStream = new FileOutputStream(filePath, true)) { // true 为追加模式,false 为覆盖模式
System.out.println("使用 write(byte[] b) 写入字节数组:");
outputStream.write(contentBytes); // 写入字节数组
System.out.println("写入 Hello123 成功");
} catch (IOException e) {
e.printStackTrace();
}
// 3. 使用 write(byte[] b, int off, int len) 写入部分字节数组
try (FileOutputStream outputStream = new FileOutputStream(filePath, true)) { // true 为追加模式,false 为覆盖模式
System.out.println("使用 write(byte[] b, int off, int len) 写入部分字节数组:");
outputStream.write(contentBytes, 0, 5); // 从字节数组 contentBytes 的 0 位置开始,写入 5 个字节。
System.out.println("写入 Hello 成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.3 字符输入流( Reader )
Reader 是 Java 中用于读取字符数据的抽象类,常用的子类有 FileReader。它主要用于读取文本文件,FileReader 的常用方法如下:
方法 | 描述 |
---|---|
int read() | 读取单个字符,返回值为字符的 Unicode 值。如果返回 -1,表示文件结束。 |
int read(char[] cbuf) | 将字符读入到字符数组 cbuf 中,返回实际读取的字符数。 |
int read(char[] cbuf, int off, int len) | 该方法从字符输入流中最多读取 len 个字符,并将它们存储到字符数组 cbuf 中,从数组的 off 位置开始存储。 |
void close() | 关闭输入流,释放资源。 |
文件的内容如图所示:
public class ReaderExample {
public static void main(String[] args) {
String filePath = "C:/Users/86198/Desktop/演示文件.txt";
// 1. 使用 read() 读取单个字符
try (FileReader reader = new FileReader(filePath)) {
System.out.println("使用 read() 读取单个字符:");
int data;
while ((data = reader.read()) != -1) {
System.out.print((char) data); // 输出字符
//输出abcdefg1234567
}
System.out.println();
} catch (IOException e) {
e.printStackTrace();
}
// 2. 使用 read(char[] cbuf) 读取多个字符
char[] buffer = new char[8]; // 创建字符数组
try (FileReader reader = new FileReader(filePath)) {
System.out.println("使用 read(char[] cbuf) 读取多个字符:");
int charsRead = reader.read(buffer); // 读取最多 8 个字符
System.out.println(new String(buffer, 0, charsRead)); // 输出读取的字符
//输出abcdefg1
} catch (IOException e) {
e.printStackTrace();
}
// 3. 使用 read(char[] cbuf, int off, int len) 从偏移量开始读取
char[] buffer2 = new char[8];
try (FileReader reader = new FileReader(filePath)) {
System.out.println("使用 read(char[] cbuf, int off, int len) 从偏移量开始读取:");
int charsRead = reader.read(buffer2, 2, 6); // 从 buffer2 数组偏移量为 2 开始读入 6 个字符
for (int i = 2; i < 2 + charsRead; i++) {//第一第二个字符的0,所以要跳过
System.out.print(buffer2[i]); // 输出字符
//输出abcdef
}
System.out.println();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.4 字符输出流( Writer )
Writer 是 Java 中用于写入字符数据的抽象类,常用的子类有 FileWriter,FileWriter 的常用方法如下:
方法 | 描述 |
---|---|
void write(int c) | 写入单个字符。 |
void write(char[] cbuf) | 将字符数组 cbuf 中的字符写入输出流。 |
void write(char[] cbuf, int off, int len) | 将字符数组 cbuf 中从偏移量 off 开始的 len 个字符写入输出流。 |
void close() | 关闭输出流,释放资源。 |
public class WriterExample {
public static void main(String[] args) {
String filePath = "C:/Users/86198/Desktop/演示文件.txt";
String content = "Hello World";
// 1. 使用 write(int c) 写入单个字符
try (FileWriter writer = new FileWriter(filePath)) {
System.out.println("使用 write(int c) 写入单个字符:");
writer.write('A'); // 写入字符 'A'
System.out.println("写入 A 成功");
} catch (IOException e) {
e.printStackTrace();
}
// 2. 使用 write(char[] cbuf) 写入字符数组
try (FileWriter writer = new FileWriter(filePath, true)) { // true 为追加模式,false 为覆盖模式
System.out.println("使用 write(char[] cbuf) 写入字符数组:");
writer.write(content.toCharArray()); // 写入字符数组
System.out.println("写入 Hello World 成功");
} catch (IOException e) {
e.printStackTrace();
}
// 3. 使用 write(char[] cbuf, int off, int len) 写入部分字符数组
try (FileWriter writer = new FileWriter(filePath, true)) { // true 为追加模式,false 为覆盖模式
System.out.println("使用 write(char[] cbuf, int off, int len) 写入部分字符数组:");
writer.write(content.toCharArray(), 0, 5); // 写入 "Hello"
System.out.println("写入 Hello 成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}