Java 文本文件读写全面指南:从经典 IO 到现代 NIO 的演进与实践
一、经典 IO (java.io
包)
这是最传统和广为人知的方式,核心是 Reader
和 Writer
这两个抽象类,用于处理字符文本。
1. FileReader
/ FileWriter
这是最基础的实现,用于直接读写文件。
写文件示例:
import java.io.FileWriter;import java.io.IOException;import java.util.ArrayList;import java.util.List;public class Demo { public static void main(String[] args) { List contents = new ArrayList(); contents.add(\"test1\"); contents.add(\"test2\"); write(\"D:\\\\test.txt\", contents); } private static void write(String fileName, List contents) { try (FileWriter writer = new FileWriter(fileName)) { for (String content : contents) { writer.write(content + \"\\n\"); } } catch (IOException e) { e.printStackTrace(); } }}
读文件示例:
import java.io.FileReader;import java.io.IOException;public class Demo { public static void main(String[] args) { read(\"D:\\\\test.txt\"); } private static void read(String fileName) { try (FileReader reader = new FileReader(fileName)) { int charCode; while ((charCode = reader.read()) != -1) { System.out.print((char) charCode); } } catch (IOException e) { e.printStackTrace(); } }}
-
优点:
-
API 简单直观,易于学习和使用。
-
-
缺点:
-
性能极差:
FileReader
的read()
方法是逐字符读取的,会产生大量的系统调用。 -
功能单一:没有缓冲区,不支持按行读取等高级功能。
-
编码问题:使用平台默认的字符编码(如 Windows 上是 GBK),无法指定编码,容易导致乱码。这是最大的坑。
-
-
适用场景:
-
基本不用。除非是处理非常小的、简单的、且编码与系统默认一致的文件,否则不推荐使用。
-
2. BufferedReader
/ BufferedWriter
在基础流之上包装了缓冲区,是最常用和经典的高效文本处理方式。
写文件示例 (BufferedWriter):
import java.io.BufferedWriter;import java.io.FileWriter;import java.io.IOException;import java.util.ArrayList;import java.util.List;public class Demo { public static void main(String[] args) { List contents = new ArrayList(); contents.add(\"test1\"); contents.add(\"test2\"); write(\"D:\\\\test.txt\", contents); } private static void write(String fileName, List contents) { try (FileWriter fw = new FileWriter(fileName); BufferedWriter writer = new BufferedWriter(fw)) { for (String content : contents) { writer.write(content); writer.newLine(); // 换行,平台无关,比写 \"\\n\" 更好 } } catch (IOException e) { e.printStackTrace(); } }}
读文件示例 (BufferedReader):
import java.io.BufferedReader;import java.io.FileReader;import java.io.IOException;public class Demo { public static void main(String[] args) { read(\"D:\\\\test.txt\"); } private static void read(String fileName) { try (FileReader fr = new FileReader(fileName); BufferedReader reader = new BufferedReader(fr)) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } }}
-
优点:
-
性能高:内置缓冲区(默认8KB),大大减少了底层系统的读写次数。
-
功能强大:
BufferedReader.readLine()
是处理文本文件的神器,非常适合逻辑按行处理的场景。
-
-
缺点:
-
仍然依赖于
FileReader
/FileWriter
,因此默认编码问题依然存在。
-
-
适用场景:
-
需要按行处理文本内容的绝大多数情况(如日志分析、数据文件读取)。
-
读写普通大小的文本文件。是 Java 7 之前事实上的标准。
-
3. InputStreamReader
/ OutputStreamWriter
(解决编码问题的关键)
这是桥梁,用于将字节流转换为字符流,并且可以显式指定字符编码。
写文件示例 (指定UTF-8编码):
import java.io.BufferedWriter;import java.io.FileOutputStream;import java.io.IOException;import java.io.OutputStreamWriter;import java.nio.charset.StandardCharsets;import java.util.ArrayList;import java.util.List;public class Demo { public static void main(String[] args) { List contents = new ArrayList(); contents.add(\"test1\"); contents.add(\"test2\"); write(\"D:\\\\test.txt\", contents); } private static void write(String fileName, List contents) { try (FileOutputStream fos = new FileOutputStream(fileName); OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); // 指定编码 BufferedWriter writer = new BufferedWriter(osw)) { // 再用Buffer包装,提升性能 for (String content : contents) { writer.write(content); writer.newLine(); } } catch (IOException e) { e.printStackTrace(); } }}
读文件示例 (指定UTF-8编码):
import java.io.BufferedReader;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStreamReader;import java.nio.charset.StandardCharsets;public class Demo { public static void main(String[] args) { read(\"D:\\\\test.txt\"); } private static void read(String fileName) { try (FileInputStream fis = new FileInputStream(fileName); InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8); // 指定编码 BufferedReader reader = new BufferedReader(isr)) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } }}
-
优点:
-
解决了编码问题:可以明确指定字符集(如 UTF-8, GBK),确保跨环境一致性,避免乱码。
-
灵活性高,可以对接任何字节流。
-
-
缺点:
-
用法比
FileReader/Writer
稍复杂一些。
-
-
适用场景:
-
所有需要处理编码的文本读写场景。这是处理文本文件的最佳实践组合:
BufferedReader
+InputStreamReader
+FileInputStream
。
-
4. PrintWriter
提供了丰富的打印格式方法,如 print()
, println()
, printf()
。
写文件示例:
import java.io.FileWriter;import java.io.IOException;import java.io.PrintWriter;import java.util.ArrayList;import java.util.List;public class Demo { public static void main(String[] args) { List contents = new ArrayList(); contents.add(\"test1\"); contents.add(\"test2\"); writ(\"D:\\\\test.txt\", contents); } private static void writ(String fileName, List contents) { try (FileWriter fw = new FileWriter(fileName); PrintWriter writer = new PrintWriter(fw)) { // 也可以直接包装 FileOutputStream for (String content : contents) { writer.println(content); } } catch (IOException e) { e.printStackTrace(); } }}
-
优点:
-
输出格式非常方便,类似于
System.out
。 -
可以自动刷新缓冲区(可选功能)。
-
-
缺点:
-
异常处理 silent:它的方法不会抛出
IOException
,需要通过checkError()
方法自己检查错误状态,容易让人忽略错误。
-
-
适用场景:
-
需要向文件或输出流中写入格式化文本,比如生成报告、数据文件。
-
二、NIO 和 NIO.2 (java.nio.file
包)
Java 7 引入了 NIO.2,提供了更现代、更强大的文件操作 API。
1. Files
工具类 (一次性读写)
Files
类提供了一系列静态方法,可以用一行代码完成整个文件的读写,极其方便。
读文件示例 (读取所有行到List):
import java.io.IOException;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Paths;import java.util.List;public class Demo { public static void main(String[] args) { read(\"D:\\\\test.txt\"); } private static void read(String fileName) { try { // 读取所有行,默认编码是 UTF-8 List lines = Files.readAllLines(Paths.get(fileName)); for (String line : lines) { System.out.println(line); } // 读取整个文件为字节数组(适用于任何文件,包括文本) byte[] bytes = Files.readAllBytes(Paths.get(fileName)); System.out.println(new String(bytes, StandardCharsets.UTF_8)); } catch (IOException e) { e.printStackTrace(); } }}
写文件示例:
import java.io.IOException;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Paths;import java.util.ArrayList;import java.util.List;public class Demo { public static void main(String[] args) { List contents = new ArrayList(); contents.add(\"test1\"); contents.add(\"test2\"); write(\"D:\\\\test.txt\", contents); } private static void write(String fileName, List contents) { try { // 将行的集合写入文件 Files.write(Paths.get(fileName), contents, StandardCharsets.UTF_8); // 将字节数组写入文件 Files.write(Paths.get(fileName), \"Hello Bytes\".getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { e.printStackTrace(); } }}
-
优点:
-
极其简洁:一行代码搞定,无需关心流的打开和关闭。
-
功能强大:支持指定编码、文件打开选项(如追加、创建等)。
-
-
缺点:
-
不适合大文件:它会一次性将整个文件内容加载到内存中,如果文件非常大(如几个GB),会导致内存溢出(OOM)。
-
-
适用场景:
-
读写小文本文件(如配置文件、小规模数据文件)。
-
追求代码简洁和可读性的场景。
-
2. Files.newBufferedReader
/ Files.newBufferedWriter
(NIO 的流式处理)
Files
类也提供了方法来创建基于 NIO Path 的 BufferedReader
和 BufferedWriter
。
示例:
import java.io.BufferedReader;import java.io.BufferedWriter;import java.io.IOException;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Paths;import java.nio.file.StandardOpenOption;import java.util.ArrayList;import java.util.List;public class Demo { public static void main(String[] args) { read(\"D:\\\\test.txt\"); List contents = new ArrayList(); contents.add(\"test1\"); contents.add(\"test2\"); write(\"D:\\\\test.txt\", contents); } private static void read(String fileName) { // 读 try (BufferedReader reader = Files.newBufferedReader(Paths.get(fileName), StandardCharsets.UTF_8)) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } } private static void write(String fileName, List contents) { // 写 (支持OpenOption,比如StandardOpenOption.APPEND追加,CREATE创建等) try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(fileName), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { for (String content : contents) { writer.write(content); writer.newLine(); } } catch (IOException e) { e.printStackTrace(); } }}
-
优点:
-
结合了传统
BufferedReader/Writer
的高效和 NIO.2 的现代 API(直接使用Path
和指定编码更简单)。 -
支持丰富的文件打开选项。
-
适用于大文件,因为是流式处理,内存友好。
-
-
缺点:
-
相比
Files.readAllLines
,代码量稍多。
-
-
适用场景:
-
处理大文本文件,需要流式、按行读取。
-
需要向文件追加内容等更复杂的操作。
-
3. Java 8 Stream API (函数式处理)
结合 Files.lines()
方法,可以用声明式的流式 API 来处理文本。
示例:
import java.io.IOException;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Paths;import java.util.stream.Stream;public class Demo { public static void main(String[] args) { read(\"D:\\\\test.txt\"); } private static void read(String fileName) { try (Stream lines = Files.lines(Paths.get(fileName), StandardCharsets.UTF_8)) { lines.filter(line -> line.contains(\"test\")) // 过滤出包含\"test\"的行 .map(String::toUpperCase) // 转换为大写 .forEach(System.out::println); // 打印每一行 } catch (IOException e) { e.printStackTrace(); } }}
-
优点:
-
代码非常优雅,符合函数式编程思想。
-
可以轻松地进行过滤、映射、收集等复杂操作。
-
背后也是缓冲读取,性能好且内存友好(流是惰性求值的)。
-
-
缺点:
-
需要熟悉 Java 8 Stream 的概念。
-
I/O 异常在 Lambda 表达式中处理起来不太方便。
-
-
适用场景:
-
需要对文本内容进行复杂的查找、过滤、转换、统计等操作。
-
追求现代、简洁、声明式的代码风格。
-
三、实用工具类
Scanner
(主要用于读取结构化输入)
虽然常用于 System.in
,但也可以用于读取文件。
示例:
import java.io.File;import java.io.FileNotFoundException;import java.util.Scanner;public class Demo { public static void main(String[] args) { read(\"D:\\\\test.txt\"); } private static void read(String fileName) { try (Scanner scanner = new Scanner(new File(fileName), \"UTF-8\")) { while (scanner.hasNextLine()) { String line = scanner.nextLine(); System.out.println(line); } // 或者按分隔符(如空格)读取 // while (scanner.hasNext()) { String token = scanner.next(); } } catch (FileNotFoundException e) { e.printStackTrace(); } }}
-
优点:
-
可以按正则表达式或自定义分隔符解析文本,非常适合读取结构化的数据(如 CSV)。
-
-
缺点:
-
性能通常比
BufferedReader
差。
-
-
适用场景:
-
读取格式已知的、结构化的文本数据(如由空格、逗号分隔的数据文件)。
-
选择指南
(包装 FileReader/Writer)
(包装 InputStreamReader/Writer)
对内容进行复杂查询、过滤、转换
建议:
-
处理小文件(<几十MB):直接使用
Files.readAllLines()
或Files.write()
,代码最简洁。 -
处理大文件或需要控制内存:使用
Files.newBufferedReader()
或Files.newBufferedWriter()
,或者传统的BufferedReader
+InputStreamReader
组合。这是最稳健、最通用的方法。 -
需要对文本进行复杂处理:使用
Files.lines().stream()
,利用 Stream API 的强大功能。 -
读取结构化数据:考虑使用
Scanner
。 -
永远明确指定字符编码(如
StandardCharsets.UTF_8
),不要依赖平台默认值。 -
始终使用 try-with-resources 语句(如示例中所示),确保流会被正确关闭,避免资源泄漏。