第十一章 NIO.2

      在〈第十一章 NIO.2〉中尚無留言

Java File I/O(NIO.2)

Java NIO.2 是Java SE7.0新增的功能, 此功能在6.0及以前的版本是不支援的

目的

使用Path介面操作檔案及目錄
使用Files 類別檢查, 刪除, 拷貝, 移動檔案及目錄
使用Files類別的channel I/O及stream I/O 讀取及寫入檔案
讀取及變更檔案目錄的屬性
遞回處理目錄
使用PathMatcher類別尋找檔案

New File I/O – NIO2

每個作業系統都有其自己的檔案系統, 也有其相對的存取方式, Java為了求統一, 開發了java.nio.file.* 套件, 最主要的目的就是要用來取代老舊的java.io.file

新版的檔案I/O API基於buffers, channel及charset送交JSR51文件審核, 提供了非阻塞, 多樣性I/O, 提高開發之可塑性, 且不需依賴native code.

新的API具跨平台的一致性, 更容易撰寫程式碼來處理操作檔案系統的錯誤, 更有效的存取大檔案

java.io.File限制

許多的方法不能throw exception, 所以不可能取得錯誤訊息
沒有copy, move等功能
重新命名的功能無法跨平台
不支援連結(symbolic links)
無法使用檔案權限, 檔案擁有者, 及其他安全屬性
存取檔案metadata效能極差
遞回處理的可靠性差

檔案系統, 路徑及檔案

NIO.2之前, 檔案皆由java.io.File類別實作.
而在NIO.2時, 實作了java.nio.file.file.Path物件, 處理相對及絕對路徑的檔案及目錄
檔案系統是階層樹狀結構, 會有一個或多個根目錄, 比如Windows會有c:\ d:\的根目錄

相對路徑及絕對路徑

絕對路徑一定會包含根目錄, 相對路徑則會包含於另一個路徑

Symbolic Links 連結

就是Linux的軟連結, 參考到一個實体的目錄檔案

NIO.2 觀念

java.nio.file.Path : 使用系統指定檔案及目錄
java.nio.file.Files : 使用Path執行檔案及目錄的操作
java.nio.file.FileSystem : 提供介面建立路徑及其他操作檔案系統的物件
所有存取檔案系統的方法都會拋出IOException及子類別

Path介面

Path本身是一個介面, 取得Path物件, 原始方法為
FileSystem fs=FileSystems.getDefault(); //參考沒 “s”, 類別有 “s”
Path p1=fs.getPath(“d:/test1.srt”);

精簡法為  :
Path p2=Paths.get(“d:/test1.srt”);
Paths為Path的輔助類別, 最主要是用來精簡Path的取得

Windows檔案系統路徑是 “\”, 所以需使用”\\” , 但也可以用”/”

Path的方法

public class PathTest {
    public static void main(String[] args) {
        FileSystem fs=FileSystems.getDefault();
        Path p1=fs.getPath("d:/test1.srt");
        Path p2=Paths.get("d:/test1.srt");
        System.out.printf("getFileName : %s\n", p1.getFileName());
        System.out.printf("getParent : %s\n", p1.getParent());
        System.out.printf("getNameCount : %d\n", p1.getNameCount());
        System.out.printf("getRoot : %s\n", p1.getRoot());
        System.out.printf("toAbsoultePath : %s\n", p1.toAbsolutePath());
        System.out.printf("toURI : %s\n", p1.toUri());
    }
}

刪除Path多餘的字元

Path p3=Paths.get("/home/peter/../clarence/foo");
Path p4=p3.normalize();
System.out.println(p4);

結果為
/home/clarence/foo

subpath的用法

Path subpath(int beginIndex, int endIndex) : beginIndex由0開始, endIndex由1開始
Path p1=Paths.get(“d:/Temp/foo/bar”);
Path p2=p1.subpath(1,3);
結果是 foo/bar  <==d:/不算, beginIndex由0開始, endIndex由1開始. 所以Temp對beginIndex 而言, 編號為0.  對endIndex而言, 編號為1.

路徑合併 -resolve()

Path p1=Paths.get(“/home/foo/bar”);
p1=p1.resolve(“abcd”);
System.out.println(p1); <==印出 \home\foo\bar\abcd

後合併目錄若有根目錄,則合併無效
Path p1=Paths.get(“/home/foo/bar”);
p1=Paths.get(“test”).resolve(p1);<==無作用, 還是\home\foo\bar

創建兩個路徑之間的相對路徑 – relativize()

創建兩個指定的目錄或檔之間的相對路徑
Path p1 = Paths.get(“joe/foo”);
Path p2 = Paths.get(“sally”);
首先, Java會自動認為 joe和sally是同一級的兄弟目錄

Path p1_to_p2 = p1.relativize(p2);   // 結果是 ../../sally
Path p2_to_p1 = p2.relativize(p1);   // 結果是 ../joe/foo

Path p1 = Paths.get(“home”);
Path p3 = Paths.get(“home/sally/bar”);
Path p1_to_p3 = p1.relativize(p3);  // 结果是 sally/bar
Path p3_to_p1 = p3.relativize(p1);  // 结果是 ../..

Files類別操作

檢查檔案或目錄
刪除檔案或目錄
copy 檔案或目錄
移動檔案或目錄
管理Metadata
讀寫及建立檔案
隨機存取檔案
建立及讀取目錄

檢查檔案及目錄

System.out.println(Files.exists(p1, LinkOption.NOFOLLOW_LINKS)); <==檢查此檔案或目錄是否存在

Files.notExists(p1, LinkOption.NOFOLLOW_LINKS)<==如果p1不存在, 則為true

注意 !Files.exists() 並不等於Files.notExists(), 因為二個可能都是false, 造成此現象是因為去存取了離線的裝置, 如CD-ROM

其他方法
Files.isReadable(path)
Files.isWritable(path)
Files.isExecutable(path)
Files.isSameFile(Path, Path)

建立檔案及目錄

Files.createDirectory(Paths.get(“d:/test/abc”)); <==建立目錄, 注意, d:\test必需先存在
Files.createFile(Paths.get(“d:/test/1.txt”)); <==建立檔案, 注意, d:\test必需先存在

上面會產生IOException

刪除檔案及目錄

Files.delete(path) 會丟出NoSuchFileException
Files.deleteIfExists(path) 不會丟出NoSuchFileException, 但會會丟出IOException例外(如裏面有東西時)

若刪除目錄, 而裏面有東西的話, 則砍不掉

複製檔案及目錄

若複製的是目錄, 則裏面的檔案及目錄不會被複製
Files.copy(p1, p2, StandardCopyOption.REPLACE_EXISTING, LinkOption.NOFOLLOW_LINKS);

copy Stream to Path

可將網路上的網頁, 圖片等, 下載到檔案裏. 先將網址用URI產生, 再使用URI物件的toRUL().openStream() 開啟InputStream連結物件, 此時就可以使用Files.copy(inputStream, Paths.get(“檔名”) 進行下載

public class PathTest {
    public static void main(String[] args) {
        Path path=Paths.get("d:/test.html");
        URI u=URI.create("https://www.oracle.com/index.html");
        try(InputStream in=u.toURL().openStream()){
            Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException ex) {
            Logger.getLogger(PathTest.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

ps. URI 是uniform resource identifier的縮寫(統一資源標識符), 是一個唯一的識別標籤.  而URL是uniform resource locator(統一資源定位器), 是真正的網址, 也只能由此定位器取得資料

移動檔案或目錄

    Path path=Paths.get("d:/test.html");
    try {
        Files.move(path, Paths.get("d:/test.txt"), StandardCopyOption.REPLACE_EXISTING);
    } catch (IOException ex) {
        Logger.getLogger(PathTest.class.getName()).log(Level.SEVERE, null, ex);
    }

如果target 目錄存在且裏面有東西, 則丟出DirectoryNotEmptyException

Source如果是檔案, 且target是一個己存在的目錄, 則目錄會被砍掉, 再將檔案改式跟目錄同名的檔案

Source裏面如果有東西, 也會跟著移動

列出目錄裏的內容

利用Files.newDirectoryStream()產生DirectoryStream泛型物件, 然後就可以將DirectoryStream裏的東西一一列出

    Path path=Paths.get("d:/");
    try {DirectoryStream<Path> stream=Files.newDirectoryStream(path, "*");
        for(Path file : stream){
            System.out.println(file.getFileName());
        }
    } catch (IOException ex) {
        Logger.getLogger(PathTest.class.getName()).log(Level.SEVERE, null, ex);
    }

* : 包含檔案及目錄
*.* : 只列出檔案

Reading/Writing all bytes or lines from a file

    public static void main(String[] args) {
        Path path=Paths.get("d:/test1.srt");
        List<String> lines=null;
        Charset cs=Charset.forName("Unicode");
        try {
            lines=Files.readAllLines(path, cs);
        } catch (IOException ex) {
            Logger.getLogger(PathTest.class.getName()).log(Level.SEVERE, null, ex);
        }
        for (String s:lines){
            System.out.println(s);
        }
        Path target=Paths.get("d:/test2.txt");
        try {
            Files.write(target, lines, 
                 StandardOpenOption.CREATE, 
                 StandardOpenOption.TRUNCATE_EXISTING, 
                 StandardOpenOption.WRITE);
        } catch (IOException ex) {
            Logger.getLogger(PathTest.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

一次讀寫檔案, 對小檔案比較有效率,大檔案就千萬別用.
readAllLines需加入Charset

Channels and ByteBuffers

Stream I/O一次只讀取一個字元, Channel I/O一次會讀取一個buffer的字元. ByteChannel介面提供此基本讀寫功能. SeekableByteChannel 具改變位置的功能

隨機存取檔案

使用SeekableByteChannel介面, 先開檔, 再seek到特定位置, 再讀寫
常用方法
position() : 目前位置
position(long) : 設定位置
read(ByteBuffer) : 讀取bytes放入buffer
write(ByteBuffer) : 從buffer寫入bytes
truncate(long) : 切除檔案

Buffered I/O 方法

就是使用Files.newBufferedReader()來產生BufferedReader物件. 切記, 尤如第十章所言, 這是讀取字元資料

BufferedReader reader=Files.newBufferedReader(path, cs);
String line=reader.readLine();
System.out.println(line);
BufferedWriter writer=Files.newBufferedWriter(path, cs);
writer.write(line, 0, line.length());

Byte Streams

NIO.2 支援開啟byte stream, 就是使用BufferedReader(new InputStreamReader(Files.newInputStream(path)), 如下程式碼
InputStream in=Files.newInputStream(path);
BufferedReader reader=new BufferedReader(new InputStreamReader(in));
String line=reader.readLine();

建檔, append 到檔案, 或寫入檔案, 可使用newOutputStream
OutputStream out=new BufferedOutputStream(Files.newOutputStream(path));
out.write(line.getBytes(), 0, line.length());

管理Metadata -java.nio.file.attribute套件

Method Explanation
size 取得檔案大小
isDirectory 是否為目錄
isRegularFile 是否為一般檔案
isSymbolicLink 是否為連結
isHidden 是否為隱藏檔
getLastModifiedTime 取得最後修改時間
setLastModifiedTime 設定最後修改時間
getAttribute 取得檔案屬性
setAttribute 設定檔案屬性

如Files.size(path)

一次讀取所有屬性

BasicFileAttributes bfa =Files.readAttributes(path, BasicFileAttributes.class);
System.out.println(bfa.creationTime());

DOS檔案屬性

DosFileAttributes attrs =Files.readAttributes(path, DosFileAttributes.class);
System.out.println(attrs.creationTime());
Files.setAttribute(path, “dos:hidden“, true);

屬性有dos:readonly, dos:hidden, dos:system, dos:archive
屬性類別有BasicFileAttribute, PosixFileAttribute, FileOwnerAttribute, AclFileAttribute

常見的檔案系統
BasicFileAttributes : 所有檔案系統共通的屬性
DosFileAttributes : Dos檔案系統
AclFileAttributes : NTFS 檔案系統
PosixFileAttributes : Linux檔案系統

遞回操作

取得檔案樹需使用Files.walkFileTree(Path, FileVisitor<Path>)
FileVisitor是一個介面, 需實作四個方法
preVisitDirectory : 進入目錄前, 可再此統計目錄深度
postVisitDirectory : 所有目錄皆走完後, 要砍目錄的時機
visitFile : 訪問檔案時, 要砍檔案的時機
visitFileFailed : 訪問失敗時

四個方法都需有傳回值
FileVisitorResult.CONTINUE : 繼續下一個節點
FileVisitorResult.SIBLINGS : 跳過同等級的檔案目錄
FileVisitorResult.SUBTREE : 跳過此目錄
FileVisitorResult.TERMINATE : 終止

SimpleFileVisitor己實作上面四個方法, 所以只要繼承此類別, 再覆寫需要改的方法即可

列出所有檔案

public class PathTest {
    public static void main(String[] args) {
        Path path=Paths.get("d:/");
        try {
            Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
                //拜訪檔案後,會執行的方法
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs){
                    System.out.println(file.toAbsolutePath());
                    return FileVisitResult.CONTINUE;//繼續訪問下一個資料夾
                }
            });
        } catch (IOException ex) {
            Logger.getLogger(PathTest.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

 刪除檔案

public static void main(String[] args) {
        try {
            Path p1=Paths.get("d:/2018");
            Files.walkFileTree(p1, new SimpleFileVisitor<Path>(){
                
                //拜訪檔案後,會執行的方法
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs){
                    try{
                        Files.delete(file);
                    }
                    catch(IOException e){}
                    return FileVisitResult.CONTINUE;
                }
                
                //拜訪完所有檔案後, 會執行的方法,也是刪除目錄的好時機
                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc){
                    try{
                        Files.delete(dir);
                    }
                    catch(IOException e){}
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException ex) {
            System.out.println("Error: "+ex.getMessage());
        }
    }

搜尋檔案

使用java.nio.file.PathMatcher介面搜尋, 每個檔案系統都有自己的因子, 所以PathMatcher需如下產生
PathMatcher matcher=FileSystems.getDefault().getPathMatcher(“glob:*.java”);

搜尋字串 : syntax:pattern

syntax可以是 glob 或 regex

Pattern語法如下
*.java : 以 .java作結尾的檔案
*.* : 中間有點的檔案
*.{java, classs} : 以 .java 或 .class 結尾的檔案
foo.? : 開頭是foo. 且結尾只有一個字的檔案
c:\\* : c:\下的所有檔案

將PathMatcher傳入FileVisitor裏, 然後再使用walkFileTree訪問檔案樹

public class PathTest {
    public static void main(String[] args) {
        Path path=Paths.get("d:/");
        PathMatcher matcher=FileSystems.getDefault().getPathMatcher("glob:*.java");
        Finder finder=new Finder(path, matcher);
        try {
            Files.walkFileTree(path, finder);
        } catch (IOException ex) {
            Logger.getLogger(PathTest.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}
class Finder extends SimpleFileVisitor{
    private Path file;
    private PathMatcher matcher;
    public Finder(Path file, PathMatcher matcher){
        this.file=file;
        this.matcher=matcher;
    }
    private void find(Path file){
        Path name=file.getFileName();
        if(name!=null && matcher.matches(name)){
            System.out.println(file);
        }
    }
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs){
        find(file);
	return FileVisitResult.CONTINUE; //繼續往下找
    }
}

FileStore class

FileStore類別是取得所有硬碟的狀況. 先用FileSystems.getDefault()取得FileSystem fs後, 再用fs.getFileStores()得到FileStore物件

    public class FileStoreTest {
        public static void main(String[] args) {
            FileSystem fs=FileSystems.getDefault();
            Iterable<FileStore> fileStores=fs.getFileStores();
            DecimalFormat format=new DecimalFormat("###,###.##");
            for (FileStore store:fileStores){
                try {
                    System.out.println(store +"\t總空間 : "+format.format(store.getTotalSpace()/1024/1024.0f)+
                            "M\t可用空間 : "+format.format(store.getUsableSpace()/1024/1024.0f)+"M");
                } catch (IOException ex) {
                    Logger.getLogger(FileStoreTest.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    }

    列印結果 :
    Win7 (C:) 總空間 : 244,197M 可用空間 : 89,013.61M
    HDISK-D (D:) 總空間 : 953,867M 可用空間 : 512,419.12M
    Wormhole (G:) 總空間 : 2.88M 可用空間 : 0M

WatchService

此介面可以監控指定目錄下是否有改變發生. 先由FileSystems.getDefault().newWatchService() 取得WatchService ws物件, 再由Path path物件註冊要監控的事件. 然了就可以ws.take取得發生的事件

    public class WatchServiceTest {
        public static void main(String[] args) throws Exception{
            WatchService ws=FileSystems.getDefault().newWatchService();
            Path path=Paths.get("d:/");
            path.register(ws,
                    StandardWatchEventKinds.ENTRY_CREATE,
                    StandardWatchEventKinds.ENTRY_DELETE,
                    StandardWatchEventKinds.ENTRY_MODIFY);
            while(true){
                System.out.println("系統監聽中....");
                WatchKey key=ws.take();
                for(WatchEvent<?>event:key.pollEvents()){
                    System.out.println("Event : "+event.kind().name() +",內容 : "+event.context().toString());
                }
                key.reset();
            }
        }
    }

Java.io.File 轉換

早期使用java.io.File所寫的程式, 可以直接使用 Path path=file.toPath()轉成NIO.2
NIO.2也可以使用File file=path.toFile()轉成早期的用法

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *