Queue+FileChannel實現非遞歸高效率目錄拷貝
一、 摘要
本文介紹非遞歸目錄遍歷的實現、FileChannel的使用,從而實現非遞歸的、安全的目錄拷貝。
二、 非遞歸目錄遍歷 - Queue
對於文件夾拷貝,我們常用的目錄遍歷方式是遞歸,在一個方法體中調用File.listFiles(),然後對每一個子file再調用該方法體,這樣實現起來看似簡單,實際上有很大的隱患。
當我們的目錄層次過大時,會拋出StackOverflowError錯誤,此處不作分析,可以參考這兩篇文章,總結得相當好:
因此,我們需要使用遞歸的替代——隊列。直接上代碼,裏面有註釋作爲解釋:
public void copyDir(String absSrcBase, String absDesBase) throws Exception {
Queue<File> fileQueue = new LinkedList<>();
File srcRootFile = new File(absSrcBase);
// 判斷原路徑是否有效
if (!srcRootFile.exists())
throw new InvalidPathException(srcRootFile.getAbsolutePath(), "Nothing to copy.");
if (!srcRootFile.isDirectory())
throw new InvalidPathException(srcRootFile.getAbsolutePath(), "Only a directory path accepted.");
File desRootFile = new File(absDesBase);
// 判斷目標路徑是否有效
if (desRootFile.exists() && !desRootFile.isDirectory())
throw new InvalidPathException(desRootFile.getAbsolutePath(), "Couldn't copy a directory to a file.");
// 目標路徑文件夾不存在則創建
if (!desRootFile.exists())
if (!desRootFile.getParentFile().mkdirs()) throw new IOException("Make dirs failed.");
// 加入原文件夾根節點
fileQueue.offer(srcRootFile);
// 當隊列不爲空時一直循環
while (!fileQueue.isEmpty()) {
// 隊首取出一個節點
File nodeFile = fileQueue.poll();
if (nodeFile == null) continue;
// 相對路徑則直接用nodeFile.getPath()創建File對象
File desFile = new File(getAbsDesPath(absSrcBase, nodeFile.getAbsolutePath(), absDesBase));
// 原文件節點是文件類型
if (nodeFile.isFile()) {
// 此例採用覆蓋的方式處理已有文件
if (desFile.exists()) desFile.delete();
try {
if (desFile.createNewFile()) {
// 複製的具體實現稍後講解
fastCopyFile(nodeFile, desFile);
}
} catch (IOException e) {
e.printStackTrace();
}
} else
// 原文件節點是文件夾類型
if (nodeFile.isDirectory()) {
if (!desFile.exists()) desFile.mkdirs();
File[] childFiles = nodeFile.listFiles();
// File.listFiles必須判空,並非默認返回空數組
if (childFiles != null) {
for (File file : childFiles) {
// 每一個子文件入列
fileQueue.offer(file);
}
}
}
}
}
/**
* 以根節點爲基礎,截取相對路徑,拼接到目標路徑尾部
* @param absSrcBase
* @param absSrc
* @param absDesBase
* @return
*/
private String getAbsDesPath(String absSrcBase, String absSrc, String absDesBase) {
return absDesBase + absSrc.substring(absSrc.indexOf(absSrcBase) + absSrcBase.length());
}
三、 高效率IO - FileChannel
關於FileChannel,JDK中如此描述(節選):
/**
* A channel for reading, writing, mapping, and manipulating a file.
*
* File channels are safe for use by multiple concurrent threads. The
* {@link Channel#close close} method may be invoked at any time, as specified
* by the {@link Channel} interface. Only one operation that involves the
* channel's position or can change its file's size may be in progress at any
* given time; attempts to initiate a second such operation while the first is
* still in progress will block until the first operation completes. Other
* operations, in particular those that take an explicit position, may proceed
* concurrently; whether they in fact do so is dependent upon the underlying
* implementation and is therefore unspecified.
*
* <p> A file channel is created by invoking one of the {@link #open open}
* methods defined by this class. A file channel can also be obtained from an
* existing {@link java.io.FileInputStream#getChannel FileInputStream}, {@link
* java.io.FileOutputStream#getChannel FileOutputStream}, or {@link
* java.io.RandomAccessFile#getChannel RandomAccessFile} object by invoking
* that object's <tt>getChannel</tt> method, which returns a file channel that
* is connected to the same underlying file. Where the file channel is obtained
* from an existing stream or random access file then the state of the file
* channel is intimately connected to that of the object whose <tt>getChannel</tt>
* method returned the channel.
*/
可知,FileChannel是多線程情況下併發安全的,任何時候,只會有一個涉及通道的位置或可以更改其文件大小的操作在進行;在第一個操作仍在進行時,嘗試發起第二個這樣的操作將會阻塞,直到第一個操作完成。其他操作,尤其是特定位置的操作,可以同時進行。
除了通過FileChannel.open來獲取一個實例,還可以通過FileInputStream、FileOutputStream、RandomAccessFile等對象的getChannel方法獲得對應文件的通道。
因此讀者可以嘗試使用線程池+LinkedBlockingQueue實現多線程目錄拷貝,針對單個文件體積過大的情況,效率可以進一步提升。
此例中我們要用到一個重要的方法transferTo:
/**
* Transfers bytes from this channel's file to the given writable byte
* channel.
*
* <p> This method is potentially much more efficient than a simple loop
* that reads from this channel and writes to the target channel. Many
* operating systems can transfer bytes directly from the filesystem cache
* to the target channel without actually copying them. </p>
*
*/
public abstract long transferTo(long position, long count,
WritableByteChannel target)
throws IOException;
翻譯過來就是,這個方法可能比從這個通道讀取並寫入目標通道的常規循環更有效。許多操作系統可以直接將字節從文件系統緩存傳輸到目標通道,而不需要實際複製它們。
然後直接上代碼:
public static void fastCopyFile(File srcFile, File desFile)
throws NonReadableChannelException, NonWritableChannelException {
// 檢查原文件是否可讀
if (!srcFile.canRead()) throw new NonReadableChannelException();
// 檢查目標文件是否可寫
if (!desFile.canWrite()) throw new NonWritableChannelException();
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel fcInput = null;
FileChannel fcOutput = null;
try {
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(desFile);
fcInput = fis.getChannel();
fcOutput = fos.getChannel();
fcInput.transferTo(0, fcInput.size(), fcOutput);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fcInput != null) {
try {
fcInput.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fcOutput != null) {
try {
fcOutput.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
比較簡單,不再詳細講解。
四、 對比測試
這一篇文章對FileWriter、BufferedWriter、FileOutputStream、BufferedOutputStream、FileChannel進行了詳細的比較: