一:认识文件
我们先来认识狭义上的文件(file)。针对硬盘这种持久化存储的I/O设备,当我们想要进行数据保存时,往往不是保存成一个整体,而是独立成一个个的单位进行保存,这个独立的单位就被抽象成文件的概念,就类似办公桌上的一份份真实的文件一般。
文件除了有数据内容之外,还有一部分信息,例如文件名、文件类型、文件大小等并不作为文件的数据而存在,我们把这部分信息可以视为文件的元信息。
同时,随着文件越来越多,我们就需要对文件进行管理,如何进行文件的组织呢,一种合乎自然的想法出现了,就是按照层级结构进行组织 —— 也就是我们数据结构中学习过的树形结构。这样,一种专门用来存放管理信息的特殊文件诞生了,也就是我们平时所谓文件夹(folder)或者目录(directory)的概念。
1.1 文件路径
如何在文件系统中如何定位我们的一个唯一的文件就成为当前要解决的问题,这其实很简单,因为从树型结构的角度来看,树中的每个结点都可以被一条从根开始,一直到达的结点的路径所描述,而这种描述方式就被称为文件的绝对路径(absolute path)。
除了可以从根开始进行路径的描述,我们可以从任意结点出发,进行路径的描述,而这种描述方式就被称为相对路径(relative path),相对于当前所在结点的一条路径。
文件的相对路径是以当前所在目录(工作目录)为基准的,每个程序运行的时候都有一个工作目录,IDEA的工作目录默认为当前项目所在的目录
1.2 文件的分类
文件根据其保存数据的不同,被分为不同的类型,我们一般简单的划分为文本文件和二进制文件,分别指代保存被字符集编码的文本和按照标准格式保存的非被字符集编码过的文件。
Windows 操作系统上,会按照文件名中的后缀来确定文件类型以及该类型文件的默认打开程序。但这个习俗并不是通用的,在 OSX、Unix、Linux 等操作系统上,就没有这样的习惯,一般不对文件类型做如此精确地分类。
1.3 文件管理
文件由于被操作系统进行了管理,所以根据不同的用户,会赋予用户不同的对待该文件的权限,一般地可以认为有可读、可写、可执行权限。
Windows 操作系统上,还有一类文件比较特殊,就是平时我们看到的快捷方式(shortcut),这种文件只是对真实文件的一种引用而已。其他操作系统上也有类似的概念,例如,软链接(soft link)等。
最后,很多操作系统为了实现接口的统一性,将所有的 I/O 设备都抽象成了文件的概念,使用这一理念最为知名的就是 Unix、Linux 操作系统 —— 万物皆文件。
二: Java 中操作文件
Java 中通过 java.io.File 类来对一个文件(包括目录)进行抽象的描述。注意,有 File 对象,并不代表真实存在该文件。
下面我们来看看 File 类中的常见属性、构造方法和方法:
- 属性
修饰符及类型 | 属性 | 说明 |
---|---|---|
static | String | pathSeparator |
static | char | pathSeparator |
- 构造方法
方法签名 | 说明 |
---|---|
File(File parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例 |
File(String pathname) | 根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者相对路径 |
File(String parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例,父目录用路径表示 |
下面是一个示例 Java 程序来演示如何使用这三个构造方法:
import java.io.File;public class FileConstructorExample {public static void main(String[] args) {// 使用父目录 + 孩子文件路径创建一个新的 File 实例File file1 = new File("C:/myfolder", "myfile.txt");// 使用文件路径创建一个新的 File 实例File file2 = new File("C:/myfolder/myfile.txt");// 使用父目录 + 孩子文件路径创建一个新的 File 实例,父目录用路径表示File parentDir = new File("C:/myfolder");File file3 = new File(parentDir, "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 FileDemo {public static void main(String[] args) {// 创建File对象File file = new File("path/to/file.txt");// 并不要求该文件真实存在// getParent()String parent = file.getParent(); // 返回父目录路径System.out.println("父目录路径:" + parent); // 输出:父目录路径:path/to// getName()String name = file.getName(); // 返回文件名System.out.println("文件名:" + name); // 输出:文件名:file.txt// getPath()String path = file.getPath(); // 返回文件路径System.out.println("文件路径:" + path); // 输出:文件路径:path/to/file.txt// getAbsolutePath()String absolutePath = file.getAbsolutePath(); // 返回绝对路径System.out.println("绝对路径:" + absolutePath); // 输出:绝对路径:path/to/file.txt// getCanonicalPath()try {String canonicalPath = file.getCanonicalPath(); // 返回修饰过的绝对路径System.out.println("修饰过的绝对路径:" + canonicalPath); // 输出:修饰过的绝对路径:path/to/file.txt} catch (IOException e) {e.printStackTrace();}// exists()boolean exists = file.exists(); // 判断文件是否存在System.out.println("文件是否存在:" + exists); // 输出:文件是否存在:false// isDirectory()boolean isDirectory = file.isDirectory(); // 判断是否为目录System.out.println("是否为目录:" + isDirectory); // 输出:是否为目录:false// isFile()boolean isFile = file.isFile(); // 判断是否为文件System.out.println("是否为文件:" + isFile); // 输出:是否为文件:false// createNewFile()try {boolean created = file.createNewFile(); // 创建空文件System.out.println("文件创建是否成功:" + created); // 输出:文件创建是否成功:true} catch (IOException e) {e.printStackTrace();}// delete()boolean deleted = file.delete(); // 删除文件System.out.println("文件是否删除成功:" + deleted); // 输出:文件是否删除成功:true// deleteOnExit()file.deleteOnExit(); // 标注文件在JVM运行结束时删除// list()File directory = new File("path/to/directory");String[] fileList = directory.list(); // 获取目录下的所有文件名System.out.println("目录下的文件名:");for (String fileName : fileList) {System.out.println(fileName);}// listFiles()File[] files = directory.listFiles(); // 获取目录下的所有文件对象System.out.println("目录下的文件路径:");for (File fileObject : files) {System.out.println(fileObject.getPath());}// mkdir()boolean directoryCreated = directory.mkdir(); // 创建目录System.out.println("目录创建是否成功:" + directoryCreated); // 输出:目录创建是否成功:true// mkdirs()boolean directoriesCreated = directory.mkdirs(); // 创建目录,包括中间目录,mkdir() 的时候,如果中间目录不存在,则无法创建成功; mkdirs() 可以解决这个问题。System.out.println("目录创建是否成功:" + directoriesCreated); // 输出:目录创建是否成功:true// renameTo()File newFile = new File("path/to/newFile.txt");boolean renamed = file.renameTo(newFile); // 文件重命名(移动文件)System.out.println("文件重命名是否成功:" + renamed); // 输出:文件重命名是否成功:true// canRead()boolean canRead = file.canRead(); // 判断文件是否可读System.out.println("文件是否可读:" + canRead); // 输出:文件是否可读:true// canWrite()boolean canWrite = file.canWrite(); // 判断文件是否可写System.out.println("文件是否可写:" + canWrite); // 输出:文件是否可写:true}
}
三:文件内容的读写——数据流
3.1 InputStream
- 方法:
修饰符及返回值类型 | 方法签名 | 说明 |
---|---|---|
int | read() | 读取一个字节的数据,返回 -1 代表已经完全读完了 |
int | read(byte[] b) | 最多读取 b.length 字节的数据到 b 中,返回实际读到的数量;-1 代表已经读完了 |
int | read(byte[] b, int off, int len) | 最多读取 len - off 字节的数据到 b 中,放在从 off 开始,返回实际读到的数量;-1 代表已经读完了 |
void | close() | 关闭字节流 |
InputStream 只是一个抽象类,要使用还需要具体的实现类。关于 InputStream 的实现类有很多,基本可以认为不同的输入设备都可以对应一个 InputStream 类,我们现在只关心从文件中读取,所以使用 FileInputStream
FileInputStream的构造方法:
签名 | 说明 |
---|---|
FileInputStream(File file) | 利用 File 构造文件输入流 |
FileInputStream(String name | 利用文件路径构造文件输入流 |
对于流的分类我们有两种分法:
- 按流的方向分类:输入流、输出流
- 按数据处理单位:字节流、字符流
现在我们理解一下什么是输入,什么是输出:
3.2 字节流
3.2.1字节输入流InputStream
-
read()方法:
- 读取一个字节的数据,并返回读取的字节值。如果已经达到文件的末尾,则返回-1。
-
read(byte[] b)方法:
- 将数据读取到给定的字节数组b中,并返回读取的字节数。如果已经达到文件的末尾,则返回-1。
-
read(byte[] b, int off, int len)方法:
- 将最多len个字节的数据读取到给定的字节数组b的off位置开始的地方,并返回读取的字节数。如果已经达到文件的末尾,则返回-1。
-
close()方法:
- 关闭输入流,并释放与之关联的系统资源。
假设存在一个名为"example.txt"的文本文件,并且文件内容为 hello world!下面是这四个方法的使用示例:
import java.io.FileReader;
import java.io.IOException;public class FileReaderExample {public static void main(String[] args) {try (FileReader reader = new FileReader("example.txt")) {int data;// 使用 read() 方法读取一个字节的数据,并返回读取的字节值// 如果已经达到文件的末尾,则返回-1data = reader.read();System.out.println((char) data); // 输出:h// 使用 read(byte[] b) 方法将数据读取到给定的字节数组b中// 并返回读取的字节数,如果已经达到文件的末尾,则返回-1byte[] buffer = new byte[5];int bytesRead = reader.read(buffer);String text = new String(buffer, 0, bytesRead);System.out.println(text); // 输出:ello// 使用 read(byte[] b, int off, int len) 方法将最多 len 个字节的数据读取到给定的字节数组b的off位置开始的地方// 并返回读取的字节数,如果已经达到文件的末尾,则返回-1bytesRead = reader.read(buffer, 0, 5);text = new String(buffer, 0, bytesRead);System.out.println(text); // 输出: worl// 关闭输入流,并释放与之关联的系统资源reader.close();} catch (IOException e) {e.printStackTrace();}}
}
3.2.2 字节输出流OutputStream`
-
write(byte[] b)
:将给定的字节数组写入输出流。 -
write(byte[] b, int off, int len)
:将指定的字节数组的一部分写入输出流。off
参数表示起始偏移量,len
参数表示要写入的字节数。 -
write(int b)
:将指定的字节写入输出流。参数b
是一个int
类型的整数,但只会写入低8位的字节。 -
close()
:关闭输出流。关闭后,将无法继续写入数据。
以下是一个示例的Java程序,展示了如何使用FileOutputStream
和FileInputStream
类对文件执行写入和读取操作。程序中演示了write(byte[] b)
、write(byte[] b, int off, int len)
、write(int b)
和close()
方法的使用。假设存在一个名为"example.txt"的文本文件,并且文件内容为"hello world!"。
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;public class FileWriterExample {public static void main(String[] args) {String fileName = "example.txt";try {// 使用 FileOutputStream 进行文件写入FileOutputStream fos = new FileOutputStream(fileName);// 使用 write(byte[] b) 方法替换整个文件内容byte[] replaceBytes = "new content".getBytes();fos.write(replaceBytes);// 使用 write(byte[] b, int off, int len) 方法替换文件一部分内容byte[] partialBytes = " content".getBytes();fos.write(partialBytes, 4, partialBytes.length - 4);// 使用 write(int b) 方法写入单个字节int singleByte = '!'; // 写入 ASCII 值为 33 的字符fos.write(singleByte);// 关闭输出流fos.close();// 使用 FileInputStream 进行文件读取FileInputStream fis = new FileInputStream(fileName);// 读取文件内容并打印int content;while ((content = fis.read()) != -1) {System.out.print((char) content);}// 关闭输入流fis.close();} catch (IOException e) {System.out.println("发生错误:" + e.getMessage());}}
}
3.3 字符流
3.3.1 字符输入流Reader
-
read()
:从输入流中读取一个字符,并返回读取的字符的ASCII值。如果已经到达流的末尾,则返回-1。 -
read(char[] cbuf)
:从输入流中读取字符,并将它们存储在字符数组cbuf
中。返回读取的字符数。 -
read(char[] cbuf, int off, int len)
:从输入流中读取字符,并将它们存储在字符数组cbuf
的指定位置off
开始的位置,并最多读取len
个字符。返回读取的字符数。 -
read(CharBuffer target)
:从输入流中读取字符,并将它们存储在CharBuffer
中。返回读取的字符数。 -
flush()
:刷新输出流并将任何缓冲的输出字符写入到底层流中。 -
close()
:关闭输入流,释放与其关联的资源。
代码和字节输入流中的代码效果类似,只不过读取是通过字符而已
3.3.2 字符输出流Writer
当使用字符输出流 Writer
进行操作时,可以使用以下方法:
-
write(char[] cbuf)
:将字符数组cbuf
中的字符写入输出流。 -
write(char[] cbuf, int off, int len)
:将字符数组cbuf
中从索引off
开始的len
个字符写入输出流。 -
write(int c)
:将给定的字符编码c
写入输出流。 -
write(String str)
:将字符串str
写入输出流。 -
write(String str, int off, int len)
:将字符串str
中从索引off
开始的len
个字符写入输出流。 -
flush()
:刷新输出流,将缓冲区中的内容立即写入到目标设备中。 -
close()
:关闭输出流,并释放相关的系统资源。
代码和字节输出流中的代码效果类似,只不过输出是通过字符而已
案例:进行普通的文件复制
import java.io.*;
import java.util.*;
public class Main {public static void main(String[] args) throws IOException {Scanner scanner = new Scanner(System.in);System.out.print("请输入要复制的文件(绝对路径 OR 相对路径): ");String sourcePath = scanner.next();File sourceFile = new File(sourcePath);if (!sourceFile.exists()) {System.out.println("文件不存在,请确认路径是否正确");return;}if (!sourceFile.isFile()) {System.out.println("文件不是普通文件,请确认路径是否正确");return;}System.out.print("请输入要复制到的目标路径(绝对路径 OR 相对路径): ");String destPath = scanner.next();File destFile = new File(destPath);if (destFile.exists()) {if (destFile.isDirectory()) {System.out.println("目标路径已经存在,并且是一个目录,请确认路径是否正
确");return;}if (destFile.isFile()) {System.out.println("目录路径已经存在,是否要进行覆盖?y/n");String ans = scanner.next();if (!ans.toLowerCase().equals("y")) {System.out.println("停止复制");return;}}}try (InputStream is = new FileInputStream(sourceFile)) {try (OutputStream os = new FileOutputStream(destFile)) {byte[] buf = new byte[1024];int len;while (true) {len = is.read(buf);if (len == -1) {break;}os.write(buf, 0, len);}os.flush();}}System.out.println("复制已完成");}
}
3.3.3 读写代码参考
- 如何按字节进行数据读
try (InputStream is = ...) {byte[] buf = new byte[1024];while (true) {int n = is.read(buf);if (n == -1) {break;}// buf 的 [0, n) 表示读到的数据,按业务进行处理}
}
- 如何按字节进行数据写
try (OutputStream os = ...) {byte[] buf = new byte[1024];while (/* 还有未完成的业务数据 */) {// 将业务数据填入 buf 中,长度为 nint n = ...;os.write(buf, 0, n);}os.flush(); // 进行数据刷新操作
}
- 如何按字符进行数据读
try (InputStream is = ...) {try (Scanner scanner = new Scanner(is, "UTF-8")) {while (scanner.hasNextLine()) {String line = scanner.nextLine();// 根据 line 做业务处理}}
}
- 如何按字符进行数据写
try (OutputStream os = ...) {try (OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8")) {try (PrintWriter writer = new PrintWriter(osWriter)) {while (/* 还有未完成的业务数据 */) {writer.println(...);}writer.flush(); // 进行数据刷新操作}}
}