天天看點

NIO複習(2):channel

上篇學習了NIO的buffer,繼續來學習channel,類圖如下(注:為了不讓圖看起來太複雜,隐藏了一些中間的接口)

NIO複習(2):channel

Channel派生了很多子接口,其中最常用的有FileChannel(用于檔案操作)以及SocketChannel、ServerSocketChannel(用于網絡通訊),下面用幾段示例代碼學習其基本用法:

一、檔案寫入

1.1 入門示例

public static void fileWriteReadSimpleDemo() throws IOException {
    String filePath = "/tmp/yjmyzz.txt";
    //檔案寫入
    String fileContent = "菩提樹下的楊過";
    FileOutputStream outputStream = new FileOutputStream(filePath);
    FileChannel writeChannel = outputStream.getChannel();

    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    byteBuffer.put(fileContent.getBytes());
    byteBuffer.flip();//别忘記了,反轉position,否則此時position已經移到最後1個有效字元處,下一行将讀不到資料

    //緩沖區的資料,通過channel寫入檔案
    writeChannel.write(byteBuffer);

    writeChannel.close();

    //檔案讀取
    File file = new File(filePath);
    FileInputStream inputStream = new FileInputStream(file);
    FileChannel readChannel = inputStream.getChannel();

    //注:這裡要重新指定實際大小,否則byteBuffer前面初始化成1024長度,檔案内容不足1024位元組,
    // 後面的空餘部分全是預設0填充,最終轉換成字元串時,填充的0,也會轉換成不可見字元輸出
    byteBuffer = ByteBuffer.allocate((int) file.length());
    readChannel.read(byteBuffer);
    System.out.println(new String(byteBuffer.array()));
    readChannel.close();
}           

複制

FileOutputStream類中内嵌了一個FileChannel的執行個體,通過getChannel()方法可以擷取引用。寫檔案緩沖區初始化時,如何設定正确的大小,這個不太好掌握,設定太大浪費記憶體,設定太小又裝不下,正确姿勢可參考下面的示例2

1.2 緩沖區不夠大時循環寫入

public static void writeFileDemo() throws IOException {
        String fileContent = "菩提樹下的楊過(http://yjmyzz.cnblogs.com/)\n\n" +
                "送柴侍禦\n" +
                "【作者】王昌齡 【朝代】唐\n" +
                "沅水通波接武岡,送君不覺有離傷。\n" +
                "青山一道同雲雨,明月何曾是兩鄉。\n";
        //故意設定一個很小的緩沖區,示範緩沖區不夠大的情況
        ByteBuffer byteBuffer = ByteBuffer.allocate(5);
        String filePath = "/tmp/yjmyzz.txt";
        FileOutputStream outputStream = new FileOutputStream(filePath);
        FileChannel writeChannel = outputStream.getChannel();

        //将檔案内容,按緩沖區大小拆分成一段段寫入
        byte[] src = fileContent.getBytes();
        int pages = (src.length % byteBuffer.capacity() == 0) ? (src.length / byteBuffer.capacity())
                                                                : (src.length / byteBuffer.capacity() + 1);
        for (int i = 0; i < pages; i++) {
            int start = i * byteBuffer.capacity();
            int end = Math.min(start + byteBuffer.capacity() - 1, src.length - 1);
            for (int j = start; j <= end; j++) {
                byteBuffer.put(src[j]);
            }
            byteBuffer.flip();
            writeChannel.write(byteBuffer);
            //記得清空
            byteBuffer.clear();
        }
        writeChannel.close();
    }           

複制

注意:檔案讀取時,直接通過File對象的length可以提前知道緩沖的大小,能精确指定Buffer大小,不需要類似這麼複雜的循環處理。

二、檔案複制

public static void copyFileDemo() throws IOException {
        String srcFilePath = "/tmp/yjmyzz.txt";
        File srcFile = new File(srcFilePath);
        String targetFilePath = "/tmp/yjmyzz.txt.bak";
        FileInputStream inputStream = new FileInputStream(srcFile);
        FileOutputStream outputStream = new FileOutputStream(targetFilePath);

        FileChannel inputChannel = inputStream.getChannel();
        FileChannel outputChannel = outputStream.getChannel();

        //檔案複制
        ByteBuffer buffer = ByteBuffer.allocate((int) srcFile.length());
        inputChannel.read(buffer);
        buffer.flip();
        outputChannel.write(buffer);

        //也可以用這一行,搞定檔案複制(推薦使用)
//        outputChannel.transferFrom(inputChannel, 0, srcFile.length());

        inputChannel.close();
        outputChannel.close();
    }           

複制

NIO複習(2):channel

三、檔案修改

場景:某個檔案需要把最後1個漢字,修改成其它字。先寫一段代碼,生成測試用的檔案

public static void writeLargeFile() throws IOException {
        String content = "12345678-abcdefg-菩提樹下的楊過\n";
        String filePath = "/tmp/yjmyzz.txt";
        FileOutputStream outputStream = new FileOutputStream(filePath);
        FileChannel writeChannel = outputStream.getChannel();

        ByteBuffer buffer = ByteBuffer.allocate(128);
        buffer.put(content.getBytes());
        for (int i = 0; i < 10; i++) {
            buffer.flip();
            writeChannel.write(buffer);
        }
        writeChannel.close();
    }           

複制

運作完後,測試檔案中的内容如下:

12345678-abcdefg-菩提樹下的楊過
12345678-abcdefg-菩提樹下的楊過
12345678-abcdefg-菩提樹下的楊過
12345678-abcdefg-菩提樹下的楊過
12345678-abcdefg-菩提樹下的楊過
12345678-abcdefg-菩提樹下的楊過
12345678-abcdefg-菩提樹下的楊過
12345678-abcdefg-菩提樹下的楊過
12345678-abcdefg-菩提樹下的楊過
12345678-abcdefg-菩提樹下的楊過           

複制

3.1 正常方法示例

public static void modify1() throws IOException {
        String filePath = "/tmp/yjmyzz.txt";
        File file = new File(filePath);
        FileInputStream inputStream = new FileInputStream(file);
        FileChannel inputChannel = inputStream.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate((int) file.length());

        byte[] tempBytes = "佛".getBytes();
        inputChannel.read(buffer);
        buffer.flip();
        //修改最後1個漢字
        for (int i = 0; i < tempBytes.length; i++) {
            //最後有一個回車符,然後漢字utf-8占3個位元組,是以這裡要減4,才是最後1個漢字
            int j = buffer.limit() - 4 + i;
            buffer.put(j, tempBytes[i]);
        }

        FileOutputStream outputStream = new FileOutputStream(filePath);
        FileChannel outputChannel = outputStream.getChannel();
        outputChannel.write(buffer);

        inputChannel.close();
        outputChannel.close();
    }           

複制

運作完後,從下面的截圖可以看到,測試最後1個字,從“過”變成了“佛”:

NIO複習(2):channel

這個方法,對于小檔案而言沒什麼問題,但如果檔案是一個幾G的巨無霸,會遇到2個問題:

NIO複習(2):channel

首先是allocate方法,隻接受int型參數,對于幾個G的大檔案,File.length很有可能超過int範圍,無法配置設定足夠大的緩沖。其次,就算放得下,幾個G的内容全放到記憶體中,也很可能造成OOM,是以需要其它辦法。

3.2 利用RandomAccessFile及Channel.map修改檔案

public static void modify2() throws IOException {
        String filePath = "/tmp/yjmyzz.txt";
        RandomAccessFile file = new RandomAccessFile(filePath, "rw");
        FileChannel channel = file.getChannel();
        //将最後一個漢字映射到記憶體中
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, file.length() - 4, 3);

        byte[] lastWordBytes = "新".getBytes();

        //這樣就直接在記憶體中修改了檔案,不再需要調用channel.write
        mappedByteBuffer.put(lastWordBytes);

        channel.close();
    }           

複制

這個方法相對就進階多了,RandomAccessFile類是File類的加強版,允許以遊标的方式,直接讀取檔案的某一部分,另外Channel.map方法,可以直接将檔案中的某一部分映射到記憶體,在記憶體中直接修MappedByteBuffer後,檔案内容就相應的修改了。

NIO複習(2):channel

值得一提的是,從上面調試的截圖來看,FileChannel.map方法傳回的MappedByteBuffer,真實類型是它下面派生的子類DirectByteBuffer,這是“堆外”記憶體,不在JVM 自動垃圾回收的管轄範圍。

參考文章:

https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/nio/channels/Channel.html

http://ifeve.com/channels/