Java核心技術 輸入與輸出3

訪問目錄中的項

靜態的Files.list方法會返回一個可以讀取目錄中各個項的Stream< Path >對象。目錄是被惰性讀取的,因爲讀取目錄涉及需要關閉的系統資源,所以應該使用try塊:

Path path = Paths.get("corejava\\src");
try(Stream<Path> entries = Files.list(path)) {
    entries.forEach(p -> System.out.println(p.getFileName()));
}

list方法不會進入子目錄,爲了處理目錄中的所有子目錄,需要使用Files.walk方法,只要遍歷的項是目錄,那麼在進入它之前,會繼續訪問它的兄弟項。
可以通過File.walk(pathToRoot, depth)來限制想要訪問的樹的深度。兩種walk方法都具有FileVisitOption…的可變長參數,但只能提供一種選項:FOLLOW_LINKS,即跟蹤符號鏈接。
無法通過Files.walk方法來刪除目錄樹,因爲需要在刪除父目錄之前先刪除子目錄。

使用目錄流

如果需要對遍歷過程進行更加細粒度的控制,應該使用Files.newDirectoryStream,它會產生一個DirectoryStream,它不是java.util.stream.Stream的子接口,而是專門用於目錄遍歷的接口。它是Iterable的子接口,因此可以在增強for循環中使用目錄流,訪問目錄中的項並沒有具體順序。
可以用glob模式來過濾文件:

// try塊確保目錄流被正確關閉
try (DirectoryStream<Path> directories = Files.newDirectoryStream(path, "*.java")) {
    for (Path directory : directories) {
        System.out.println(directory.getFileName());
    }
}
模式 描述 示例
* 匹配路徑組成部分中0個或多個字符 *.java匹配當前目錄中所有的java文件
** 匹配跨目錄邊界的0個或多個字符 **.java匹配在所有子目錄中的Java文件
匹配一個字符 ???.java匹配所有四個字符的java文件
[…] 匹配一個字符集合,可以使用連線符[0-9]和取反符[!0-9] Test[0-9A-F].java匹配Testx.java,x是一個十六機制數字
{…} 匹配由逗號隔開的多個可選項之一 *.{java,class}匹配鄋java文件和class文件
\ 轉義上訴任意模式中的字符以及\字符 * \ **匹配所有文件中包含*的文件

Windows的glob語法則必須對反斜槓轉義兩次,一次爲glob語法轉義,一次爲java字符串轉義:

Files.newDirectoryStream(dir, "C:\\\\");

如果想要訪問某個子目錄的所有子孫成員,可以調用walkFileTree方法,並向其傳遞一個FileVisitor類型的對象,這個對象會得到下列通知:
1.在遇到一個文件或目錄時,FileVisitResult visitFile(T path, BasicFileAttributes attrs)
2.在一個目錄被處理前,FileVisitResult preVisitDirectory(T dir, IOException ex)
3.在一個目錄被處理後,FileVisitResult postVisitDirectory(T dir, IOException ex)
4.在視圖訪問文件或目錄時發送錯誤,例如沒有權限打開,FileVisitResult visitDirectory(path, IOException)
對於上述每種情況,都可以指定是否希望執行下面的操作:
1.繼續訪問下一個文件:FileVisitResult.CONTINUE
2.繼續訪問,但是不在訪問這個目錄的任何項,FileVisitResult.SKIP_SUBTREE
3.繼續訪問,但是不在訪問這個文件的兄弟文件,FileVisitResult.SKIP_SIBLINGS
4.終止訪問,FileVisitResult.TERMINATE
當有任何方法拋出異常時,就會終止訪問,這個異常從walkFileTree方法拋出。
FileVisitor< ? super Path >接口是泛型類型,但也不太可能使用出Path之外的東西,因爲Path並沒有多少超類型。
便捷類SimpleFileVisitor實現了FileVisitor接口,但是其除visitFileFailed方法之外的所有方法並不做任何處理而是直接訪問,而visitFileFailed方法會拋出由失敗導致的異常,並進而終止訪問:

Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
    @Override
    public FileVisitResult preVisitDirectory(Path p, BasicFileAttributes attrs) {
        System.out.println(p);
        return FileVisitResult.CONTINUE;
    }
    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
       return FileVisitResult.CONTINUE;
    }
    
    @Override
    public FileVisitResult visitFileFailed(Path p, IOException exc) {
        return FileVisitResult.SKIP_SUBTREE;
    }
});

需要覆蓋postVisitDirectory和visitFileFailed方法,否則,訪問會遇到不允許打開或不允許任何訪問的文件時立即失敗。
路徑的衆多屬性是作爲preVisitDirectory和visitFile方法的參數傳遞的。訪問者不得不通過操作系統調用來獲得這些屬性,因爲它需要區分文件和目錄。因此不需要再次執行系統調用了。
如果在進入或離開一個目錄是要執行某些操作(刪除目錄樹):

Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Files.delete(file);
        return FileVisitResult.CONTINUE;
    }
    
    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
        if (e != null) {
            throw e;
        }
        Files.delete(dir);
        return FileVisitResult.CONTINUE;
    }
});

ZIP文件系統

Paths類會在默認文件系統查找路徑,即在用戶本地磁盤中的文件。也可以使用別的文件系統,最有用之一是ZIP文件系統:

FileSystem fs = FileSystems.newFileSystem(Paths.get(zipname), null);

將建立一個文件系統,包含ZIP文檔中的所有文件。如果知道文件名,那麼從ZIP文檔中複製出這個文件就變得容易:

Files.copy(fs.getPath(sourceName), targetPath);

列出ZIP文檔中的所有文件,可以遍歷文件樹:

Files.walkFileTree(fs.getPath("/"), new SimpleFileVisitor<Path>(){
   @Override
   public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
       System.out.println(file);
       return FileVisitResult.CONTINUE;
   }
});

6.內存映射文件

大多數操作系統都可以利用虛擬內存實現將一個文件或文件的一部分映射到內存中。然後這個文件就可以當做是內存數組一樣地訪問,這比傳統的文件操作要快得多。

內存映射文件的性能

在這裏插入圖片描述
內存映射比使用帶緩衝的順序輸入要稍微快一點,但比使用RandomAccessFile快很多。對於中等尺寸的順序讀入則沒有必要使用內存映射。

首先從文件中獲得一個通道(channel),通道是用於磁盤文件的一種抽象,它可以訪問注入內存映射、文件加鎖機制以及文件間快速數據傳遞等操作系統特性。

FileChannel channel = FileChannel.open(path, options);

然後通過FileChannel類的map方法從通道中獲得ByteBuffer。可以指定想要的文件區域與映射模式,支持的模式有三種:
1.FileChannel.MapMode.READ_ONLY:所產生的緩衝區是隻讀的,任何對該緩衝區寫入的嘗試都會導致ReadOnlyBufferException異常
2.FileChannel.MapMode.READ_WRITE:所產生的緩衝區是可寫的,任何修改都會在某個時刻寫回到文件中。注意,其他映射同一個文件的程序可能不能立即看到這些修改,多個程序同時進行文件映射的確切行爲是依賴於操作系統的
3.FileChannel.MapMode.PRIVATE:所產生的緩衝區是可寫的,但是任何修改對這個緩衝區來說都是私有的,不會傳播到文件中
一旦有了緩存區,就可以使用ByteBuffer類和Buffer超類的方法讀寫數據了。緩衝區支持順序和隨機數據訪問,它有一個可以通過get和put操作來移動的位置。

// 順序遍歷緩衝區的所有字節
while(buffer.hasRemaining()){
	byte b = buffer.get();
}
// 隨機訪問
for (int i = 0; i < buffer.limit(); i++) {
	byte b = buffer.get(i);
}

使用下面方法讀寫字節數組:
get(byte[] bytes)
get(byte[] bytes, int offset, int length)
還有getInt,getLong,getShort,getChar,getFloat,getDouble用來讀寫在文件中存儲爲二進制的基本類型。Java對二進制數據使用高位在前的排序機制。但是如果需要以低位在前方式處理二進制數字的文件:

// 查詢緩衝區當前字節順序
ByteOrder b = buffer.order();
buffer.order(ByteOrder.LITTLE_ENDIAN);

向緩衝區寫數字,putInt,putLong,putShort,putChar,putFloat,putDouble,在恰當時機,以及當通道關閉時,會將這些修改寫回到文件中。

下面程序是用於計算文件的32位的循環冗餘校驗和(CRC32),這個數值就是經常用來判斷一個文件是否已損壞的,因爲文件損壞極可能導致校驗和改變。java.util.zip包中包含一個CRC32類,可以計算一個字節序列的校驗和:

public class MemoryMaoTest {
    public static long checksumInputStream(Path filename) throws IOException {
        try (InputStream in = Files.newInputStream(filename)) {
            CRC32 crc = new CRC32();
            int c;
            while ((c = in.read()) != -1) {
                crc.update(c);
            }
            return crc.getValue();
        }
    }

    public static long checksumBufferedInputStream(Path filename) throws IOException {
        try (InputStream in = new BufferedInputStream(Files.newInputStream(filename))) {
            CRC32 crc = new CRC32();
            int c;
            while ((c = in.read()) != -1) {
                crc.update(c);
            }
            return crc.getValue();
        }
    }

    public static long checksumRandomAccessFile(Path filename) throws IOException {
        try (RandomAccessFile file = new RandomAccessFile(filename.toFile(), "r")) {
            long length = file.length();
            CRC32 crc = new CRC32();
            for (long p = 0; p < length; p++) {
                file.seek(p);
                int c = file.readByte();
                crc.update(c);
            }
            return crc.getValue();
        }
    }

    public static long checksumMappedFile(Path filename) throws IOException {
        try (FileChannel channel = FileChannel.open(filename)) {
            CRC32 crc = new CRC32();
            int length = (int) channel.size();
            MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, length);
            for (int p = 0; p < length; p++) {
                int c = buffer.get(p);
                crc.update(c);
            }
            return crc.getValue();
        }
    }

    public static void main(String[] args) throws IOException {
        System.out.println("Input Stream:");
        long start = System.currentTimeMillis();
        Path filename = Paths.get("C:\\Program Files\\Java\\jre1.8.0_77\\lib\\rt.jar");
        long crcValue = checksumInputStream(filename);
        long end = System.currentTimeMillis();
        System.out.println(Long.toHexString(crcValue));
        System.out.println((end - start) + " ms");

        System.out.println("Buffered Input Stream:");
        start = System.currentTimeMillis();
        crcValue = checksumBufferedInputStream(filename);
        end = System.currentTimeMillis();
        System.out.println(Long.toHexString(crcValue));
        System.out.println((end - start) + " ms");

        System.out.println("Random Access File:");
        start = System.currentTimeMillis();
        crcValue = checksumRandomAccessFile(filename);
        end = System.currentTimeMillis();
        System.out.println(Long.toHexString(crcValue));
        System.out.println((end - start) + " ms");

        System.out.println("Mapped File:");
        start = System.currentTimeMillis();
        crcValue = checksumMappedFile(filename);
        end = System.currentTimeMillis();
        System.out.println(Long.toHexString(crcValue));
        System.out.println((end - start) + " ms");
    }
    //Input Stream:
    //414112ba
    //98183 ms
    //Buffered Input Stream:
    //414112ba
    //288 ms
    //Random Access File:
    //414112ba
    //109077 ms
    //Mapped File:
    //414112ba
    //203 ms
}

緩衝區數據結構

緩衝區是由相同類型的數值構成的數組,Buffer類是一個抽象類,他有衆多的具體子類,包括ByteBuffer、CharBuffer、DoubleBuffer、IntBuffer、LongBuffer和ShortBuffer(StringBuffer和緩衝區沒關係)。
最常用的將是ByteBuffer和CharBuffer。每個緩衝區都具有:
1.一個容量,它永遠不能改變
2.一個讀寫位置,下一個值將在此進行讀寫
3.一個界限,超過它進行讀寫是沒有意義的
4.一個可選的標記,用於重複一個讀入或寫出操作
在這裏插入圖片描述
0<=標記<=位置<=界限<=容量
使用緩衝區的主要目的是執行“寫,然後讀入”循環。假設一個緩衝區在一開始,它的位置爲0,界限等於容量。不斷地調用put將值添加到這個緩衝區中,當耗盡所有的數據或寫出的數據量達到容量大小時,就該切換到讀入操作了。
這時調用flip方法將界限設置到當前位置,並把位置復位到0。現在在remaining方法返回正數時(它返回的值是“界限-位置”),不斷地調用get。將緩衝區中所有的值都讀入之後,調用clear使緩衝區爲下一次寫循環做準備。clear方法將位置復位到0,並將界限復位到容量。
如果想重讀緩衝區,可以使用rewind或mark/reset方法。
要獲取緩衝區,可以調用諸如ByteBuffer.allocate或ByteBuffer.wrap這樣的靜態方法。
然後可以用來自某個通道的數據填充緩衝區,或者將緩衝區內容寫出通道中:

ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
channel.read(buffer);
channel.position(newpos);
buffer.flip();
channel.write(buffer);

文件加鎖機制

多個同時執行的程序修改同一個文件的情形,很明顯這些程序需要以某種方式通信,不然文件很容易被損壞。文件鎖可以解決這個問題,它可以控制對文件或文件中某個範圍的字節的訪問。
兩個實例都會希望寫一個配置文件,第一個實例應該鎖定這個文件,當第二個實例發現這個文件被鎖定時,必須決策是等待直至這個文件解鎖,還是直接跳過這個寫操作。
要鎖定一個文件,可以調用FileChannel類的lock域或tryLock方法:

FileChannel channel = FileChannel.open(path);
FileLock lock = channel.lock();
// 或
// FileLock lock = channel.tryLock();

第一個調用會阻塞直至可獲得鎖,而第二個調用將立即返回,要麼返回鎖,要麼在鎖不可獲得的情況下返回null。這個文件將保持鎖定狀態,直至這個通道關閉,或者在鎖上調用了release方法。
可以通過調用鎖文件的一部分:

FileLock lock(long start, long size, boolean shared)
FileLock tryLock(long start, long size, boolean shared)

如果鎖定了文件的結尾,但這個文件的長度隨增長超過了鎖定部分,那麼額外部分是未鎖定的,如果想要鎖定所有字節,可以使用Long.MAX_VALUE來表示尺寸。
如果shared標誌爲false,則鎖定文件的目的是讀寫,而如果爲true,則是一個共享鎖,它允許多個進程從文件中讀入,並阻止任何進程獲得獨佔的鎖。
並非所有操作系統都支持共享鎖,因此可能在請求共享鎖時候得到獨佔的鎖。調用FileLock類的isShared方法可以查詢鎖的類型。

要確保在操作完釋放鎖時,與往常一樣最好在一個try語句中釋放鎖:

try (FileLock lock = channel.lock()) {
	//訪問鎖定的文件或文件段
}

文件加鎖機制依賴於操作系統,需要注意幾點:
1.在某些系統中,文件加鎖僅僅是建議性的,如果一個應用未能得到鎖,它仍舊可以被另一個應用併發鎖定的文件執行寫操作
2.在某些系統中,不能在鎖定一個問加你的同時將其映射到內存中
3.文件鎖是由整個Java虛擬機持有的(兩個程序由同一虛擬機啓動,那不可能在每一個獲得同一文件的鎖),調用lock時,如果虛擬機已經在同一個文件鎖持有了另一個重疊的鎖,那麼將拋出OverlappingFileLockException
4.在一些文件中,關閉一個通道會釋放由Java虛擬機持有的底層文件上的所有鎖。因此,在同一個鎖定文件上應避免使用多個通道
5.在網絡文件系統上鎖定是高度依賴於系統的,因此應儘量避免

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