Java文件与IO流完全指南
🎯 适合人群:Java小白到进阶开发者
🧭 学习目标:读懂并正确使用 File、InputStream/OutputStream、Reader/Writer、缓冲流、编码与性能优化
⏱️ 阅读时长:约30-40分钟
🗺️ 导读:为什么要学 IO 流?
- 程序要与“外部世界”交互——读写文件、网络传输、控制台输入输出,本质都是“数据流”。
- Java 提供了两大类 IO:
- 字节流:
InputStream/OutputStream(处理二进制:图片、音频、视频、任意文件) - 字符流:
Reader/Writer(处理文本:txt、md、json、java 源码等)
- 字节流:
- 记忆口诀:
- “看得懂的是字符流(文本),看不懂的是字节流(二进制)”。
- “文本优先用字符流,其他都用字节流,拿不准选字节流”。
🧱 基石:File 类(文件与目录)
java.io.File 不是“文件内容”,而是“文件或目录的路径抽象”。常见用法:
import java.io.File;public class FileBasicsDemo {public static void main(String[] args) {// 1) 路径与构造File file = new File("D:/IO/hello.txt"); // 建议使用 / 或者使用 Paths 构建File dir = new File("D:/IO/sub");// 2) 基本信息System.out.println(file.getAbsolutePath());System.out.println(file.getName());System.out.println(file.exists());System.out.println(file.isFile());System.out.println(file.isDirectory());// 3) 创建目录/文件if (!dir.exists()) {boolean ok = dir.mkdirs(); // 递归创建目录System.out.println("mkdirs: " + ok);}// 创建新文件(若父目录不存在会失败)try {if (!file.exists()) {boolean created = file.createNewFile();System.out.println("createNewFile: " + created);}} catch (Exception e) {e.printStackTrace();}// 4) 遍历目录File root = new File("D:/IO");File[] children = root.listFiles();if (children != null) {for (File f : children) {System.out.println((f.isDirectory() ? "[DIR] " : "[FILE]") + f.getName());}}}
}
提示:在 Windows 上用 "D:/path/file.txt" 更稳妥,避免 \\ 转义麻烦。生产代码推荐 java.nio.file.Paths 与 Files(NIO.2),本文以 IO 基础为主。
🧩 两大体系的核心区别
- 字节流(8-bit):
InputStream/OutputStream,按“字节”处理,适合任何数据; - 字符流(16-bit):
Reader/Writer,按“字符”处理,关注“编码”,适合文本; - 关系:字符流往往是“基于字节流 + 编码解码器(
InputStreamReader/OutputStreamWriter)”。
✍️ 字符流(Reader/Writer):处理文本最顺手
1)字符输入:Reader 家族
Reader:抽象基类- 常见实现:
FileReader:快捷读取文件文本(使用平台默认编码,不建议在生产环境依赖默认编码)InputStreamReader:桥接器,将字节流解码为字符流,可指定编码(推荐)BufferedReader:带缓冲、支持readLine(),高效读取
修正与提升后的综合示例(包含三种读取方式,改为 try-with-resources、指定编码、健壮性更好):
import java.io.*;
import java.nio.charset.StandardCharsets;public class ReaderDemo {public static void main(String[] args) {String path = "D:/IdeaProjects/JavaTest/src/com/qcby/a.txt";// 1) FileReader:简单,但依赖平台默认编码(不推荐在生产使用)try (FileReader reader = new FileReader(path)) {int ch;while ((ch = reader.read()) != -1) {System.out.print((char) ch);}} catch (IOException e) {e.printStackTrace();}// 2) InputStreamReader:可指定编码(推荐)try (InputStreamReader reader = new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8)) {int ch;while ((ch = reader.read()) != -1) {System.out.print((char) ch);}} catch (IOException e) {e.printStackTrace();}// 3) BufferedReader:高效且支持按行读取try (BufferedReader br = new BufferedReader(new FileReader(path))) {String line;while ((line = br.readLine()) != null) {System.out.println(line);}} catch (IOException e) {e.printStackTrace();}}
}
说明:
FileReader与FileWriter在编码未知或跨平台时不够稳妥,实际开发更推荐InputStreamReader/OutputStreamWriter并显式指定编码。
2)字符输出:Writer 家族
Writer:抽象基类- 常见实现:
FileWriter:快捷写文本(默认编码,不建议长期依赖)OutputStreamWriter:桥接器,将字符流编码为字节流,可指定编码(推荐)BufferedWriter:带缓冲、newLine()友好换行
综合示例(修正:使用 try-with-resources,必要时 flush/close,路径与编码更明确):
import java.io.*;
import java.nio.charset.StandardCharsets;public class WriterDemo {public static void main(String[] args) {String outPath = "D:/IO/output.txt";// 1) FileWriter:简单写文本try (FileWriter writer = new FileWriter(outPath)) { // 默认编码writer.write("Hello, World!\n");writer.write("This is a new line.\n");} catch (IOException e) {e.printStackTrace();}// 2) OutputStreamWriter:指定编码(推荐)try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(outPath, true), StandardCharsets.UTF_8)) { // 追加写writer.write("追加一行,使用UTF-8编码。\n");} catch (IOException e) {e.printStackTrace();}// 3) BufferedWriter:高效按行写try (BufferedWriter bw = new BufferedWriter(new FileWriter(outPath, true))) {bw.write("Hello, World!");bw.newLine();bw.write("This is a new line.");} catch (IOException e) {e.printStackTrace();}}
}
3)Writer 的五种 write 方法(快速对照)
write(int c):写入单个字符(低 16 位有效)write(char[] cbuf):写入字符数组write(char[] cbuf, int off, int len):写入字符数组的一部分write(String str):写入字符串write(String str, int off, int len):写入字符串的一部分
示例(修正笔记,补全导入与路径):
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;public class WriterExample {public static void main(String[] args) {String out = "D:/IO/writer-demo.txt";try (Writer writer = new FileWriter(out)) {// 1. 写入单个字符writer.write('H');// 2. 写入字符数组char[] array = {'e', 'l', 'l', 'o'};writer.write(array);// 3. 写入字符数组的一部分(写入 "ll")writer.write(array, 2, 2);// 4. 写入字符串writer.write(", World!");// 5. 写入字符串的一部分(写入 "This is Java")String str = "\nThis is Java IO.";writer.write(str, 1, 12);System.out.println("数据已写入文件!");} catch (IOException e) {e.printStackTrace();}}
}
📦 字节流(InputStream/OutputStream):万能读写器
1)字节输入:InputStream 家族
- 核心方法:
int read():读 1 字节(0-255),EOF 返回 -1int read(byte[] b):批量读取到数组,返回本次读取长度或 -1int read(byte[] b, int off, int len):从 off 开始最多读 len 个字节void close():关闭流并释放资源
- 常见实现:
FileInputStream、BufferedInputStream、ByteArrayInputStream等
基于笔记修正后的示例:
import java.io.*;
import java.nio.charset.StandardCharsets;public class FileInputStreamTest {public static void main(String[] args) {File file = new File("D:/IO/hello.txt");// 建议:优先使用 try-with-resources,避免手动 finally 关闭try (FileInputStream fis = new FileInputStream(file)) {byte[] buffer = new byte[1024];int len;while ((len = fis.read(buffer)) != -1) {// 如果是文本,可按编码构造字符串;若是二进制,直接处理字节String chunk = new String(buffer, 0, len, StandardCharsets.UTF_8);System.out.print(chunk);}} catch (IOException e) {e.printStackTrace();}}
}
注意:如果文件不是 UTF-8 文本,上面把字节解码为字符串可能出现乱码。处理图片/音频/视频等二进制文件时,不要把字节强行转成字符串。
2)字节输出:OutputStream 家族
- 核心方法:
void write(int b):写 1 个字节(低 8 位有效)void write(byte[] b):写入整个字节数组void write(byte[] b, int off, int len):写入数组的一部分void flush():刷新缓冲区(有缓冲的输出流/Writer)void close():关闭流并释放资源
- 常见实现:
FileOutputStream、BufferedOutputStream、ByteArrayOutputStream等
增强示例(追加写入、换行、片段写入):
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;public class FileOutputStreamTest {public static void main(String[] args) {File file = new File("D:/IO/hello.txt");// true 表示追加写try (FileOutputStream fos = new FileOutputStream(file, true)) {fos.write(97); // 写入单字节 'a'fos.write("\r\n".getBytes(StandardCharsets.UTF_8));fos.write("中国人!\r\n".getBytes(StandardCharsets.UTF_8));fos.write("ABCDEFGH".getBytes(StandardCharsets.UTF_8), 2, 4); // 输出 CDEF} catch (IOException e) {e.printStackTrace();}}
}
Windows 的换行是
\r\n,Linux/Unix 是\n。跨平台建议使用System.lineSeparator()或BufferedWriter.newLine()。
🚀 性能与最佳实践
- 使用缓冲:
BufferedInputStream/BufferedOutputStream/BufferedReader/BufferedWriter - 大块读写:优先
byte[]/char[]缓冲区读写,减少系统调用 - 明确编码:文本读写显式指定
Charset(如StandardCharsets.UTF_8) - 及时关闭:使用
try-with-resources自动关闭(JDK 7+) - 追加写入:输出构造器带
append = true,如new FileOutputStream(path, true) - 避免小而频繁的
read()/write():合并为批量操作
示例:拷贝任意文件(字节流 + 缓冲 + 大块读写)
import java.io.*;public class FileCopy {public static void copyFile(String src, String dest) throws IOException {try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(src));BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(dest))) {byte[] buf = new byte[8192];int len;while ((len = in.read(buf)) != -1) {out.write(buf, 0, len);}}}public static void main(String[] args) throws IOException {copyFile("D:/IO/source.bin", "D:/IO/target.bin");}
}
🧠 字符编码与常见坑
FileReader/FileWriter使用“平台默认编码”,在不同机器/IDE/系统下可能不同,易乱码。- 推荐始终显式使用编码:
- 读:
new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8) - 写:
new OutputStreamWriter(new FileOutputStream(path), StandardCharsets.UTF_8)
- 读:
- 文本文件中若包含表情/罕见字符,优先
UTF-8(覆盖更全)。
🧭 什么时候用字节流?什么时候用字符流?
- “只要涉及文本内容且你需要‘字符’语义(如按行、按字符处理),用字符流”。
- “不确定是不是文本、或需要按原始字节处理(如复制图片/压缩包/视频),用字节流”。
- “需要指定编码(文本跨平台),用 InputStreamReader/OutputStreamWriter 代替 FileReader/FileWriter”。
🧰 实战:综合示例(读取一个 UTF-8 文本,转换后写入新文件)
import java.io.*;
import java.nio.charset.StandardCharsets;public class TextTransformDemo {public static void main(String[] args) {String src = "D:/IO/input-utf8.txt";String dest = "D:/IO/output-utf8.txt";try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(src), StandardCharsets.UTF_8));BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(dest), StandardCharsets.UTF_8))) {String line;int lineNo = 1;while ((line = br.readLine()) != null) {// 简单转换:在每行前加上行号bw.write(String.format("%04d: %s", lineNo++, line));bw.newLine();}} catch (IOException e) {e.printStackTrace();}}
}
🧩 进阶一瞥:NIO 与 Files(了解即可)
java.nio.file.Files/Paths提供更现代的 API:Files.readAllLines(path, charset)读所有行Files.write(path, bytes, options...)写入字节Files.copy/Files.move/Files.delete文件操作
FileChannel、MappedByteBuffer可进行高性能文件读写(大文件、零拷贝场景)。
❗ 常见错误与改正
- 错误:在
try外创建流,在finally中close()时忽略null判断
修正:使用try-with-resources自动关闭。 - 错误:文本使用字节流读取后直接
new String(bytes)未指定编码
修正:显式使用Charset;或改用字符流。 - 错误:频繁
read()/write()单字节
修正:使用缓冲流 + 批量读写。 - 错误:路径用
C:\\a\\b.txt忘记转义
修正:改用正斜杠C:/a/b.txt或Paths.get("C:", "a", "b.txt")。
🎯 面试常见问题与答案
1)为什么有了字节流还需要字符流?
- 字节流面向原始数据,不关心编码;字符流关注“字符语义”,内置解码/编码,更适合文本操作(按行、按字符)。
2)FileReader 与 InputStreamReader 区别?
FileReader是简化版字符输入,使用平台默认编码;InputStreamReader是“字节→字符”的桥接器,可显式指定编码,跨平台更可靠,推荐。
3)BufferedReader.readLine() 为什么常用?
- 带缓冲、按行读取,避免频繁系统调用,性能更好;并且直接得到“行”这个文本级别的语义单位。
4)hashCode/equals 和 IO 有关系吗?
- 直接关系不大,但在使用
Set/Map统计文件名、缓存流对象时会涉及相等性与散列。IO 场景更多关注性能、正确编码与资源释放。
5)如何高效复制大文件?
- 使用
BufferedInputStream+BufferedOutputStream+ 大缓冲区(8KB~1MB),或使用 NIO 的Files.copy/FileChannel.transferTo/transferFrom。
6)什么时候需要 flush()?
- 对“带缓冲”的输出流/Writer,在你希望“立即”写出数据时(如网络/日志/交互式输出),或程序即将结束但未关闭流时需要
flush()。
7)为什么 Windows 下是 \r\n 换行?
- 历史原因;跨平台代码建议使用
System.lineSeparator()或BufferedWriter.newLine()来统一处理。
8)读取二进制文件时出现乱码怎么办?
- 不要把二进制“按字符”解码,直接按字节处理;只有在确定编码并且是文本时,才使用字符流或按正确编码构造字符串。
9)File 与 Files 的区别?
File是老 IO 的路径抽象,Files是 NIO.2 的工具类,提供更丰富、更现代的文件操作 API。
10)try-with-resources 的底层原理?
- 编译器语法糖:会在编译期生成
try { ... } finally { resource.close(); }结构,并按“逆序”关闭多个资源。
✅ 小结
- 分清字节流与字符流,记住“文本=字符流,其他=字节流(拿不准选字节流)”。
- 文本必须重视编码,推荐
UTF-8,用InputStreamReader/OutputStreamWriter显式指定。 - 性能优化三件套:缓冲、批量读写、try-with-resources。
File只代表路径,现代项目逐步拥抱 NIO.2(Paths/Files)。
如果这篇文章对你有帮助,欢迎点赞、收藏、评论交流!也欢迎把你经常踩的“IO 坑”留言,我来补充示例与讲解~
