Skip to content

Java 文件与 I/O

输入/输出 (I/O) 操作对于任何需要从源(如文件、网络连接或键盘)读取数据或将数据写入目标(如文件、网络或控制台)的应用程序都是必不可少的。

Java 提供了丰富的 API 来处理 I/O,主要位于 java.io 和 java.nio(新 I/O)包中。

在 Java I/O 中,**流(stream)**代表数据从源流向目标的序列。可以把它想象成一条传输数据的管道。

  • 输入流: 从源读取数据(例如,InputStream,Reader)。
  • 输出流: 将数据写入目标(例如,OutputStream,Writer)。

流根据它们处理的数据类型进行分类:

  • 字节流: 处理原始二进制数据(8 位字节)。适用于图像、音频或任何非文本数据。核心类:InputStream,OutputStream。
  • 字符流: 处理字符数据(16 位 Unicode 字符)。自动处理字符编码转换。非常适合文本文件。核心类:Reader,Writer。

现代文件 I/O:java.nio.file 包 (NIO.2)

Section titled “现代文件 I/O:java.nio.file 包 (NIO.2)”

java.nio.file 包(通常称为 NIO.2)在 Java 7 中引入,与旧的 java.io.File 类相比,它提供了更现代、更灵活、更强大的文件系统交互 API。它通常是推荐的文件操作方法。

  • Path:表示文件系统中的文件或目录路径。它是操作系统无关的。
  • Paths:一个工厂类,包含静态方法(如 get()),用于从字符串或 URI 创建 Path 对象。
  • Files:一个工具类,包含大量静态方法用于常见的文件操作(读取、写入、复制、删除、检查存在性、管理属性等)。
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathExample {
public static void main(String[] args) {
// 创建 Path 对象(操作系统无关的方式)
Path filePath = Paths.get("data", "input.txt"); // 相对路径
Path configPath = Paths.get("/etc/app/config.properties"); // 绝对路径(示例)
Path userHome = Paths.get(System.getProperty("user.home"), "documents", "report.pdf");
System.out.println("File Path: " + filePath);
System.out.println("File Name: " + filePath.getFileName());
System.out.println("Parent Directory: " + filePath.getParent());
System.out.println("Absolute Path: " + filePath.toAbsolutePath());
}
}

读取小文件(将所有行读入内存):

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.List;
import java.nio.charset.StandardCharsets;
public class ReadAllLines {
public static void main(String[] args) {
Path filePath = Paths.get("input.txt"); // 假设 input.txt 存在
try {
List<String> lines = Files.readAllLines(filePath, StandardCharsets.UTF_8);
System.out.println("File Content:");
for (String line : lines) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
// e.printStackTrace(); // 用于详细的堆栈跟踪
}
}
}

读取大文件(使用流和 try-with-resources): 对于大文件,逐行读取更节省内存。

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ReadLineByLine {
public static void main(String[] args) {
Path filePath = Paths.get("large_input.txt");
// try-with-resources 确保 reader 会自动关闭
try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
String line;
System.out.println("Reading line by line:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
}
}

写入字符串或字符串列表:

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.nio.charset.StandardCharsets;
import java.nio.file.StandardOpenOption;
public class WriteToFile {
public static void main(String[] args) {
Path outputPath = Paths.get("output.txt");
List<String> linesToWrite = Arrays.asList("First line", "Second line", "End of file.");
try {
// 写入行(默认会覆盖现有文件)
Files.write(outputPath, linesToWrite, StandardCharsets.UTF_8);
System.out.println("Successfully wrote lines to " + outputPath);
// 向文件中追加行
Files.write(outputPath, Arrays.asList("Appended line."), StandardCharsets.UTF_8,
StandardOpenOption.APPEND);
System.out.println("Successfully appended to " + outputPath);
} catch (IOException e) {
System.err.println("Error writing to file: " + e.getMessage());
}
}
}

写入大量数据(使用流和 try-with-resources):

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class WriteWithStream {
public static void main(String[] args) {
Path filePath = Paths.get("stream_output.txt");
try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) {
for (int i = 1; i <= 10; i++) {
writer.write("Line number " + i);
writer.newLine(); // 写入平台特定的换行符
}
System.out.println("Successfully wrote using stream to " + filePath);
} catch (IOException e) {
System.err.println("Error writing file: " + e.getMessage());
}
}
}

I/O 的一个关键方面是完成使用后要关闭资源(如流),以释放系统资源并防止泄漏。未能关闭流可能导致问题。

try-with-resources 语句(在 Java 7 中引入)自动化资源管理。任何在 try 语句括号内声明的资源(该资源必须实现 java.lang.AutoCloseable 接口,大多数 I/O 流都实现了该接口)都会在 try 块结束时自动关闭,无论它是正常完成还是抛出异常。

这是处理 I/O 资源的标准且强烈推荐的方式。 除非是 Java 7 之前的代码,否则应避免在 finally 块中手动调用 close()。

try (ResourceType resource1 = initializeResource1();
ResourceType resource2 = initializeResource2()) {
// 使用 resource1 和 resource2
} catch (IOException e) {
// 处理异常
}
// 资源在此处自动关闭

Files 类提供了许多有用的方法:

import java.nio.file.*;
import java.io.IOException;
public class FileOperations {
public static void main(String[] args) throws IOException { // 简单示例,实际代码需要 try-catch
Path source = Paths.get("input.txt");
Path destination = Paths.get("backup", "input_copy.txt");
Path newDir = Paths.get("data", "subdir");
Path fileToCheck = Paths.get("output.txt");
// 检查是否存在
boolean exists = Files.exists(fileToCheck);
System.out.println(fileToCheck + " exists: " + exists);
System.out.println(fileToCheck + " does not exist: " + Files.notExists(fileToCheck));
System.out.println(fileToCheck + " is readable: " + Files.isReadable(fileToCheck));
// 创建目录(等同于 mkdirs)
if (Files.notExists(newDir.getParent())) {
Files.createDirectories(newDir.getParent()); // 如果需要,创建父目录
}
if (Files.notExists(newDir)) {
Files.createDirectory(newDir);
System.out.println("Created directory: " + newDir);
}
// 复制文件
if (Files.exists(source) && Files.exists(destination.getParent())) {
Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copied " + source + " to " + destination);
}
// 移动(重命名)文件
// Path target = Paths.get("data", "renamed_input.txt");
// Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
// System.out.println("Moved " + source + " to " + target);
// 删除文件
// if (Files.exists(destination)) {
// Files.delete(destination);
// System.out.println("Deleted: " + destination);
// }
// 列出目录内容
Path dirToList = Paths.get("."); // 当前目录
System.out.println("\nListing contents of: " + dirToList.toAbsolutePath());
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dirToList)) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
} catch (IOException e) {
System.err.println("Error listing directory: " + e); // 列出目录时出错:
}
}
}

Java 通过 System 类提供了三个标准流:

  • System.in:标准输入流(通常是键盘输入)。一个 InputStream。
  • System.out:标准输出流(通常是控制台输出)。一个 PrintStream。
  • System.err:标准错误流(通常是控制台输出,用于错误消息)。一个 PrintStream。

从控制台读取格式化输入(如数字、单词、行)最简单的方法是使用 java.util.Scanner 类。

import java.util.Scanner;
public class ReadConsole {
public static void main(String[] args) {
// 对 Scanner 使用 try-with-resources
try (Scanner scanner = new Scanner(System.in)) {
System.out.print("Enter your name: ");
String name = scanner.nextLine(); // 读取一整行
System.out.print("Enter your age: ");
int age = scanner.nextInt(); // 读取一个整数
System.out.println("\nHello, " + name + "! You are " + age + " years old.");
} catch (java.util.InputMismatchException e) {
System.err.println("Invalid input. Please enter a number for age.");
}
// Scanner 在此处自动关闭
}
}

写入标准输出/错误 (System.out, System.err)

Section titled “写入标准输出/错误 (System.out, System.err)”

你已经大量使用过 System.out.println()。System.err 的工作方式相同,但习惯上用于错误消息。

public class StandardOutput {
public static void main(String[] args) {
System.out.println("This is standard output.");
System.err.println("This is an error message.");
int value = 10;
System.out.printf("The value is %d%n", value); // 格式化输出
}
}

虽然新代码优先使用 java.nio.file,但你可能会遇到使用 java.io.File、FileInputStream、FileOutputStream、FileReader 和 FileWriter 等类的旧代码。认识这些类是有用的。

java.io.File:表示文件或目录路径,但与 java.nio.file.Path 相比存在局限性。它的许多方法在失败时不会抛出异常,这使得错误处理不够健壮。

FileInputStream/FileOutputStream:用于读取/写入原始字节。

FileReader/FileWriter:用于读取/写入字符,使用平台的默认字符编码(这可能存在问题)。通常最好使用 InputStreamReader/OutputStreamWriter 包装字节流,并显式指定编码(例如,StandardCharsets.UTF_8)。

示例(使用 try-with-resources 的遗留复制):

import java.io.*;
public class LegacyCopyFile {
public static void main(String[] args) {
File sourceFile = new File("input.txt");
File destFile = new File("output_legacy.txt");
// 仍然使用 try-with-resources!
try (InputStream in = new FileInputStream(sourceFile);
OutputStream out = new FileOutputStream(destFile)) {
byte[] buffer = new byte[1024]; // 分块读写
int lengthRead;
while ((lengthRead = in.read(buffer)) > 0) {
out.write(buffer, 0, lengthRead);
}
System.out.println("File copied successfully using legacy IO.");
} catch (IOException e) {
System.err.println("Error copying file: " + e.getMessage());
}
}
}

对于现代 Java I/O 操作,始终优先使用 java.nio.file 和 try-with-resources。

理解如何在 I/O 或其它操作期间处理潜在错误至关重要。下一章将介绍 Java 异常 (Exceptions),解释如何在程序中优雅地预期、捕获和处理错误。