【JavaEE初阶】告别小白!Java IO 流读写 + 文件操作实战

我的个人主页我的专栏:人工智能领域、java-数据结构、Javase、C语言,MySQL,JavaEE初阶,希望能帮助到大家!!!点赞👍收藏❤
目录
前言:
对于刚接触 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 系列方法:获取文件信息
比如想知道文件的父目录、名称、路径,用这几个方法就行,:
importjava.io.File;importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{// 这里的文件不一定真实存在File file =newFile("..\\hello-world.txt");System.out.println(file.getParent());// 输出父目录:..System.out.println(file.getName());// 输出文件名:hello-world.txtSystem.out.println(file.getPath());// 输出路径:..\hello-world.txtSystem.out.println(file.getAbsolutePath());// 输出绝对路径:D:\代码练习\文件示例1\..\hello-world.txtSystem.out.println(file.getCanonicalPath());// 输出简化绝对路径:D:\代码练习\hello-world.txt}}
(2)创建与删除文件:createNewFile() 和 delete()
importjava.io.File;importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{File file =newFile("hello-world.txt");// 确保文件初始不存在System.out.println(file.exists());// 初始不存在:falseSystem.out.println(file.createNewFile());// 创建成功:trueSystem.out.println(file.exists());// 创建后存在:trueSystem.out.println(file.isFile());// 是普通文件:trueSystem.out.println(file.delete());// 删除成功:trueSystem.out.println(file.exists());// 删除后不存在:false}}
这里要注意:createNewFile() 只能创建普通文件,不能创建目录;delete() 直接删除文件,不会进回收站,操作要谨慎。
(3)创建目录:mkdir() 和 mkdirs() 的区别
新手最容易踩的坑就是这两个方法的区别!
mkdir():只能创建单层目录,如果父目录不存在,创建失败;mkdirs():能创建多层目录(包括不存在的父目录)。
比如要创建“some-parent\some-dir”这个多层目录,用 mkdir() 会失败,用 mkdirs() 才能成功:
package IO;importjava.io.File;publicclass demo3 {publicstaticvoidmain(String[] args){File dir=newFile("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()
importjava.io.File;importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{File oldFile =newFile("some-file.txt");// 确保该文件存在File newFile =newFile("dest.txt");// 确保该文件不存在System.out.println(oldFile.exists());System.out.println(newFile.exists());System.out.println(oldFile.renameTo(newFile));// 重命名成功:trueSystem.out.println(oldFile.exists());// 原文件不存在:falseSystem.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”:
单个字节读:
package IO;importjava.io.FileInputStream;importjava.io.IOException;importjava.io.InputStream;publicclass demo5 {//需要在项目目录下创建hello.txt文件publicstaticvoidmain(String[] args)throwsIOException{try(InputStream is=newFileInputStream("hello.txt")){while(true){int b=is.read();if(b==-1){break;}System.out.printf("%c",b);}}}}数组读:
package IO;importjava.io.FileInputStream;importjava.io.IOException;importjava.io.InputStream;publicclass demo6 {publicstaticvoidmain(String[] args)throwsIOException{try(InputStream is=newFileInputStream("hello.txt")){byte[] buf=newbyte[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;importjava.io.FileInputStream;importjava.io.IOException;importjava.io.InputStream;publicclass demo7 {publicstaticvoidmain(String[] args)throwsIOException{try(InputStream is=newFileInputStream("hello.txt")){byte[] buf=newbyte[1024];int len;while(true){ len=is.read(buf);if(len==-1){break;}for(int i=0;i<len;i+=3){String s=newString(buf,i,3,"UTF-8");System.out.printf("%s",s);}}}}}
我们看到了对字符类型直接使用 InputStream 进行读取是非常麻烦且困难的,所以,我们使用一种我们之前比较熟悉的类来完成该工作,就是Scanner 类。
| 构造方法 | 说明 |
|---|---|
| Scanner(InputStream is, String charset) | 使用 charset 字符集进行 is 的扫描读取 |
package IO;importjava.io.FileInputStream;importjava.io.IOException;importjava.io.InputStream;importjava.util.Scanner;publicclass demo8 {publicstaticvoidmain(String[] args)throwsIOException{try(InputStream is=newFileInputStream("hello.txt")){try(Scanner sc=newScanner(System.in)){while(sc.hasNext()){String s=sc.next();System.out.print(s);}}}}}(2)OutputStream:写文件内容
OutputStream 也是抽象类,常用子类 FileOutputStream 写文件。核心方法是 write(),同样有三种用法,还有一个关键方法 flush()——因为 OutputStream 有缓冲区,数据会先存在内存,必须调用 flush() 才能把数据刷到硬盘。
比如写字符串“你好中国”到“output.txt”:
importjava.io.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{try(OutputStream os =newFileOutputStream("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”里的“你好中国”:
importjava.io.*;importjava.util.Scanner;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{try(InputStream is =newFileInputStream("hello.txt")){// 指定 UTF-8 编码,避免中文乱码try(Scanner scanner =newScanner(is,"UTF-8")){while(scanner.hasNextLine()){// 按行读String line = scanner.nextLine();System.out.println(line);// 输出:你好中国}}}}}
这种方式比字节流简单多了,新手读文本文件优先用这个。
(2)用 PrintWriter 写文本文件
PrintWriter 有我们熟悉的 print()、println()、printf() 方法,写文本很方便,还能指定编码。
importjava.io.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{try(OutputStream os =newFileOutputStream("output.txt")){// 先转成 OutputStreamWriter,指定 UTF-8 编码try(OutputStreamWriter osWriter =newOutputStreamWriter(os,"UTF-8")){// 用 PrintWriter 写内容try(PrintWriter writer =newPrintWriter(osWriter)){ writer.println("我是第一行");// 换行 writer.print("我是第二行");// 不换行 writer.printf("%d: 我是第三行\n",3);// 格式化输出 writer.flush();// 刷新到硬盘}}}}}
运行后“output.txt”里会有三行内容,格式清晰,比直接用 OutputStream 方便太多。
四、实战案例:把知识点串起来用
学完基础,必须通过实战巩固。通过一些经典案例,我们逐个拆解,新手跟着写一遍就能掌握。
案例 1:扫描目录,找到指定文件并删除
需求:输入根目录和关键词,找到文件名包含关键词的普通文件,询问用户是否删除。
核心思路:用“递归”遍历树形目录(因为文件系统是树形结构),找到符合条件的文件后处理。
importjava.io.*;importjava.util.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{Scanner scanner =newScanner(System.in);// 输入根目录System.out.print("请输入要扫描的根目录(绝对路径/相对路径):");String rootDirPath = scanner.next();File rootDir =newFile(rootDirPath);if(!rootDir.isDirectory()){System.out.println("根目录不存在或不是目录,退出!");return;}// 输入关键词System.out.print("请输入文件名包含的字符:");String token = scanner.next();List<File> result =newArrayList<>();// 递归扫描目录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("已删除!");}}}// 递归扫描目录的方法privatestaticvoidscanDir(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 写目标文件,用字节数组做缓冲区提高效率。
importjava.io.*;importjava.util.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{Scanner scanner =newScanner(System.in);// 输入源文件System.out.print("请输入要复制的文件路径:");String sourcePath = scanner.next();File sourceFile =newFile(sourcePath);if(!sourceFile.exists()){System.out.println("源文件不存在!");return;}if(!sourceFile.isFile()){System.out.println("不是普通文件,无法复制!");return;}// 输入目标路径System.out.print("请输入目标路径:");String destPath = scanner.next();File destFile =newFile(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 =newFileInputStream(sourceFile);OutputStream os =newFileOutputStream(destFile)){byte[] buf =newbyte[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 读文件内容,判断是否包含关键词。
importjava.io.*;importjava.util.*;publicclassMain{publicstaticvoidmain(String[] args)throwsIOException{Scanner scanner =newScanner(System.in);System.out.print("请输入要扫描的根目录:");String rootDirPath = scanner.next();File rootDir =newFile(rootDirPath);if(!rootDir.isDirectory()){System.out.println("根目录无效,退出!");return;}System.out.print("请输入要查找的关键词:");String token = scanner.next();List<File> result =newArrayList<>();scanDirWithContent(rootDir, token, result);System.out.println("共找到 "+ result.size()+" 个文件:");for(File file : result){System.out.println(file.getCanonicalPath());}}// 递归扫描目录,判断文件名或内容privatestaticvoidscanDirWithContent(File rootDir,String token,List<File> result)throwsIOException{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 处理)privatestaticbooleanisContentContains(File file,String token)throwsIOException{StringBuilder sb =newStringBuilder();try(InputStream is =newFileInputStream(file);Scanner scanner =newScanner(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 流开始吧!