Queue+FileChannel實現非遞歸高效率目錄拷貝

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進行了詳細的比較:

java中多種寫文件方式的效率對比實驗

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章