1. 损坏文件的迹象
在理想世界里,文件的读写总是毫无错误,其中的数据就像新鲜出炉的面包:柔软、香气扑鼻、整齐、完整。但在现实中,文件可能是“损坏的”、“格式不对的”、“写到一半的”或“格式不匹配的”。原因各不相同,比如写盘时突然断电导致写入失败,文件传输中的网络错误,存储介质损坏(老生常谈的坏扇区 bad sector),又或者用不合适的程序手动编辑了文件,或期望的数据格式与实际不一致而导致破坏。
在 Java 中,这些情况通常会在读/写时通过异常暴露出来,有时也会表现为程序的奇怪行为(例如数据突然读完,或者出现一堆乱码)。
读取时的异常
最明显的迹象是意外的异常。常见的有:
- EOFException — 文件意外结束(End Of File)。你以为文件里还有数据,其实已经没有了。
- MalformedInputException(或者在旧 API 中,来自 NIO 的 MalformedInputException)— 文件不符合预期的编码或结构。
- ZipException — 当你试图把压缩包当作普通文件来读时。
- StreamCorruptedException — 读取序列化对象时,如果文件已损坏。
数据格式不匹配
有时文件读取并不会抛出异常,但内容与预期格式不一致:
- 期待一行字符串,却得到一堆乱码。
- 期待特定数量的数字,结果更少。
- 以为是 CSV,结果是 JSON(或反之)。
实际案例
假设你写了一个应用,用文本文件保存任务列表。程序期望每一行都是一个独立的任务。但用户用 Excel 打开文件,做了修改,并以另一种格式保存……现在你的程序无法读取该文件了。
2. 处理损坏文件的策略
记录日志并告知用户
首要规则:不要慌!(也不要让用户慌)。务必记录错误并告知用户,如果出了问题。至于把可怕的 Java 堆栈全都展示给用户——没必要。
try {
// 读取文件
} catch (EOFException e) {
System.err.println("文件意外结束,可能已损坏。");
// 记录详细日志
e.printStackTrace();
}
尝试部分恢复
有时可以“至少救回一部分”数据。例如按行读取时,可以处理到第一处错误之前的所有行。
使用备份(backup)
严肃的程序在写入前会为重要文件创建备份。若主文件损坏,可以尝试从备份恢复。
3. 实践:读取到意外的文件结尾(EOF)
经典场景
假设有一个按顺序写入 int 的二进制文件。程序期望有 5 个,但文件被损坏,只写了 3 个。
import java.io.*;
public class DamagedFileExample {
public static void main(String[] args) {
String filename = "numbers.bin";
// 作为示例:创建包含 3 个数字的文件(而不是 5 个)
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(filename))) {
out.writeInt(42);
out.writeInt(7);
out.writeInt(2024);
// out.writeInt(1); out.writeInt(2); // 特意不写!
} catch (IOException e) {
System.err.println("创建文件时出错: " + e.getMessage());
}
// 现在尝试读取 5 个数字
try (DataInputStream in = new DataInputStream(new FileInputStream(filename))) {
for (int i = 0; i < 5; i++) {
int number = in.readInt();
System.out.println("读取到数字: " + number);
}
} catch (EOFException e) {
System.err.println("文件意外结束!可能已损坏。");
} catch (IOException e) {
System.err.println("读取错误: " + e.getMessage());
}
}
}
输出:
读取到数字: 42
读取到数字: 7
读取到数字: 2024
文件意外结束!可能已损坏。
读到第一处错误为止
通常可以在循环中读取,直到抛出异常为止,这样至少能拿到一部分信息。
try (DataInputStream in = new DataInputStream(new FileInputStream(filename))) {
while (true) {
try {
int number = in.readInt();
System.out.println("读取到数字: " + number);
} catch (EOFException e) {
System.out.println("数据已读完(或文件损坏)。");
break;
}
}
} catch (IOException e) {
System.err.println("读取错误: " + e.getMessage());
}
4. 文本文件与字符编码
编码问题
若文件以一种编码写入,却用另一种编码读取,可能会出现解码错误:
import java.nio.charset.*;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("tasks.txt"), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (MalformedInputException e) {
System.err.println("编码错误!文件已损坏,或不是以 UTF-8 编码写入。");
} catch (IOException e) {
System.err.println("读取错误: " + e.getMessage());
}
重要: 有时不会抛出异常,而是出现“乱码”——这同样表明文件损坏或编码不正确。
如何处理?
- 告知用户问题。
- 尝试用其他编码打开。
- 如果数据很关键——建议从备份恢复。
5. 数据恢复:策略
读取部分有效数据
如果文件结构允许,可以“提取”直到出错之前的有效数据。例如,若文件是按行的任务列表(一个任务一行),可以处理到出错为止的所有行。
try (BufferedReader reader = new BufferedReader(new FileReader("tasks.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
// 处理该行
}
} catch (IOException e) {
System.err.println("读取错误: " + e.getMessage());
// 可以保存已经读取的数据,或提示用户进行恢复
}
使用备份文件
如果你事先做了副本(例如 tasks.txt.bak),可以从中恢复数据:
File original = new File("tasks.txt");
File backup = new File("tasks.txt.bak");
if (!original.exists() && backup.exists()) {
// 将备份复制回原始位置
Files.copy(backup.toPath(), original.toPath(), StandardCopyOption.REPLACE_EXISTING);
System.out.println("已从备份恢复完成。");
}
校验和与校验
对于重要文件,可以保存校验和(例如 MD5 或 SHA-256),并在每次打开文件时将其与当前值比对。如果不一致,说明文件已损坏。
// 示意代码(为简化省略了哈希实现)
String expectedHash = "..."; // 之前保存的校验和
String actualHash = calculateFileHash("tasks.txt");
if (!expectedHash.equals(actualHash)) {
System.out.println("文件 tasks.txt 已损坏!请尝试从备份恢复。");
}
6. 处理损坏文件的常见错误
错误 №1:不校验文件格式。 如果你期望每一行都是数字,却出现了文本,就会抛出 NumberFormatException。最好在读取过程中进行校验。
错误 №2:没有使用 try-with-resources。 如果不使用 try-with-resources,即使发生错误,文件也可能保持“悬挂”(未关闭)状态,从而妨碍恢复或删除。
错误 №3:在未创建备份的情况下直接覆盖损坏的文件。 发生错误时立即覆盖文件会降低可恢复性。最好先保存 backup。
错误 №4:恢复过程缺乏信息。 应告知用户文件已损坏并通过副本恢复,否则他可能不明白为何部分数据丢失。
GO TO FULL VERSION