Java 文件操作与 IO 流读写实战

Java 文件操作核心涉及 File 类管理与 IO 流读写。File 类用于抽象文件和目录,提供创建、删除、重命名及路径查询功能。IO 流分为字节流(InputStream/OutputStream)和字符流(Scanner/PrintWriter),前者适用于二进制文件,后者便于文本处理且自动处理编码。通过递归扫描目录、文件复制及关键词搜索等实战案例,可掌握文件系统的遍历与内容处理技巧。注意资源关闭、缓冲区刷新及编码设置以避免常见错误。


在写代码之前,我们得先明白'文件'到底是什么。狭义上的文件,是硬盘这种持久化存储设备中独立的数据单位,就像办公桌上一份份单独的文档,不仅有文字内容,还有文件名、类型、大小这些'附加信息'——我们把这些附加信息叫做'文件的元信息'。

比如你在电脑上看到的'PSGet.Format.ps1xml'文件,它的元信息就包括'修改日期 2019/3/19''类型 Windows PowerShell 数据文件''大小 9KB',这些信息和文件内容是分开保存的。

而随着文件越来越多,系统就用'树形结构'来管理它们——这就是我们熟悉的'文件夹(folder)或者目录 (directory)'。比如 Windows 里的'此电脑→Windows(C:)→Program Files(X86)',Linux 里的'/usr/bin',都是通过层级目录把文件组织起来,既方便查找,逻辑上也更清晰。

另外,定位文件必须用到'路径',这里分了两种:
C:\Program Files (x86)\WindowsPowerShell,不管当前在哪里,都能通过它找到文件;
..\Windows NT 就行(.. 代表父目录,. 代表当前目录)。
拓展:即使是普通文件,根据其保存数据的不同,也经常被分为不同的类型,我们一般简单的划分为:
文本文件:保存被字符集编码的文本。
二进制文件:按照标准格式保存的非被字符集编码过的文件。
在 Windows 操作系统上,还有一类文件比较特殊,就是平时我们看到的快捷方式(shortcut),这种文件只是对真实文件的一种引用而已。其他操作系统上也有类似的概念,例如软链接(soft link)等。

最后,很多操作系统为了实现接口的统一性,将所有的 I/O 设备都抽象成了文件的概念,使用这一理念最为知名的就是 Unix、Linux 操作系统——万物皆文件。这种抽象设计能让操作系统对不同 I/O 设备(如硬盘、键盘、打印机等)的操作,都统一到文件操作的接口上,简化了开发和使用逻辑,无需为不同设备单独设计一套操作方式。
Java 用 java.io.File 类来抽象描述一个文件(包括目录),但要注意:创建了 File 对象,不代表真实存在这个文件,它只是对文件的'描述'而已。
属性:
| 修饰符及类型 | 属性 | 说明 |
|---|---|---|
| static String | pathSeparator | 依赖于系统的路径分隔符,String 类型的表示 |
| static char | pathSeparator | 依赖于系统的路径分隔符,char 类型的表示 |
构造方法:
| 签名 | 说明 |
|---|---|
| File(File parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例 |
| File(String pathname) | 根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者相对路径 |
| File(String parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例,父目录用路径表示 |
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() | 判断用户是否对文件有可写权限 |
比如想知道文件的父目录、名称、路径,用这几个方法就行:
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.getParent()); // 输出父目录:..
System.out.println(file.getName()); // 输出文件名:hello-world.txt
System.out.println(file.getPath()); // 输出路径:..\hello-world.txt
System.out.println(file.getAbsolutePath()); // 输出绝对路径
System.out.println(file.getCanonicalPath()); // 输出简化绝对路径
}
}

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()); // 初始不存在:false
System.out.println(file.createNewFile()); // 创建成功:true
System.out.println(file.exists()); // 创建后存在:true
System.out.println(file.isFile()); // 是普通文件:true
System.out.println(file.delete()); // 删除成功:true
System.out.println(file.exists()); // 删除后不存在:false
}
}

这里要注意:createNewFile() 只能创建普通文件,不能创建目录;delete() 直接删除文件,不会进回收站,操作要谨慎。
新手最容易踩的坑就是这两个方法的区别!
mkdir():只能创建单层目录,如果父目录不存在,创建失败;mkdirs():能创建多层目录(包括不存在的父目录)。比如要创建'some-parent\some-dir'这个多层目录,用 mkdir() 会失败,用 mkdirs() 才能成功:
package IO;
import java.io.File;
public class demo3 {
public static void main(String[] args) {
File dir = new File("some-parent\\some-dir");
System.out.println(dir.mkdir());
System.out.println(dir.isDirectory());
System.out.println(dir.mkdirs());
System.out.println(dir.isDirectory());
}
}

这个区别一定要记牢,不然创建多层目录时会卡很久。
import java.io.File;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
File oldFile = new File("some-file.txt"); // 确保该文件存在
File newFile = new File("dest.txt"); // 确保该文件不存在
System.out.println(oldFile.exists());
System.out.println(newFile.exists());
System.out.println(oldFile.renameTo(newFile)); // 重命名成功:true
System.out.println(oldFile.exists()); // 原文件不存在:false
System.out.println(newFile.exists()); // 新文件存在:true
}
}


注意:如果目标文件(dest.txt)已经存在,renameTo() 会返回 false,所以先判断目标文件是否存在很重要。
搞懂了文件操作,接下来就是'读写文件内容'——这就需要 IO 流了。可以把 IO 流比喻得很形象:读文件像'接水'(输入流 InputStream),写文件像'灌水'(输出流 OutputStream),我们就顺着这个逻辑来学。

字节流是最基础的 IO 流,以'字节'为单位读写数据,适合所有文件(比如文本、图片、视频)。
InputStream 是抽象类,我们常用它的子类 FileInputStream 来读文件。它的核心方法是 read(),有三种用法:
read():读 1 个字节,返回字节值(-1 表示读完);read(byte[] b):读多个字节到数组 b 中,返回实际读的字节数;read(byte[] b, int off, int len):从 off 位置开始,读 len 个字节到数组 b 中。close():关闭字节流。FileInputStream 类构造方法
| 签名 | 说明 |
|---|---|
| FileInputStream(File file) | 利用 File 构造文件输入流 |
| FileInputStream(String name) | 利用文件路径构造文件输入流 |
强调:用数组读比单个字节读效率高,因为减少了 IO 次数。比如读'hello.txt'里的'Hello':
单个字节读:
package IO;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class demo5 {
// 需要在项目目录下创建 hello.txt 文件
public static void main(String[] args) throws IOException {
try (InputStream is = new FileInputStream("hello.txt")) {
while (true) {
int b = is.read();
if (b == -1) {
break;
}
System.out.printf("%c", b);
}
}
}
}
数组读:
package IO;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class demo6 {
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;
}
for (int i = 0; i < len; i++) {
System.out.printf("%c", buf[i]);
}
}
}
}
}
运行后会输出'Hello',这里用了 try-with-resources 语法,能自动关闭流,避免资源泄漏,新手一定要养成这个习惯。
如果读中文文件(比如'你好中国'),要注意编码,用 UTF-8 解码,因为 UTF-8 中一个中文字符占 3 个字节:
package IO;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class demo7 {
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;
}
for (int i = 0; i < len; i += 3) {
String s = new String(buf, i, 3, "UTF-8");
System.out.printf("%s", s);
}
}
}
}
}

我们看到了对字符类型直接使用 InputStream 进行读取是非常麻烦且困难的,所以,我们使用一种我们之前比较熟悉的类来完成该工作,就是 Scanner 类。
| 构造方法 | 说明 |
|---|---|
| Scanner(InputStream is, String charset) | 使用 charset 字符集进行 is 的扫描读取 |
package IO;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
public class demo8 {
public static void main(String[] args) throws IOException {
try (InputStream is = new FileInputStream("hello.txt")) {
try (Scanner sc = new Scanner(is, "UTF-8")) {
while (sc.hasNext()) {
String s = sc.next();
System.out.print(s);
}
}
}
}
}
OutputStream 也是抽象类,常用子类 FileOutputStream 写文件。核心方法是 write(),同样有三种用法,还有一个关键方法 flush()——因为 OutputStream 有缓冲区,数据会先存在内存,必须调用 flush() 才能把数据刷到硬盘。
比如写字符串'你好中国'到'output.txt':
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
try (OutputStream os = new FileOutputStream("output.txt")) {
String s = "你好中国";
byte[] b = s.getBytes("UTF-8"); // 转成 UTF-8 字节数组
os.write(b); // 写入字节数组
os.flush(); // 必须刷新,否则数据可能留在缓冲区
}
}
}
运行后打开'output.txt',就能看到'你好中国'。如果想追加内容,把 FileOutputStream 构造方法改成 new FileOutputStream("output.txt", true) 即可(第二个参数 true 表示追加)。

字节流读中文需要处理编码,很麻烦,这时候就需要'字符流'——按'字符'为单位读写,自动处理编码问题。用 Scanner 读字符,用 PrintWriter 写字符。
Scanner 能按行读文本,还能指定编码(比如 UTF-8),避免乱码。比如读'hello.txt'里的'你好中国':
import java.io.*;
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws IOException {
try (InputStream is = new FileInputStream("hello.txt")) {
// 指定 UTF-8 编码,避免中文乱码
try (Scanner scanner = new Scanner(is, "UTF-8")) {
while (scanner.hasNextLine()) {
// 按行读
String line = scanner.nextLine();
System.out.println(line); // 输出:你好中国
}
}
}
}
}

这种方式比字节流简单多了,新手读文本文件优先用这个。
PrintWriter 有我们熟悉的 print()、println()、printf() 方法,写文本很方便,还能指定编码。
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
try (OutputStream os = new FileOutputStream("output.txt")) {
// 先转成 OutputStreamWriter,指定 UTF-8 编码
try (OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8")) {
// 用 PrintWriter 写内容
try (PrintWriter writer = new PrintWriter(osWriter)) {
writer.println("我是第一行"); // 换行
writer.print("我是第二行"); // 不换行
writer.printf("%d: 我是第三行\n", 3); // 格式化输出
writer.flush(); // 刷新到硬盘
}
}
}
}
}

运行后'output.txt'里会有三行内容,格式清晰,比直接用 OutputStream 方便太多。
学完基础,必须通过实战巩固。通过一些经典案例,我们逐个拆解,新手跟着写一遍就能掌握。
需求:输入根目录和关键词,找到文件名包含关键词的普通文件,询问用户是否删除。 核心思路:用'递归'遍历树形目录(因为文件系统是树形结构),找到符合条件的文件后处理。
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("请输入要扫描的根目录(绝对路径/相对路径):");
String rootDirPath = scanner.next();
File rootDir = new File(rootDirPath);
if (!rootDir.isDirectory()) {
System.out.println("根目录不存在或不是目录,退出!");
return;
}
// 输入关键词
System.out.print("请输入文件名包含的字符:");
String token = scanner.next();
List<File> result = new ArrayList<>();
// 递归扫描目录
scanDir(rootDir, token, result);
// 处理结果
System.out.println("共找到 " + result.size() + " 个符合条件的文件:");
for (File file : result) {
System.out.print(file.getCanonicalPath() + ",是否删除?(y/n)");
String choice = scanner.next();
if (choice.toLowerCase().equals("y")) {
file.delete();
System.out.println("已删除!");
}
}
}
// 递归扫描目录的方法
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.getAbsoluteFile());
}
}
}
}
}


这个案例用到了 File 类的 isDirectory()、listFiles(),还有递归遍历,新手要理解递归的逻辑——'自己调用自己,处理子目录'。
需求:输入源文件路径和目标路径,实现文件复制(支持所有文件类型,比如文本、图片)。 核心思路:用 InputStream 读源文件,用 OutputStream 写目标文件,用字节数组做缓冲区提高效率。
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("请输入要复制的文件路径:");
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("请输入目标路径:");
String destPath = scanner.next();
File destFile = new File(destPath);
// 处理目标文件已存在的情况
if (destFile.exists()) {
if (destFile.isDirectory()) {
System.out.println("目标是目录,无法覆盖!");
return;
}
System.out.print("目标文件已存在,是否覆盖?(y/n)");
String choice = scanner.next();
if (!choice.toLowerCase().equals("y")) {
System.out.println("停止复制!");
return;
}
}
// 开始复制:读源文件,写目标文件
try (InputStream is = new FileInputStream(sourceFile);
OutputStream os = new FileOutputStream(destFile)) {
byte[] buf = new byte[1024]; // 1KB 缓冲区
int len;
while ((len = is.read(buf)) != -1) {
os.write(buf, 0, len); // 写读到的字节
}
os.flush(); // 刷新缓冲区
}
System.out.println("复制完成!");
}
}

执行完成后 output 文件复制了 hello 文件里面的内容。

这个案例是 IO 流的经典应用,不管复制什么文件(文本、图片、视频)都能用,因为字节流不区分文件类型。
需求:输入根目录和关键词,找到文件名或内容包含关键词的普通文件。 核心思路:在案例 1 的基础上,增加'读文件内容判断'的逻辑,用 Scanner 读文件内容,判断是否包含关键词。
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("请输入要扫描的根目录:");
String rootDirPath = scanner.next();
File rootDir = new File(rootDirPath);
if (!rootDir.isDirectory()) {
System.out.println("根目录无效,退出!");
return;
}
System.out.print("请输入要查找的关键词:");
String token = scanner.next();
List<File> result = new ArrayList<>();
scanDirWithContent(rootDir, token, result);
System.out.println("共找到 " + result.size() + " 个文件:");
for (File file : result) {
System.out.println(file.getCanonicalPath());
}
}
// 递归扫描目录,判断文件名或内容
private static void scanDirWithContent(File rootDir, String token, List<File> result) throws IOException {
File[] files = rootDir.listFiles();
if (files == null || files.length == 0) {
return;
}
for (File file : files) {
if (file.isDirectory()) {
scanDirWithContent(file, token, result);
} else {
// 文件名包含,或内容包含,都加入结果
if (file.getName().contains(token) || isContentContains(file, token)) {
result.add(file);
}
}
}
}
// 判断文件内容是否包含关键词(按 UTF-8 处理)
private static boolean isContentContains(File file, String token) throws IOException {
StringBuilder sb = new StringBuilder();
try (InputStream is = new FileInputStream(file);
Scanner scanner = new Scanner(is, "UTF-8")) {
while (scanner.hasNextLine()) {
sb.append(scanner.nextLine()).append("\n"); // 读所有行
}
}
return sb.indexOf(token) != -1; // 判断是否包含关键词
}
}

这个案例增加了 isContentContains() 方法,读文件内容并判断,适合查找文本文件中的关键词(注意:大文件会影响性能,文档里也提到了这一点)。
掌握了 Java 文件操作和 IO 流的核心内容,最后再总结几个新手常踩的坑,帮你少走弯路:
createNewFile() 或 mkdirs() 才会真实创建;mkdirs();try-with-resources 语法,自动关闭流,避免资源泄漏;flush(),否则数据可能留在缓冲区,没写到硬盘;至此,Java 文件操作和 IO 流的核心知识和实战就讲完了。其实这些内容并不难,关键是多写代码、多跑案例——把文中的代码逐个复制到 IDE 里运行,改改参数(比如换个文件路径、关键词),很快就能熟练掌握。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online