「圖片複製」是驗證 IO 流理解深度的經典場景。本文通過三個遞進版本——字節流正確實現、字符流失效對比、生產級異常處理模板,徹底講透二進制文件處理的核心邏輯與工程落地規範。
public class ImageCopyByteStream {
public static void copy(File src, File dest) throws IOException {
// 1. 校驗源文件存在
if (!src.exists() || !src.isFile()) {
throw new FileNotFoundException("源文件不存在: " + src.getAbsolutePath());
}
// 2. 確保目標目錄存在
File parent = dest.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException("無法創建目標目錄: " + parent.getAbsolutePath());
}
// 3. try-with-resources 自動關閉(JDK 7+)
try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192]; // 8KB 緩衝區,平衡內存與系統調用
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len); // 關鍵:只寫入實際讀取長度
}
} // 自動調用 close(),釋放文件鎖
}
}
public class ImageCopyCharStreamWrong {
public static void copy(File src, File dest) throws IOException {
// 錯誤示範:用字符流處理二進制文件
try (FileReader fr = new FileReader(src);
FileWriter fw = new FileWriter(dest)) {
char[] buffer = new char[8192];
int len;
while ((len = fr.read(buffer)) != -1) {
fw.write(buffer, 0, len);
fw.flush(); // 頻繁 flush 更傷性能
}
}
}
}
字符流 = 字節流 + 編碼/解碼器 (CharsetDecoder/Encoder)
圖片二進制數據 (0x89 0x50 0x4E 0x47 ...)
↓ FileReader 讀取,按默認編碼 (如 GBK/UTF-8) 解碼
字符數組 char[] → 遇到無法映射的字節序列 → 替換為 (U+FFFD 替換字符)
↓ FileWriter 寫入,按編碼器編碼為字節
輸出文件 ← 已損壞:原始字節序列不可逆丟失
本質原因:圖片數組中大量字節在文本編碼表中無對應字符,解碼器將其標記為「未知字符」並替換,導致信息不可逆損失。結論:二進制文件(圖片/視頻/音頻/壓縮包)嚴禁使用字符流。
public class RobustFileCopy {
private static final Logger log = LoggerFactory.getLogger(RobustFileCopy.class);
public static void copyWithRetry(File src, File dest, int maxRetries) throws IOException {
// 參數校驗
Objects.requireNonNull(src, "源文件不能為空");
Objects.requireNonNull(dest, "目標文件不能為空");
if (!src.exists()) throw new FileNotFoundException("源文件不存在: " + src);
if (src.isDirectory()) throw new IllegalArgumentException("源路徑不能是目錄: " + src);
// 確保目標目錄存在
File parent = dest.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException("無法創建目標目錄: " + parent);
}
int attempt = 0;
IOException lastException = null;
while (attempt++ <= maxRetries) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(src);
fos = new FileOutputStream(dest);
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.flush(); // 確保數據落盤
log.info("文件複製成功: {} -> {} (嘗試 {}/{})", src, dest, attempt, maxRetries);
return; // 成功直接返回
} catch (IOException e) {
lastException = e;
log.warn("複製失敗 (嘗試 {}/{}): {}", attempt, maxRetries, e.getMessage());
// 短暫等待後重試(針對網絡存儲/臨時鎖定)
if (attempt <= maxRetries) {
try { Thread.sleep(100L * attempt); } catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("複製被中斷", ie);
}
}
} finally {
// 分別關閉,互不影響
closeQuietly(fos, "目標文件輸出流");
closeQuietly(fis, "源文件輸入流");
}
}
throw new IOException("複製失敗,已重試 " + maxRetries + " 次", lastException);
}
private static void closeQuietly(Closeable closeable, String name) {
if (closeable != null) {
try { closeable.close(); }
catch (IOException e) { log.error("關閉 {} 失敗", name, e); }
}
}
}
finally 中分別 try-catch 關閉,防止一個關閉異常屏蔽另一個lastException| 場景 | 推薦流類型 | 理由 |
|---|---|---|
| 圖片 (JPG/PNG/GIF/WebP) | FileInputStream / FileOutputStream + Buffered | 純二進制,無編碼概念 |
| 視頻/音頻 (MP4/MP3/AVI) | 同上,大文件可考慮 NIO FileChannel.transferTo | 流式二進制,不需解碼 |
| 文本文件 (TXT/JSON/XML/CSV/代碼) | InputStreamReader / OutputStreamWriter + Buffered | 需編碼轉換,支持跨平台換行 |
| 屬性/配置文件 | Properties.load(Reader) / store(Writer) | 標準 API 封裝 |
public static void copyWithNIO(File src, File dest) throws IOException {
try (FileChannel in = FileChannel.open(src.toPath(), StandardOpenOption.READ);
FileChannel out = FileChannel.open(dest.toPath(),
StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
long transferred = 0;
long size = in.size();
while (transferred < size) {
transferred += in.transferTo(transferred, size - transferred, out);
}
}
}
transferTo 直接在內核態完成數據搬運,避免用戶態/內核態上下文切換與數據拷貝,大文件(>100MB)性能提升 3-5 倍。
掌握「二進制必用字節流、文本用字符流」鐵律,配合 try-with-resources 與分級異常處理,是 Java 文件操作的工程化基石。