
我的个人主页
我的专栏: 人工智能领域、java-数据结构、Javase、C语言,MySQL,JavaEE初阶,希望能帮助到大家!!! 点赞👍收藏❤


目录
-
- 一、先搞懂:文件和文件系统的基础认知
- [二、Java 中操作文件的"核心工具":File 类](#二、Java 中操作文件的“核心工具”:File 类)
-
- [1. File 类的关键属性、构造和方法](#1. File 类的关键属性、构造和方法)
- [2. File 类实操:从获取信息到创建删除](#2. File 类实操:从获取信息到创建删除)
-
- [(1)搞懂 get 系列方法:获取文件信息](#(1)搞懂 get 系列方法:获取文件信息)
- [(2)创建与删除文件:createNewFile() 和 delete()](#(2)创建与删除文件:createNewFile() 和 delete())
- [(3)创建目录:mkdir() 和 mkdirs() 的区别](#(3)创建目录:mkdir() 和 mkdirs() 的区别)
- (4)文件重命名:renameTo()
- [三、Java IO 流:文件内容读写的核心](#三、Java IO 流:文件内容读写的核心)
-
- [1. 字节流:InputStream 和 OutputStream](#1. 字节流:InputStream 和 OutputStream)
- [2. 字符流:更方便的文本读写](#2. 字符流:更方便的文本读写)
-
- [(1)用 Scanner 读文本文件](#(1)用 Scanner 读文本文件)
- [(2)用 PrintWriter 写文本文件](#(2)用 PrintWriter 写文本文件)
- 四、实战案例:把知识点串起来用
-
- [案例 1:扫描目录,找到指定文件并删除](#案例 1:扫描目录,找到指定文件并删除)
- [案例 2:文件复制工具](#案例 2:文件复制工具)
- [案例 3:扫描目录,找到内容包含关键词的文件](#案例 3:扫描目录,找到内容包含关键词的文件)
- 五、新手避坑总结
前言:
对于刚接触 Java"文件操作"和"IO" 的小伙伴来说,"文件操作"和"IO 流"总像两座小山峰------听着有点难,实则只要找对路径,一步一步就能轻松登顶。今天这篇文章,就带着大家从基础概念到实战代码,把 Java 文件操作和 IO 流彻底搞明白。
一、先搞懂:文件和文件系统的基础认知
在写代码之前,我们得先明白"文件"到底是什么。狭义上的文件,是硬盘这种持久化存储设备中独立的数据单位 ,就像办公桌上一份份单独的文档,不仅有文字内容,还有文件名、类型、大小这些"附加信息"------我们把这些附加信息叫做"文件的元信息"。

比如你在电脑上看到的"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,不管当前在哪里,都能通过它找到文件;

- 相对路径 :从当前目录出发的路径,比如从"WindowsPowerShell"目录去"Windows NT",用
..\Windows NT就行(..代表父目录,.代表当前目录)。

拓展:即使是普通文件,根据其保存数据的不同,也经常被分为不同的类型,我们一般简单的划分为:
文本文件:保存被字符集编码的文本。
二进制文件:按照标准格式保存的非被字符集编码过的文件。
在Windows操作系统上,还有一类文件比较特殊,就是平时我们看到的快捷方式(shortcut),这种文件只是对真实文件的一种引用而已。其他操作系统上也有类似的概念,例如软链接(soft link)等。

最后,很多操作系统为了实现接口的统一性,将所有的 I/O 设备都抽象成了文件的概念,使用这一理念最为知名的就是 Unix、Linux 操作系统------万物皆文件。这种抽象设计能让操作系统对不同I/O设备(如硬盘、键盘、打印机等)的操作,都统一到文件操作的接口上,简化了开发和使用逻辑,无需为不同设备单独设计一套操作方式。
二、Java 中操作文件的"核心工具":File 类
Java 用 java.io.File 类来抽象描述一个文件(包括目录),但要注意:创建了 File 对象,不代表真实存在这个文件,它只是对文件的"描述"而已。
1. 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() | 判断用户是否对文件有可写权限 |
2. File 类实操:从获取信息到创建删除
(1)搞懂 get 系列方法:获取文件信息
比如想知道文件的父目录、名称、路径,用这几个方法就行,:
java
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()); // 输出绝对路径:D:\代码练习\文件示例1\..\hello-world.txt
System.out.println(file.getCanonicalPath()); // 输出简化绝对路径:D:\代码练习\hello-world.txt
}
}

(2)创建与删除文件:createNewFile() 和 delete()
java
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() 直接删除文件,不会进回收站,操作要谨慎。
(3)创建目录:mkdir() 和 mkdirs() 的区别
新手最容易踩的坑就是这两个方法的区别!
mkdir():只能创建单层目录,如果父目录不存在,创建失败;mkdirs():能创建多层目录(包括不存在的父目录)。
比如要创建"some-parent\some-dir"这个多层目录,用 mkdir() 会失败,用 mkdirs() 才能成功:
java
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());
}
}

这个区别一定要记牢,不然创建多层目录时会卡很久。
(4)文件重命名:renameTo()
java
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,所以先判断目标文件是否存在很重要。
三、Java IO 流:文件内容读写的核心
搞懂了文件操作,接下来就是"读写文件内容"------这就需要 IO 流了。可以把 IO 流比喻得很形象:读文件像"接水"(输入流 InputStream),写文件像"灌水"(输出流 OutputStream) ,我们就顺着这个逻辑来学。

1. 字节流:InputStream 和 OutputStream
字节流是最基础的 IO 流,以"字节"为单位读写数据,适合所有文件(比如文本、图片、视频)。
(1)InputStream:读文件内容
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":
单个字节读:
java
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);
}
}
}
}
数组读:
java
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 个字节:
java
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 的扫描读取 |
java
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(System.in)){
while(sc.hasNext()){
String s=sc.next();
System.out.print(s);
}
}
}
}
}
(2)OutputStream:写文件内容
OutputStream 也是抽象类,常用子类 FileOutputStream 写文件。核心方法是 write(),同样有三种用法,还有一个关键方法 flush()------因为 OutputStream 有缓冲区,数据会先存在内存,必须调用 flush() 才能把数据刷到硬盘。
比如写字符串"你好中国"到"output.txt":
java
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 表示追加)。

2. 字符流:更方便的文本读写
字节流读中文需要处理编码,很麻烦,这时候就需要"字符流"------按"字符"为单位读写,自动处理编码问题。用 Scanner 读字符,用 PrintWriter 写字符。
(1)用 Scanner 读文本文件
Scanner 能按行读文本,还能指定编码(比如 UTF-8),避免乱码。比如读"hello.txt"里的"你好中国":
java
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); // 输出:你好中国
}
}
}
}
}

这种方式比字节流简单多了,新手读文本文件优先用这个。
(2)用 PrintWriter 写文本文件
PrintWriter 有我们熟悉的 print()、println()、printf() 方法,写文本很方便,还能指定编码。
java
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 方便太多。
四、实战案例:把知识点串起来用
学完基础,必须通过实战巩固。通过一些经典案例,我们逐个拆解,新手跟着写一遍就能掌握。
案例 1:扫描目录,找到指定文件并删除
需求:输入根目录和关键词,找到文件名包含关键词的普通文件,询问用户是否删除。
核心思路:用"递归"遍历树形目录(因为文件系统是树形结构),找到符合条件的文件后处理。
java
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(),还有递归遍历,新手要理解递归的逻辑------"自己调用自己,处理子目录"。
案例 2:文件复制工具
需求:输入源文件路径和目标路径,实现文件复制(支持所有文件类型,比如文本、图片)。
核心思路:用 InputStream 读源文件,用 OutputStream 写目标文件,用字节数组做缓冲区提高效率。
java
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("复制完成!");
}
}

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

这个案例是 IO 流的经典应用,不管复制什么文件(文本、图片、视频)都能用,因为字节流不区分文件类型。
案例 3:扫描目录,找到内容包含关键词的文件
需求:输入根目录和关键词,找到文件名或内容包含关键词的普通文件。
核心思路:在案例 1 的基础上,增加"读文件内容判断"的逻辑,用 Scanner 读文件内容,判断是否包含关键词。
java
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 流的核心内容,最后再总结几个新手常踩的坑,帮你少走弯路:
- File 类不代表真实文件 :创建 File 对象只是"描述"文件,不代表文件存在,必须调用
createNewFile()或mkdirs()才会真实创建; - mkdir() 和 mkdirs() 别用混 :创建多层目录一定要用
mkdirs(); - IO 流必须关闭 :用
try-with-resources语法,自动关闭流,避免资源泄漏; - OutputStream 要 flush() :写完数据必须调用
flush(),否则数据可能留在缓冲区,没写到硬盘; - 读中文要指定编码:用 Scanner 或 OutputStreamWriter 时,明确指定 UTF-8,避免乱码。
至此,Java 文件操作和 IO 流的核心知识和实战就讲完了。其实这些内容并不难,关键是多写代码、多跑案例------把文中的代码逐个复制到 IDE 里运行,改改参数(比如换个文件路径、关键词),很快就能熟练掌握。告别小白,从搞定文件操作和 IO 流开始吧!