netty(三)nio之文件編程 一、FileChannel常用操作 二、channel的相互傳輸 三、Path 和 Paths 類 四、Files類

本篇文章主要討論一下NIO中的文件變成,主要是FileChannel的用法。

一、FileChannel常用操作

1.1 獲取FileChannel

有一個文件text.txt,其內容如下:

abcdef

不能直接打開 FileChannel,必須通過 FileInputStream、FileOutputStream 或者 RandomAccessFile 來獲取 FileChannel,它們都有 getChannel 方法

1.1.1 通過 FileInputStream 獲取

    public static void main(String[] args) throws Exception {
        //使用FileInputStream獲取channel
        FileInputStream fileInputStream = new FileInputStream(new File("C:\\Users\\P50\\Desktop\\text.txt"));
        FileChannel channel1 = fileInputStream.getChannel();
        ByteBuffer buffer= ByteBuffer.allocate(10);
        //channel1.write(buffer);
        channel1.read(buffer);
        buffer.flip();
        System.out.println((print(buffer)));
    }

    static String print(ByteBuffer b) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < b.limit(); i++) {
            stringBuilder.append((char) b.get(i));
        }
        return stringBuilder.toString();
    }

結果:

abcdef

通過 FileInputStream 獲取的 channel 只能讀,如果使用寫入write方法,會拋出異常:

Exception in thread "main" java.nio.channels.NonWritableChannelException
    at sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:201)
    at com.cloud.bssp.nio.FileChannel.GetFileChannel.main(GetFileChannel.java:21)

1.1.2 通過 FileOutputStream 獲取

    public static void main(String[] args) throws Exception {
        //使用FileOutputStream獲取channel
        FileOutputStream fileOutputStream = new FileOutputStream(new File("C:\\Users\\P50\\Desktop\\text.txt"),true);
        FileChannel channel2 = fileOutputStream.getChannel();
        ByteBuffer buffer= ByteBuffer.allocate(10);
        buffer.put(StandardCharsets.UTF_8.encode("helloworld"));
        buffer.flip();
        channel2.write(buffer);
    }

    static String print(ByteBuffer b) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < b.limit(); i++) {
            stringBuilder.append((char) b.get(i));
        }
        return stringBuilder.toString();
    }

文件被寫入,這裏注意FileOutputStream 的屬性append,如果是true,表示追加,否則覆蓋。本文使用的追加。

abcdefhelloworld

通過 FileOutputStream 獲取的 channel 只能寫,如果使用read方法,會拋出異常:

Exception in thread "main" java.nio.channels.NonReadableChannelException
    at sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:149)
    at com.cloud.bssp.nio.FileChannel.GetFileChannel.main(GetFileChannel.java:28)

1.1.3 通過 RandomAccessFile獲取

    public static void main(String[] args) throws Exception {
        //使用RandomAccessFile獲取channel
        RandomAccessFile file = new RandomAccessFile("C:\\Users\\P50\\Desktop\\text.txt", "rw");
        FileChannel channel3 = file.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(15);
        //讀取文件內容到buffer
        channel3.read(buffer);
        buffer.flip();
        System.out.println(print(buffer));

        // 切換爲寫模式,並且清空buffer
        buffer.clear();
        //寫入helloworld到文件
        buffer.put(StandardCharsets.UTF_8.encode("helloworld"));
        buffer.flip();
        channel3.write(buffer);
    }

        // 切換爲寫模式,並且清空buffer
        buffer.clear();
        //寫入helloworld到文件
        buffer.put(StandardCharsets.UTF_8.encode("helloworld"));
        buffer.flip();
        channel3.write(buffer);

結果:這裏讀取的少了一個字節,因爲我指定的buffer只有15,文檔中是16,只讀取了一次,。

abcdefhelloworl

文檔內容被修改爲如下,將channel讀取到的內容以及新加入的內容拼接在了一起

abcdefhelloworlhelloworld

通過 RandomAccessFile 是否能讀寫根據構造 RandomAccessFile 時的讀寫模式決定,指定rw(讀寫模式)。

1.2 讀取和寫入

1.2.1 讀取

在前面的獲取例子中已經給出了關於讀取的方式,如下所示,會返回int類型,從 channel 讀取數據填充ByteBuffer,返回值表示讀到了多少字節,返回值爲-1 表示到達了文件的末尾。

int readBytes = channel.read(buffer);

仍然使用上面的第一個例子,如果文檔是空的話,則會返回-1

 int read = channel1.read(buffer);
 System.out.println(read);
-1

1.2.2 寫入

如上一章節的例子,已經演示瞭如何寫入數據,利用write方法,將buffer的數據寫入channel,但是正確的寫入方式應該如下所示:

while(buffer.hasRemaining()) {
    channel.write(buffer);
}

hasRemaining()是buffer的一個方法,判斷position是否小於limit,是則返回true,表示buffer仍然有未讀取的數據。

在 while 中調用 channel.write 是因爲 write 方法並不能保證一次將 buffer 中的內容全部寫入 channel。

1.2.3 強制寫入

操作系統出於性能的考慮,會將數據緩存,不是立刻寫入磁盤。可以調用 channel.force(true) 方法將文件內容和元數據(文件的權限等信息)立刻寫入磁盤。

public abstract void force(boolean metaData) throws IOException;

1.3 關閉

像我們上面寫的代碼實際上都沒有去關閉流和channel的,這如果在生產環境都是會產生嚴重的問題。

channel是必須要關閉的,不過調用了 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 close() 方法會間接地調用 channel 的 close 方法。

看下FileInputStream的close方法:

    public void close() throws IOException {
        synchronized (closeLock) {
            if (closed) {
                return;
            }
            closed = true;
        }
        if (channel != null) {
           channel.close();
        }

        fd.closeAll(new Closeable() {
            public void close() throws IOException {
               close0();
           }
        });
    }

1.4 FileChannel的位置

獲取當前位置

long pos = channel.position();

設置當前位置

long newPos = ...;
channel.position(newPos);

如下獲取文件channel:

        // 文件內容爲10個字節的helloworld
        RandomAccessFile file = new RandomAccessFile("C:\\Users\\P50\\Desktop\\text.txt", "rw");
        FileChannel channel = file.getChannel();

打印不同設置時的位置:

        // 打印位置,沒有讀取時是0
        System.out.println(channel.position());

        // 讀取後是文件的長度
        ByteBuffer buffer = ByteBuffer.allocate(10);
        channel.read(buffer);
        System.out.println(channel.position());

        // 設置位置後的長度
        FileChannel position = channel.position(5);
        System.out.println(position.position());

結果:

0
10
5

1.5 獲取文件大小

channel.size();

二、channel的相互傳輸

channel提供兩個用來channel相互傳輸數據的方法:

/**
  * 將一個channel的數據傳輸到target這個channel中,其中position,count,都是調用此方法的channel的
  * in.transferTo(0, in.size(), out);
  */
transferTo(long position, long count, WritableByteChannel target)
/**
  * 一個channel從src這個channel獲取數據,其中position,count,都是src這個channel的
  * out.transferFrom(in,0,in.size());
  */
transferFrom(ReadableByteChannel src, long position, long count)

使用例子如下:

public class TestCopyFileByNIO {

    public static void fileChannelCopy(String sfPath, String tfPath) {
        FileInputStream fi = null;
        FileOutputStream fo = null;
        FileChannel in = null;
        FileChannel out = null;
        try {
            fi = new FileInputStream(new File(sfPath));
            fo = new FileOutputStream(new File(tfPath));
            in = fi.getChannel();//得到對應的文件通道
            out = fo.getChannel();//得到對應的文件通道
            in.transferTo(0, in.size(), out);//連接兩個通道,並且從in通道讀取,然後寫入out通道
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                fi.close();
                fo.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        String sPath = "E:\\workspace\\comprehend-service.rar";
        String tPath = "E:\\workspace\\comprehend-service-" + System.currentTimeMillis() + "-bak.rar";
        fileChannelCopy(sPath, tPath);
        long end = System.currentTimeMillis();
        System.out.println("用時爲:" + (end - start) + "ms");
    }
}

結果:

用時爲:194ms

2.1 channel的最大傳輸值

channel的傳輸是有大小限制的,最大爲2個g,超過會導致數據丟失。所以需要使用循環去多次傳輸數據。

public class TestCopyFileByNIO {

    public static void fileChannelCopy(String sfPath, String tfPath) {
        FileInputStream fi = null;
        FileOutputStream fo = null;
        FileChannel in;
        FileChannel out;
        try {
            fi = new FileInputStream(new File(sfPath));
            fo = new FileOutputStream(new File(tfPath));
            in = fi.getChannel();
            out = fo.getChannel();
            // 總文件大小
            long size = in.size();
            // left 剩餘文件的數量
            for (long left = size; left > 0;){
                System.out.println("position = " + (size - left) + ",left = " + left);
                // transferTo返回傳輸的數量,剩餘的減去傳輸的,就是當前剩餘的數量
                left -= in.transferTo((size -left), left, out);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                fi.close();
                fo.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        String sPath = "E:\\workspace\\workspace.zip";
        String tPath = "E:\\workspace\\workspace-" + System.currentTimeMillis() + "-bak.zip";
        fileChannelCopy(sPath, tPath);
        long end = System.currentTimeMillis();
        System.out.println("用時爲:" + (end - start) + "ms");
    }

結果:

position = 0,left = 2925330022
position = 2147483647,left = 777846375
用時爲:13664ms

三、Path 和 Paths 類

jdk7 引入了 Path 和 Paths 類

  • Path 用來表示文件路徑

  • Paths 是工具類,用來獲取 Path 實例

// 相對路徑 使用 user.dir 環境變量來定位 1.txt
Path source = Paths.get("1.txt"); 

// 絕對路徑 代表了  d:\1.txt
Path source = Paths.get("d:\\1.txt"); 

// 絕對路徑 同樣代表了  d:\1.txt
Path source = Paths.get("d:/1.txt"); 

 // 代表了  d:\data\projects
Path projects = Paths.get("d:\\data", "projects");
  • . 代表了當前路徑
  • .. 代表了上一級路徑

例如目錄結構如下

d:
    |- data
        |- projects
            |- a
            |- b

代碼

Path path = Paths.get("d:\\data\\projects\\a\\..\\b");
System.out.println(path);
// 正常化路徑
System.out.println(path.normalize()); 

會輸出

d:\data\projects\a\..\b
d:\data\projects\b

四、Files類

檢查文件是否存在

Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));

創建一級目錄

Path path = Paths.get("helloword/d1");
Files.createDirectory(path);
  • 如果目錄已存在,會拋異常 FileAlreadyExistsException
  • 不能一次創建多級目錄,否則會拋異常 NoSuchFileException

創建多級目錄用

Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);

拷貝文件

Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");

Files.copy(source, target);
  • 如果文件已存在,會拋異常 FileAlreadyExistsException

如果希望用 source 覆蓋掉 target,需要用 StandardCopyOption 來控制

Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

移動文件

Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");

Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
  • StandardCopyOption.ATOMIC_MOVE 保證文件移動的原子性

刪除文件

Path target = Paths.get("helloword/target.txt");

Files.delete(target);
  • 如果文件不存在,會拋異常 NoSuchFileException

刪除目錄

Path target = Paths.get("helloword/d1");

Files.delete(target);
  • 如果目錄還有內容,會拋異常 DirectoryNotEmptyException

遍歷目錄文件

public static void main(String[] args) throws IOException {
    Path path = Paths.get("C:\\Program Files\\Java\\jdk1.8.0_91");
    AtomicInteger dirCount = new AtomicInteger();
    AtomicInteger fileCount = new AtomicInteger();
    Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) 
            throws IOException {
            System.out.println(dir);
            dirCount.incrementAndGet();
            return super.preVisitDirectory(dir, attrs);
        }

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
            throws IOException {
            System.out.println(file);
            fileCount.incrementAndGet();
            return super.visitFile(file, attrs);
        }
    });
    System.out.println(dirCount); // 133
    System.out.println(fileCount); // 1479
}

統計 jar 的數目

Path path = Paths.get("C:\\Program Files\\Java\\jdk1.8.0_91");
AtomicInteger fileCount = new AtomicInteger();
Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
        throws IOException {
        if (file.toFile().getName().endsWith(".jar")) {
            fileCount.incrementAndGet();
        }
        return super.visitFile(file, attrs);
    }
});
System.out.println(fileCount); // 724

刪除多級目錄

Path path = Paths.get("d:\\a");
Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
        throws IOException {
        Files.delete(file);
        return super.visitFile(file, attrs);
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) 
        throws IOException {
        Files.delete(dir);
        return super.postVisitDirectory(dir, exc);
    }
});

刪除是危險操作,確保要遞歸刪除的文件夾沒有重要內容

拷貝多級目錄

long start = System.currentTimeMillis();
String source = "D:\\Snipaste-1.16.2-x64";
String target = "D:\\Snipaste-1.16.2-x64aaa";

Files.walk(Paths.get(source)).forEach(path -> {
    try {
        String targetName = path.toString().replace(source, target);
        // 是目錄
        if (Files.isDirectory(path)) {
            Files.createDirectory(Paths.get(targetName));
        }
        // 是普通文件
        else if (Files.isRegularFile(path)) {
            Files.copy(path, Paths.get(targetName));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});
long end = System.currentTimeMillis();
System.out.println(end - start);

關於NIO文件編程此處就寫到這了,有幫助的話朋友個點個贊

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