在 Java 开发中,文件操作是最基础也最常用的能力之一:小到读取配置文件、写入日志,大到文件上传下载、数据备份,都离不开文件系统交互。Java 将文件系统操作拆分为两大块:File类负责文件元信息与目录管理,InputStream/OutputStream字节流负责文件内容的读写。
本文从基础概念到核心 API,再到实战案例,带你系统掌握 Java 文件操作的完整用法。
一、文件系统基础概念
在写代码之前,我们先理清几个文件系统的核心概念。
1. 文件与目录
硬盘上的数据以独立单位持久化存储,这个单位就是文件;文件除了内容本身,还包含文件名、大小、修改时间等元信息。 为了方便管理大量文件,系统用树形结构组织文件,专门存放管理信息的特殊文件就是目录(文件夹)。
2. 绝对路径与相对路径
定位文件的路径分为两种:
- 绝对路径:从系统根目录出发,唯一确定一个文件。例如 Windows 下的
C:\Program Files\test.txt。 - 相对路径:以当前工作目录为基准,描述文件位置。例如
.\test.txt表示当前目录下的文件,..\test.txt表示上级目录下的文件。
3. 文本文件与二进制文件
按照存储格式划分:
- 文本文件:保存经过字符集编码的内容,比如
.txt、.java文件,可以用记事本直接打开阅读。 - 二进制文件:按标准格式存储非字符数据,比如图片、视频、exe 程序,直接打开会看到乱码。
字节流可以处理所有类型的文件,字符流仅适合处理文本文件。
二、File 类:文件与目录的抽象描述
java.io.File是 Java 对文件和目录的抽象表示。注意:File 对象只是路径的封装,不代表文件一定真实存在,它不负责读写文件内容,只操作文件元信息。
1. 类中的构造方法和方法
构造方法
| 签名 | 说明 |
|---|---|
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 对象的修饰过的绝对路径 |
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() | 判断用户是否对文件有可写权限 |
2. 核心方法详解
(1)获取路径信息
表格
| 方法 | 作用 |
|---|---|
String getParent() | 获取父目录路径 |
String getName() | 获取纯文件名 |
String getPath() | 获取构造时传入的路径 |
String getAbsolutePath() | 获取绝对路径 |
String getCanonicalPath() | 获取规范化的绝对路径(自动解析..等相对路径) |
示例:
File file = new File("..\\test.txt"); System.out.println(file.getParent()); // .. System.out.println(file.getName()); // test.txt System.out.println(file.getAbsolutePath());// 带..的完整路径 System.out.println(file.getCanonicalPath());// 解析后的标准路径(2)文件状态判断
表格
| 方法 | 作用 |
|---|---|
boolean exists() | 判断文件 / 目录是否真实存在 |
boolean isDirectory() | 判断是否为目录 |
boolean isFile() | 判断是否为普通文件 |
boolean canRead() | 判断是否有可读权限 |
boolean canWrite() | 判断是否有可写权限 |
(3)文件创建与删除
① 创建空文件:createNewFile ()
createNewFile()用于根据 File 对象的路径创建一个空文件,是原子操作,仅当文件不存在时才会创建成功。
代码示例
import java.io.File; import java.io.IOException; public class Main { public static void main(String[] args) throws IOException { File file = new File("hello-world.txt"); System.out.println(file.exists()); // 判断文件是否存在 System.out.println(file.isDirectory()); // 判断是否为目录 System.out.println(file.isFile()); // 判断是否为普通文件 System.out.println(file.createNewFile());// 创建空文件 System.out.println(file.exists()); // 创建后再次判断是否存在 System.out.println(file.isDirectory()); System.out.println(file.isFile()); System.out.println(file.createNewFile());// 重复创建 } }运行结果
false false false true true false true false结果解读
- 初始状态文件不存在,
exists()返回false,且既不是目录也不是普通文件; - 调用
createNewFile()成功创建空文件,返回true; - 创建完成后,
exists()与isFile()均返回true,确认普通文件已生成; - 再次调用
createNewFile(),因文件已存在,返回false,不会重复创建。
注意:该方法是原子操作,多线程并发场景下,能保证同一文件只会被创建一次。
② 文件删除的两种方式
File 类提供了两种删除方案,核心差异在于执行时机不同,分别适配不同的业务场景。
方式一:立即删除 delete ()
调用后同步执行删除操作,当场返回结果,删除成功返回true;文件不存在、文件被占用、目录非空等情况返回false。
代码示例
import java.io.File; import java.io.IOException; public class Main { public static void main(String[] args) throws IOException { File file = new File("some-file.txt"); System.out.println(file.exists()); System.out.println(file.createNewFile()); System.out.println(file.exists()); System.out.println(file.delete()); // 立即删除 System.out.println(file.exists()); } }运行结果
false true true true false结果解读
- 先创建文件,确认文件存在;
- 调用
delete()后立即执行删除,返回true表示删除成功; - 最终
exists()返回false,文件已被移除。
方式二:退出时删除 deleteOnExit ()
调用时不会立刻删除文件,仅向 JVM 注册一个 “退出删除” 的标记,等 JVM 正常退出时才会按注册顺序执行删除。
- 方法签名:
void deleteOnExit() - 执行时机:JVM 正常退出时才执行删除
- 返回值:无返回值,仅做登记,无法即时获知删除结果
代码示例
import java.io.File; import java.io.IOException; public class Main { public static void main(String[] args) throws IOException { File file = new File("some-file.txt"); System.out.println(file.exists()); System.out.println(file.createNewFile()); System.out.println(file.exists()); file.deleteOnExit(); // 注册:JVM退出时自动删除 System.out.println(file.exists()); } }运行结果
false true true true结果解读
- 调用
deleteOnExit()时,文件不会立刻消失,因此最后一行exists()仍然返回true; - 只有当
main方法执行完毕、JVM 正常结束后,文件才会被自动删除。
(4)目录操作
File dir = new File("a/b/c"); // 创建单级目录,父目录不存在则创建失败 boolean res1 = dir.mkdir(); // 创建多级目录,自动创建所有不存在的父目录 boolean res2 = dir.mkdirs(); // 列出目录下所有文件的名称 String[] names = dir.list(); // 列出目录下所有文件的File对象 File[] files = dir.listFiles();(5)重命名与移动
File src = new File("old.txt"); File dest = new File("new.txt"); // 重命名文件,也可实现剪切粘贴效果 boolean result = src.renameTo(dest);3. 常见易错点
mkdir()只能创建单级目录,多级目录必须用mkdirs();delete()删除目录时必须为空,否则删除失败;deleteOnExit()仅在 JVM 正常退出时生效,强制杀进程不会执行删除;list()/listFiles()可能返回null(路径不存在、不是目录、无权限),使用前必须判空。
那么无论是
delete()还是deleteOnExit(),都要求目录必须是空的(里面没有任何文件和子文件夹),才能删除成功。
三、字节流:文件内容的读写基础
Java 用「流」的概念处理数据读写:数据像水流一样从一端流向另一端。按数据方向划分:
- 输入流(InputStream):数据从文件流向程序,对应读文件;
- 输出流(OutputStream):数据从程序流向文件,对应写文件。
字节流以字节为单位操作,可以处理所有类型的文件(文本、图片、视频等)。
1. InputStream:字节输入流
InputStream是所有字节输入流的抽象父类,FileInputStream是其针对文件的实现类。
核心方法
表格
| 方法 | 作用 |
|---|---|
int read() | 读取一个字节,返回字节值;读到文件末尾返回-1 |
int read(byte[] b) | 批量读取字节到数组,返回实际读到的字节数;末尾返回-1 |
int read(byte[] b, int off, int len) | 读取字节到数组的指定区间 |
void close() | 关闭流,释放系统资源 |
FileInputStream概述
构造方法
签名 | 说明 |
FileInputStream(Filefile) | 利⽤File构造⽂件输⼊流 |
FileInputStream(Stringname) | 利⽤⽂件路径构造⽂件输⼊流 |
将文件完全读完的方式
① 单字节逐个读取
try (InputStream is = new FileInputStream("e:/test.txt")) { while (true) { int b = is.read(); if (b == -1) { // 读到文件末尾 break; } System.out.printf("%x ", b); // 以十六进制打印字节 } } catch (IOException e) { e.printStackTrace(); }特点:代码简单,但频繁 IO 操作,性能差,仅适合小文件。
②字节数组批量读取
try (InputStream is = new FileInputStream("e:/test.txt")) { byte[] buf = new byte[1024]; // 缓冲区 int len; while ((len = is.read(buf)) != -1) { // 只处理实际读到的len个字节 for (int i = 0; i < len; i++) { System.out.printf("%x ", buf[i]); } } } catch (IOException e) { e.printStackTrace(); }特点:一次读取一批字节,大幅减少 IO 次数,性能远高于单字节读取,是开发中的标准写法。
示例:字节流手动解码输出中文
在批量读取字节的基础上,可以手动按编码规则将字节解码为中文字符。以下为教学演示写法,仅适配纯 UTF-8 中文小文件,不是通用解决方案:
import java.io.*; // 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "你好中国" 的内容 public class Main { public static void main(String[] args) throws IOException { try (InputStream is = new FileInputStream("hello.txt")) { byte[] buf = new byte[1024]; int len; while (true) { len = is.read(buf); if (len == -1) { // 代表文件已经全部读完 break; } // 每次使用 3 字节进行 utf-8 解码,得到中文字符 // 利用 String 中的构造方法完成 // 注意:这个方法了解下即可,不是通用的解决办法 for (int i = 0; i < len; i += 3) { String s = new String(buf, i, 3, "UTF-8"); System.out.printf("%s", s); } } } } }③字符流批量读取
使用Reader字符流读取文本文件,底层内置解码器,自动按照编码规则将字节拼接为完整字符,中英文混合也能正常输出,是文本文件读取的通用标准方案。
package io; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.Reader; public class Demo4 { public static void main(String[] args) { try(Reader reader=new FileReader("e:/test2.txt")){ char[] chars = new char[1024]; // 数组只创建一次 int n; while (true) { n = reader.read(chars); if (n == -1) { break; // 读到文件末尾,退出循环 } // 只打印实际读到的n个字符 for (int i = 0; i < n; i++) { System.out.print(chars[i]); } } } catch (IOException e) { e.printStackTrace(); } } }2. OutputStream:字节输出流
OutputStream是所有字节输出流的抽象父类,FileOutputStream是其文件实现类。
核心方法
表格
| 方法 | 作用 |
|---|---|
void write(int b) | 写入一个字节 |
void write(byte[] b) | 写入整个字节数组 |
void write(byte[] b, int off, int len) | 写入数组的指定区间 |
void flush() | 刷新缓冲区,将内存数据强制写入磁盘 |
void close() | 关闭流 |
写入示例
import java.io.*; public class Demo { public static void main(String[] args) { try (OutputStream os = new FileOutputStream("e:/output.txt")) { // 1. 写入单个字节(仅适用于ASCII字符,比如英文、数字) os.write('H'); // 2. 批量写入英文内容 String enStr = "Hello World"; byte[] enData = enStr.getBytes("UTF-8"); os.write(enData); // 3. 批量写入中文内容 String cnStr = "你好中国"; byte[] cnData = cnStr.getBytes("UTF-8"); // 中文必须指定编码转成字节数组 os.write(cnData); os.flush(); // 强制刷新缓冲区 } catch (IOException e) { e.printStackTrace(); } } }flush () 的作用
为了减少磁盘 IO 次数,输出流内部有缓冲区:数据先写入内存缓冲区,缓冲区满了才真正写入磁盘。flush()会强制将缓冲区数据刷入磁盘,避免数据遗留在内存中丢失。
关闭流时会自动执行 flush,但建议关键写入后手动调用一次。
3. try-with-resources 自动关流
所有 IO 流都属于系统资源,使用完毕必须关闭,否则会造成资源泄漏。 Java 7 引入的try-with-resources语法可以自动关闭实现了AutoCloseable接口的流,无需手动写close(),也不会因为异常导致关流失败,是官方推荐的标准写法。
语法:
try (InputStream is = new FileInputStream("test.txt"); OutputStream os = new FileOutputStream("copy.txt")) { // 读写逻辑 } catch (IOException e) { e.printStackTrace(); } // 跳出try块后,两个流会自动关闭示例:
package io; import java.io.FileReader; import java.io.IOException; import java.io.Reader; public class Demo4 { public static void main(String[] args) { // try-with-resources:自动关闭流,无需手动close try (Reader reader = new FileReader("e:/test2.txt")) { // 方式2:字符数组批量读取 char[] chars = new char[1024]; // 数组只创建一次 int n; while (true) { n = reader.read(chars); if (n == -1) { break; // 读到文件末尾,退出循环 } // 只打印实际读到的n个字符 for (int i = 0; i < n; i++) { System.out.print(chars[i]); } } } catch (IOException e) { e.printStackTrace(); } } }四、扩展:字节流转字符流
经上可知字节流处理文本文件不够方便,Java 提供了转换流,可以将字节流包装成字符流,更适合处理文本内容。
1. 字符读取:Scanner 包装 InputStream
上述例子中,我们看到了对字符类型直接使用 InputStream 进⾏读取是⾮常⿇烦且困难的,所以,我们使用⼀种我们之前⽐较熟悉的类来完成该工作,就是 Scanner 类。
构造⽅法 | 说明 |
Scanner(InputStreamis,Stringcharset) | 使⽤charset字符集进行is的扫描读取 |
import java.io.*; import java.util.*; // 需要先在项⽬⽬录下准备好⼀个 hello.txt 的⽂件,⾥⾯填充 "你好中国" 的内容 public class Main { public static void main(String[] args) throws IOException { try (InputStream is = new FileInputStream("hello.txt")) { try (Scanner scanner = new Scanner(is, "UTF-8")) { while (scanner.hasNext()) { String s = scanner.next(); System.out.print(s); } } } } }2. 字符写入:PrintWriter 包装 OutputStream
import java.io.*; public class Main { public static void main(String[] args) throws IOException { // try-with-resources 自动关闭所有流 try (OutputStream os = new FileOutputStream("output.txt"); OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8"); PrintWriter writer = new PrintWriter(osWriter)) { writer.println("我是第一行"); writer.print("我的第二行\r\n"); writer.printf("%d: 我的第三行\r\n", 1 + 1); writer.flush(); } } }五、实战案例
案例 1:递归遍历目录查找文件
import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); // 1. 输入并校验根目录 System.out.print("请输入要扫描的根目录(绝对路径 OR 相对路径): "); String rootDirPath = scanner.next(); File rootDir = new File(rootDirPath); if (!rootDir.isDirectory()) { System.out.println("您输入的根目录不存在或者不是目录,退出"); return; } // 2. 输入要查找的文件名关键词 System.out.print("请输入要找出的文件名中的字符: "); String token = scanner.next(); List<File> result = new ArrayList<>(); // 3. 递归遍历目录,收集符合条件的文件 scanDir(rootDir, token, result); // 4. 遍历结果,逐个询问是否删除 System.out.println("共找到了符合条件的文件 " + result.size() + " 个"); for (File file : result) { System.out.print(file.getCanonicalPath() + " 请问您是否要删除该文件(y/n): "); String in = scanner.next(); if (in.toLowerCase().equals("y")) { file.delete(); System.out.println("已删除"); } } scanner.close(); } /** * 递归扫描目录,收集文件名包含关键词的普通文件 */ private static void scanDir(File rootDir, String token, List<File> result) { File[] files = rootDir.listFiles(); // 空目录、无权限、路径无效时直接返回,避免空指针 if (files == null || files.length == 0) { return; } for (File file : files) { if (file.isDirectory()) { // 是目录,递归进入继续扫描 scanDir(file, token, result); } else { // 是普通文件,判断文件名是否包含关键词 if (file.getName().contains(token)) { result.add(file); } } } } }案例 2:文件复制工具
import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); // 1. 输入并校验源文件 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; } // 2. 输入并校验目标路径 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.print("目标文件已经存在,是否要进行覆盖?y/n: "); String ans = scanner.next(); if (!ans.toLowerCase().equals("y")) { System.out.println("停止复制"); return; } } } // 3. 执行文件复制 try (InputStream is = new FileInputStream(sourceFile); 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("复制已完成"); scanner.close(); } }案例3:对名称与内容两个维度进行检索
扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)
注意:我们现在的方案性能较差,所以尽量不要在太复杂的目录下或者大文件下实验
import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); // 1. 输入并校验根目录 System.out.print("请输入要扫描的根目录(绝对路径 OR 相对路径): "); String rootDirPath = scanner.next(); File rootDir = new File(rootDirPath); if (!rootDir.isDirectory()) { System.out.println("您输入的根目录不存在或者不是目录,退出"); return; } // 2. 输入要搜索的内容关键词 System.out.print("请输入要查找的文件内容关键词: "); String token = scanner.next(); List<File> result = new ArrayList<>(); // 3. 递归遍历,按内容筛选文件 scanDirWithContent(rootDir, token, result); // 4. 输出结果 System.out.println("共找到了符合条件的文件 " + result.size() + " 个,它们分别是:"); for (File file : result) { System.out.println(file.getCanonicalPath()); } scanner.close(); } /** * 递归扫描目录,收集内容包含关键词的普通文件 */ private static void scanDirWithContent(File rootDir, String token, List<File> result) { File[] files = rootDir.listFiles(); // 空目录、无权限、路径无效直接返回,避免空指针 if (files == null || files.length == 0) { return; } for (File file : files) { if (file.isDirectory()) { // 是目录,递归进入继续扫描 scanDirWithContent(file, token, result); } else { // 是普通文件,判断内容是否包含关键词 if (isContentContains(file, token)) { result.add(file); } } } } /** * 判断单个文本文件的内容是否包含指定关键词 * 统一按 UTF-8 编码处理 */ private static boolean isContentContains(File file, String token) throws IOException { StringBuilder sb = new StringBuilder(); // 字节流 + Scanner 按行读取文本 try (InputStream is = new FileInputStream(file); Scanner scanner = new Scanner(is, "UTF-8")) { while (scanner.hasNextLine()) { sb.append(scanner.nextLine()); sb.append("\r\n"); // 保留换行,避免跨行关键词匹配失效 } } // 判断内容中是否包含关键词 return sb.indexOf(token) != -1; } }六、总结与选型建议
- File 类:只负责文件元信息操作(创建、删除、重命名、遍历目录),不读写内容。
- 字节流:
InputStream/OutputStream是字节流的抽象父类,文件操作使用FileInputStream/FileOutputStream实现;- 批量读写(字节数组缓冲区)性能远高于单字节读写,是标准写法;
- 必须使用 try-with-resources 自动关流,避免资源泄漏。
- 选型建议:
- 操作图片、视频、压缩包等二进制文件 → 直接用字节流;
- 操作纯文本文件 → 优先用字符流(包装字节流),避免编码问题;
- 大文件读写务必使用缓冲区,减少磁盘 IO 次数。