Sao chép file trong Java là một thao tác rất phổ biến. Tuy nhiên, lớp java.io.File lại không cung cấp sẵn phương thức tiện lợi nào để thực hiện việc này một cách trực tiếp.

Trong bài blog này, tôi sẽ cùng các bạn đi sâu vào bốn cách tiếp cận phổ biến nhất để sao chép file trong Java, phân tích ưu nhược điểm của từng cái, và thậm chí chúng ta sẽ cùng xem một bài kiểm tra hiệu năng nhỏ để có cái nhìn trực quan hơn.
4 cách sao chép File trong Java
1. Sao chép File bằng Stream (Phương pháp truyền thống)
Đây là cách tiếp cận “cổ điển” và cơ bản nhất mà bất kỳ lập trình viên Java nào cũng nên biết. Ý tưởng rất đơn giản: chúng ta đọc dữ liệu từ file nguồn thông qua InputStream và ghi từng byte (hoặc từng khối byte) vào file đích bằng OutputStream. Sử dụng một bộ đệm byte[] buffer giúp việc đọc/ghi hiệu quả hơn, tránh việc đọc ghi từng byte riêng lẻ, vốn rất chậm.
Dưới đây là phương thức minh họa cách sao chép file bằng Stream:
private static void copyFileUsingStream(File source, File dest) throws IOException {
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream(source);
os = new FileOutputStream(dest);
byte[] buffer = new byte[1024]; // Bộ đệm 1KB
int length;
// Đọc dữ liệu từ nguồn vào buffer cho đến khi hết dữ liệu
while ((length = is.read(buffer)) > 0) {
// Ghi dữ liệu từ buffer vào đích
os.write(buffer, 0, length);
}
} finally {
// Đảm bảo đóng các luồng để tránh rò rỉ tài nguyên
if (is != null) {
is.close();
}
if (os != null) {
os.close();
}
}
}
2. Sao chép File bằng java.nio.channels.FileChannel (Java NIO)
Java NIO (New I/O) được giới thiệu từ Java 1.4, mang đến các cơ chế I/O hiệu quả hơn, đặc biệt là với FileChannel. FileChannel có phương thức transferFrom() mạnh mẽ, được quảng cáo là nhanh hơn đáng kể so với việc sử dụng Stream truyền thống, nhờ khả năng tận dụng “zero-copy” của hệ điều hành, tức là dữ liệu được chuyển trực tiếp giữa kernel buffers mà không cần đi qua user space, giảm thiểu việc sao chép dữ liệu giữa kernel và user space.
Dưới đây là phương thức sao chép file sử dụng FileChannel:
private static void copyFileUsingChannel(File source, File dest) throws IOException {
FileChannel sourceChannel = null;
FileChannel destChannel = null;
try {
// Lấy FileChannel từ FileInputStream và FileOutputStream
sourceChannel = new FileInputStream(source).getChannel();
destChannel = new FileOutputStream(dest).getChannel();
// Chuyển toàn bộ dữ liệu từ sourceChannel sang destChannel
destChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
}finally{
// Đảm bảo đóng các kênh
if (sourceChannel != null) {
sourceChannel.close();
}
if (destChannel != null) {
destChannel.close();
}
}
}
3. Sao chép File bằng Apache Commons IO FileUtils
Với những dự án đã và đang sử dụng thư viện Apache Commons IO, việc sao chép file trở nên cực kỳ đơn giản với FileUtils.copyFile(File srcFile, File destFile). Ưu điểm lớn nhất là sự tiện lợi và gọn gàng của code, giúp tăng tốc độ phát triển. Về mặt hiệu năng, thư viện này thường sử dụng FileChannel của Java NIO bên trong, nên hiệu năng tương đương hoặc rất gần với FileChannel thuần.
Để sử dụng phương pháp này, bạn cần thêm dependency Apache Commons IO vào dự án của mình (ví dụ với Maven):
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version> <!-- Hoặc phiên bản mới nhất -->
</dependency>
Và đây là cách sử dụng:
import org.apache.commons.io.FileUtils;
private static void copyFileUsingApacheCommonsIO(File source, File dest) throws IOException {
FileUtils.copyFile(source, dest);
}
Lưu ý:
- Nếu dự án của bạn chưa có dependency này, có thể cân nhắc xem có đáng để thêm vào chỉ vì chức năng copy file hay không. Tuy nhiên,
Apache Commons IOlà một thư viện rất hữu ích với nhiều tiện ích khác ngoài copy file, nên việc thêm vào có thể mang lại nhiều lợi ích khác.
4. Sao chép File bằng lớp Files (Java 7 trở lên)
Từ Java 7, gói java.nio.file và đặc biệt là lớp Files đã được giới thiệu, mang đến một API hiện đại và trực quan hơn cho việc thao tác với file và đường dẫn (Path). Phương thức Files.copy() là lựa chọn tuyệt vời cho các ứng dụng Java 7+ và được khuyến nghị sử dụng. Nó tận dụng File System Providers của Java để sao chép file, thường là cách hiệu quả và an toàn nhất được khuyến nghị.
import java.nio.file.Files;
import java.nio.file.StandardCopyOption; // Tùy chọn sao chép
private static void copyFileUsingJava7Files(File source, File dest) throws IOException {
// Chuyển đổi đối tượng File sang Path
Files.copy(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
Bài kiểm tra hiệu năng: Phương pháp nào nhanh nhất?
Để có cái nhìn trực quan hơn về hiệu năng, tôi đã thực hiện một bài kiểm tra nhỏ. Tôi sử dụng một file có kích thước khoảng 1GB và chạy từng phương thức sao chép file, đo thời gian thực thi bằng System.nanoTime(). Để đảm bảo tính công bằng, tôi đã sử dụng các file nguồn và đích khác nhau cho mỗi lần chạy để tránh việc caching của hệ điều hành làm sai lệch kết quả.
Dưới đây là đoạn code test:
package com.journaldev.files;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import org.apache.commons.io.FileUtils;
public class JavaCopyFile {
public static void main(String[] args) throws InterruptedException, IOException {
// Thay đổi đường dẫn file cho phù hợp với máy của bạn
String baseDir = "/Users/pankaj/tmp/"; // Ví dụ đường dẫn gốc
//copy file conventional way using Stream
File sourceStream = new File(baseDir + "source_stream.avi");
File destStream = new File(baseDir + "dest_stream.avi");
long start = System.nanoTime();
copyFileUsingStream(sourceStream, destStream);
System.out.println("Time taken by Stream Copy = "+(System.nanoTime()-start) + " ns");
//copy files using java.nio FileChannel
File sourceChannel = new File(baseDir + "source_channel.avi");
File destChannel = new File(baseDir + "dest_channel.avi");
start = System.nanoTime();
copyFileUsingChannel(sourceChannel, destChannel);
System.out.println("Time taken by Channel Copy = "+(System.nanoTime()-start) + " ns");
//copy files using apache commons io
File sourceApache = new File(baseDir + "source_apache.avi");
File destApache = new File(baseDir + "dest_apache.avi");
start = System.nanoTime();
copyFileUsingApacheCommonsIO(sourceApache, destApache);
System.out.println("Time taken by Apache Commons IO Copy = "+(System.nanoTime()-start) + " ns");
//using Java 7 Files class
File sourceJava7 = new File(baseDir + "source_java7.avi");
File destJava7 = new File(baseDir + "dest_java7.avi");
start = System.nanoTime();
copyFileUsingJava7Files(sourceJava7, destJava7);
System.out.println("Time taken by Java7 Files Copy = "+(System.nanoTime()-start) + " ns");
}
// Các phương thức copyFileUsingStream, copyFileUsingChannel,
// copyFileUsingApacheCommonsIO, copyFileUsingJava7Files
// được định nghĩa như ở trên.
private static void copyFileUsingStream(File source, File dest) throws IOException {
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream(source);
os = new FileOutputStream(dest);
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
} finally {
if (is != null) is.close();
if (os != null) os.close();
}
}
private static void copyFileUsingChannel(File source, File dest) throws IOException {
FileChannel sourceChannel = null;
FileChannel destChannel = null;
try {
sourceChannel = new FileInputStream(source).getChannel();
destChannel = new FileOutputStream(dest).getChannel();
destChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
}finally{
if (sourceChannel != null) sourceChannel.close();
if (destChannel != null) destChannel.close();
}
}
private static void copyFileUsingApacheCommonsIO(File source, File dest) throws IOException {
FileUtils.copyFile(source, dest);
}
private static void copyFileUsingJava7Files(File source, File dest) throws IOException {
Files.copy(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}
Kết quả thực thi (trên máy của tôi, với file 1GB):
Time taken by Stream Copy = 44582575000 ns
Time taken by Channel Copy = 104138195000 ns
Time taken by Apache Commons IO Copy = 108396714000 ns
Time taken by Java7 Files Copy = 89061578000 ns
Phân tích kết quả:
Ngược lại với những gì chúng ta có thể mong đợi, trong bài kiểm tra cơ bản này, Stream Copy lại cho ra thời gian nhanh nhất. Điều này có thể gây ngạc nhiên, đặc biệt khi FileChannel và Files.copy thường được quảng cáo là nhanh hơn do tận dụng các cơ chế I/O cấp thấp như “zero-copy”.
Tuy nhiên, cần nhấn mạnh đây chỉ là một bài kiểm tra rất cơ bản trên một môi trường cụ thể (máy tính, hệ điều hành, cấu hình JVM). Hiệu năng thực tế có thể thay đổi đáng kể tùy thuộc vào hệ điều hành, phần cứng (đặc biệt là tốc độ ổ đĩa), kích thước file (file nhỏ so với file lớn), và thậm chí cả phiên bản JVM. Một số hệ điều hành có thể tối ưu việc đọc/ghi Stream rất tốt, hoặc các cơ chế transferFrom có thể hoạt động hiệu quả hơn với các kích thước file hoặc loại đĩa khác.
Qua những phân tích và bài kiểm tra trên, chắc hẳn các bạn đã có cái nhìn tổng quan về các phương pháp sao chép file trong Java. Vậy đâu là điều chúng ta cần ghi nhớ và áp dụng trong thực tế?
Tổng kết
Chúng ta đã cùng nhau khám phá bốn cách chính để sao chép file trong Java: sử dụng Stream truyền thống, FileChannel của Java NIO, thư viện Apache Commons IO tiện lợi, và API Files hiện đại từ Java 7. Điều quan trọng nhất cần nắm vững là không có một phương pháp nào là “tốt nhất” tuyệt đối cho mọi tình huống. Hy vọng bài blog này đã cung cấp cho bạn những kiến thức hữu ích để “làm chủ” việc sao chép file trong Java.