Java 从入门到精通十二File 与 IO 流基础为什么程序“读写文件”时总是容易出问题前一篇我们把异常处理与自定义异常讲清楚了。但很多人学完异常之后真正第一次强烈感受到“异常不是语法题而是工程题”的场景往往不是算术运算也不是集合操作而是文件读写。比如你很快就会遇到这些问题为什么同样一段代码昨天能读文件今天就报错为什么文件明明存在程序却说找不到为什么写进去的中文打开后变成乱码为什么复制文件时一不小心就把内容写坏了为什么代码里已经close()了资源问题还是层出不穷这些问题表面看起来零散背后其实都指向同一件事程序和外部世界打交道时事情就不再像内存里的变量那样“理所当然”。内存里的数据是瞬时的程序一停就没了但文件是落在磁盘上的是持久化的是要跨时间、跨程序、甚至跨机器被读取的。所以学 File 与 IO不是为了背几个类名而是为了建立一套很重要的理解Java 里“文件”到底怎么表示程序是如何把数据写出去、再读回来的为什么字符、字节、路径、缓冲区这些概念必须分清怎样写出不容易出错的基础读写代码这一篇不追求一次把所有 IO API 背完而是先把最核心的骨架搭起来。一、先搞清楚什么是 File什么是 IO很多初学者会把这两个词混在一起。其实它们不是一回事。1File表示“文件或目录”这个对象在 Java 里File更像是对磁盘路径的一种抽象表示。比如FilefilenewFile(demo.txt);这并不等于“文件内容已经读进来了”。它只是告诉 Java这里有个路径这个路径可能指向一个文件也可能指向一个目录你可以基于它去判断是否存在、是否可读、是否可写、是否创建所以你要先记住File主要负责“描述文件”不负责真正“读写内容”。2IOInput / OutputIO 就是输入输出。站在程序角度Input把外部数据读进程序Output把程序中的数据写到外部例如从磁盘读取文本文件把日志写入文件从网络读取数据向控制台输出内容它们本质上都属于 IO。所以这两个概念的关系可以简单理解成File告诉你“目标是谁”IO告诉你“数据怎么进出”二、为什么文件操作总比普通变量更容易出问题因为变量大多活在内存里环境相对可控。例如intage18;StringnameTom;这类代码如果报错通常原因很直接。但文件操作会额外受很多外部因素影响文件是否真的存在路径写得对不对程序有没有权限访问当前工作目录是不是你以为的那个目录读写时编码是否一致资源有没有及时关闭所以 IO 之所以让初学者头疼不是因为 API 难而是因为它天然带着不确定性。这也是为什么文件读写和异常处理经常是连在一起学的。三、File 类最常用的能力有哪些先看一个最基础的例子importjava.io.File;publicclassDemo{publicstaticvoidmain(String[]args){FilefilenewFile(test.txt);System.out.println(文件是否存在file.exists());System.out.println(是不是文件file.isFile());System.out.println(是不是目录file.isDirectory());System.out.println(文件名file.getName());System.out.println(绝对路径file.getAbsolutePath());}}这些方法非常适合做“前置判断”。比如你要读一个文件最好不要上来就直接读而是先确认它存在吗它真的是文件吗路径是不是你想要的那个很多“找不到文件”的问题本质上不是文件丢了而是你以为程序在 A 目录运行实际上它在 B 目录运行。所以getAbsolutePath()这个方法调试时非常有用。四、创建文件和目录时要注意什么1创建文件importjava.io.File;importjava.io.IOException;publicclassDemo{publicstaticvoidmain(String[]args)throwsIOException{FilefilenewFile(note.txt);if(!file.exists()){file.createNewFile();}}}这里的createNewFile()会抛IOException因为创建文件这件事并不是百分之百能成功。可能失败的原因包括路径不合法没有权限磁盘或目录状态异常2创建目录FiledirnewFile(data);if(!dir.exists()){dir.mkdir();}如果要创建多级目录更常用的是dir.mkdirs();区别是mkdir()只能创建单级目录mkdirs()可以连父目录一起创建这个细节很常见也很容易写错。五、为什么 IO 要分“字节流”和“字符流”这是 File 与 IO 入门里最重要的一个分界线。1字节流适合处理一切二进制数据字节流按byte来读写。典型类InputStreamOutputStreamFileInputStreamFileOutputStream它适合处理图片视频音频压缩包以及任何“你不想擅自按字符解释”的数据2字符流适合处理文本字符流按char来读写。典型类ReaderWriterFileReaderFileWriter它更适合处理普通文本配置文件日志内容用户可读字符串为什么一定要区分因为“文本”最终在底层也是字节。字符流做的事情本质上是帮你处理“字节 ↔ 字符”之间的转换。如果你处理的是纯文本用字符流通常更自然。如果你处理的是图片、压缩包这类二进制内容用字符流就可能直接把数据搞坏。所以不要死记先记原则文本优先考虑字符流二进制优先考虑字节流六、先看最基础的字节输出把内容写进文件importjava.io.FileOutputStream;importjava.io.IOException;publicclassDemo{publicstaticvoidmain(String[]args){try(FileOutputStreamfosnewFileOutputStream(a.txt)){fos.write(65);fos.write(66);fos.write(67);}catch(IOExceptione){e.printStackTrace();}}}运行后文件里会出现ABC因为65 对应字符 A66 对应字符 B67 对应字符 C但你也会发现这种写法并不直观。实际开发里更常见的是写字节数组try(FileOutputStreamfosnewFileOutputStream(a.txt)){Stringstrhello;fos.write(str.getBytes());}catch(IOExceptione){e.printStackTrace();}这里的getBytes()本质上是把字符串转成字节数组再写出去。七、读取文件时在做什么importjava.io.FileInputStream;importjava.io.IOException;publicclassDemo{publicstaticvoidmain(String[]args){try(FileInputStreamfisnewFileInputStream(a.txt)){intdata;while((datafis.read())!-1){System.out.print((char)data);}}catch(IOExceptione){e.printStackTrace();}}}这里要理解两个关键点。1read()每次读一个字节它返回的是一个int不是byte。原因是 Java 需要用-1表示“读到文件末尾了”。2为什么还能强转成char因为这个例子里文件内容刚好是普通英文字符。但如果内容是中文或者编码不一致这种写法就容易出现乱码。所以它更适合作为“理解 IO 原理”的示例而不是直接照搬到所有文本读取场景。八、字符流为什么更适合读文本如果你读取的是文本字符流代码通常更顺手。例如importjava.io.FileReader;importjava.io.IOException;publicclassDemo{publicstaticvoidmain(String[]args){try(FileReaderfrnewFileReader(a.txt)){intch;while((chfr.read())!-1){System.out.print((char)ch);}}catch(IOExceptione){e.printStackTrace();}}}它和字节流看起来很像但思路上已经更偏向“文本字符”而不是“原始字节”。对应地写文本也可以用importjava.io.FileWriter;importjava.io.IOException;publicclassDemo{publicstaticvoidmain(String[]args){try(FileWriterfwnewFileWriter(b.txt)){fw.write(Java IO 入门);fw.write(\n学会读写文件很重要);}catch(IOExceptione){e.printStackTrace();}}}这类写法更适合初学者理解“写文本文件”这件事。九、为什么现在更推荐 try-with-resources很多旧教程会这样写FileInputStreamfisnull;try{fisnewFileInputStream(a.txt);// 读取逻辑}catch(IOExceptione){e.printStackTrace();}finally{if(fis!null){try{fis.close();}catch(IOExceptione){e.printStackTrace();}}}这当然没错但太啰嗦而且很容易漏。现在更推荐try(FileInputStreamfisnewFileInputStream(a.txt)){// 读取逻辑}catch(IOExceptione){e.printStackTrace();}因为它会自动帮你关闭资源。这非常重要。文件流、网络流、数据库连接本质上都属于“资源”。如果你总忘记关轻则浪费资源重则导致句柄耗尽、文件被占用、程序异常。所以从工程习惯上说能用 try-with-resources就尽量别手写 finally 关闭。十、初学 IO 最容易踩的 5 个坑1把相对路径想当然比如你写newFile(data.txt)你以为它在项目根目录实际上它可能相对于当前运行目录。所以一旦出问题先打印System.out.println(file.getAbsolutePath());2文本和二进制不分文本优先字符流图片、压缩包、音视频优先字节流别混着来。3读写完不关流这会带来各种隐蔽问题。现在直接养成try-with-resources的习惯。4忽略编码问题最典型的表现就是写进去是中文打开之后变乱码这通常不是“Java 坏了”而是编码没处理一致。5一次只读一个字符/字节还觉得没问题教学示例可以这样写但真实开发里通常会结合缓冲数组、缓冲流来提升效率。这也是为什么下一篇继续讲 IO 时通常就会进入缓冲流高效复制编码转换更现代的 NIO十一、你现在应该建立的不是 API 记忆而是 IO 思维学到这里真正重要的不是你能不能一口气背出所有流类名而是你有没有形成下面这套判断当你要操作文件时先问自己 4 个问题目标是什么文件还是目录路径是否正确处理的是什么数据文本还是二进制应该用什么流字节流还是字符流资源怎么安全关闭是否使用 try-with-resources如果你形成了这套思路后面学缓冲流、转换流、NIO、序列化时就不会乱。十二、最后总结这一篇你要真正带走的不是“File 和 IO 有哪些类”而是这几个核心认识1File主要负责描述路径和文件信息它不负责真正的内容读写。2IO 的本质是数据在程序和外部世界之间流动输入是读进来输出是写出去。3字节流和字符流必须分清文本优先字符流二进制优先字节流4文件操作天然带着不确定性所以异常处理和资源关闭非常重要。5try-with-resources是现代 Java 基础习惯能自动关流就别手动把代码写复杂。如果前面的面向对象、集合、泛型是在帮你把“程序内部的数据结构”搭起来那么 File 与 IO 做的事情就是把程序真正接到外部世界上。从这一篇开始你写的代码就不再只是“在控制台跑一下”而是会开始读配置写日志保存数据处理文件这一步很关键。因为很多真正像“软件”的程序都是从这里开始有了现实感。