专栏首页菩提树下的杨过NIO复习(2):channel

NIO复习(2):channel

上篇学习了NIO的buffer,继续来学习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();
    }

三、文件修改

场景:某个文件需要把最后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个字,从“过”变成了“佛”:

这个方法,对于小文件而言没什么问题,但如果文件是一个几G的巨无霸,会遇到2个问题:

首先是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后,文件内容就相应的修改了。

值得一提的是,从上面调试的截图来看,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/

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Silverlight:利用Panel实现自定义布局

    虽然Silverlight提供了几种基本的布局方式,比如Canvas,Grid,StackPanel,Border...,但有时候可能仍然会觉得不够用。 这时候...

    菩提树下的杨过
  • java: web应用中不经意的内存泄露

    前面有一篇讲解如何在spring mvc web应用中一启动就执行某些逻辑,今天无意发现如果使用不当,很容易引起内存泄露,测试代码如下: 1、定义一个类App ...

    菩提树下的杨过
  • java: web应用中不经意的内存泄露

    前面有一篇讲解如何在spring mvc web应用中一启动就执行某些逻辑,今天无意发现如果使用不当,很容易引起内存泄露,测试代码如下: 1、定义一个类App ...

    菩提树下的杨过
  • async/await 写法示例

    async/await 让写异步代码感觉像写同步代码。async/await 并不是 ES6 的一部分,但可以通过使用 Babel 来使用它。

    Joel
  • C#调用OpenCV开发简易版美图工具

    本文主要介绍在WPF项目中使用OpenCVSharp3-AnyCPU开源类库处理图片,下面我们先来做开发前的准备工作。

    Kiba518
  • Gym 100952C&&2015 HIAST Collegiate Programming Contest C. Palindrome Again !!【字符串,模拟】

    C. Palindrome Again !! time limit per test:1 second memory limit per test:64 meg...

    Angel_Kitty
  • 推荐系统丨YouTube召回模型设计

    随着互联网行业的高速发展,人们获取信息的方式越来越多。人们对信息获取的有效性和针对性的需求随之出现,推荐系统也应运而生。推荐系统就是互联网时代的一种信息检索工具...

    博文视点Broadview
  • 似然与概率的异同

    假设有一枚硬币,我们想确定这枚硬币是否质地均匀。即想知道抛这枚硬币,正反面出现的概率各是多少?于是我们将这枚硬币抛了10次,得到的数据x0是:反正正正正反正正正...

    Bo_hemian
  • 我的CMS开发记 -引子

            我今年4月份的时候,需要给公司做一个门户网站。我倒是还从来没使用过CMS系统,于是上网搜了一把,冥冥之中注定我搜到的是DotNetNuke.  ...

    用户1687945
  • python crontab 坑

    最近用Python写了一些数据统计的脚本,并使用crontab自动执行,但是配置crontab总是要过几个坑才行的,这里总结一下这次遇到的坑。

    有福

扫码关注云+社区

领取腾讯云代金券