首页 > 其他分享 >文件IO:实现高效正确的文件读写

文件IO:实现高效正确的文件读写

时间:2024-09-24 15:45:00浏览次数:1  
标签:文件 读取 读写 try IO 使用 new txt

背景

本篇将会讲一些文件读写的推荐使用姿势以及编码时的注意事项,便于新手更好地理解如何高效地进行大文件读写,比如利用好缓冲区避免出现OOM,或者及时地释放资源以保证资源被及时地关闭,避免资源泄露。

处理中文时读取到乱码

大家都知道,中文的编码和英文的编码使用的字符集是不一样的,字符集不匹配的时候读取中文很容易出现乱码问题。下面我举个例子,说明一下读取中文时如何解决乱码问题。
1、使用下面代码先创建一个hello.txt文件,编码格式为GBK;文件内容是“你好hi”

        Files.deleteIfExists(Paths.get("hello.txt"));
        Files.write(Paths.get("hello.txt"), "你好hi".getBytes(Charset.forName("GBK")));
        log.info("bytes:{}", Hex.encodeHexString(Files.readAllBytes(Paths.get("hello.txt"))));

2、使用下面代码读取这个hello文件中的中文并打印


        char[] chars = new char[10];
        String content = "";
        try (FileReader fileReader = new FileReader("hello.txt")) {
            int count;
            while ((count = fileReader.read(chars)) != -1) {
                content += new String(chars, 0, count);
            }
        }

3、打印结果。

12:33:42.976 [main] INFO com.example.demo3.commonpitfalls.FileIOTest - bytes:c4e3bac36869
12:33:42.993 [main] INFO com.example.demo3.commonpitfalls.FileIOTest - result:���hi

可以发现你好没有正确显示,而是出现乱码。
4、分析
出现乱码的原因是我们在对你好hi进行编码的时候,使用的是GBK, 但是读取时使用FileReader,这边想说明的是,FileReader 是以当前机器的默认字符集来读取文件的
也就是说,默认使用IDEA默认的机器码来解码,默认的字符集是UTF-8。所以,当前机器默认字符集是 UTF-8,自然无法读取 GBK 编码的汉字,因而出现了乱码。
解决这个问题也很简单,就是我们在编码的时候就使用UTF_8, 解码的时候本来默认的就是UTF_8, 这样就不会有乱码问题了。
修复代码如下:

 Files.deleteIfExists(Paths.get("hello3.txt"));
        Files.write(Paths.get("hello3.txt"), "你好hi".getBytes(Charset.forName("UTF-8")));
        log.info("bytes:{}", Hex.encodeHexString(Files.readAllBytes(Paths.get("hello3.txt"))));


        char[] chars = new char[10];
        String content = "";
        try (FileReader fileReader = new FileReader("hello3.txt")) {
            int count;
            while ((count = fileReader.read(chars)) != -1) {
                content += new String(chars, 0, count);
            }
        }

         log.info("result:{}", content);

打印结果:

12:41:10.105 [main] INFO com.example.demo3.commonpitfalls.FileIOTest - bytes:e4bda0e5a5bd6869
12:41:10.112 [main] INFO com.example.demo3.commonpitfalls.FileIOTest - result:你好hi

这个时候有人又会问了,如果我就要使用GBK来编码hello.txt文件,如何能解码成功不会乱码呢?当然也是有方法的。可直接使用 FileInputStream 拿文件流,
然后使用 InputStreamReader 读取字符流,并指定字符集为 GBK!

// 使用FileInputStream, InputStreamReader
        char[] chars = new char[10];
        String content = "";
        // 使用try-with-resources来释放资源,语句中打开的资源会在代码块执行完毕后自动关闭,无需手动调用关闭方法,避免了资源泄漏。
        // 无需使用finally 手动释放!
        try( FileInputStream fileInputStream = new FileInputStream("hello.txt");
                InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, Charset.forName("GBK"));) {
                int count;
                while ((count = inputStreamReader.read(chars)) != -1) {
                    content += new String(chars, 0, count);
                }
                log.info("result: {}", content);
        } catch (IOException ex){
            ex.printStackTrace();
        }

5、总结

String text = "你好,世界!";
byte[] gbkBytes = text.getBytes(Charset.forName("GBK"));
String decodedText = new String(gbkBytes, Charset.forName("GBK"));
System.out.println(decodedText);

以上是使用GBK对中文编码解码的一个简单的例子,这个例子是可以正常打印“你好世界!”说到这, 就有人想问问GBK和UTF-8的区别了。
GBK和UTF-8都是字符编码方案,用于在计算机中表示和存储文本数据。对于中文的表示,这二者都可以使用。只不过在使用的时候有一些区别,我们一般会使用UTf-8比较多一点。这是因为:
对字节长度而言,
GBK 是双字节编码,即一个汉字通常占用两个字节,但有些生僻字可能需要三个或四个字节来表示。
UTF-8 是一种变长编码方案,一个字符的长度可以是1到4个字节不等,常见的英文字符只占用一个字节,常见的汉字占用三个字节。
所以,UTF-8可以表示更多的汉字。
从用途上来说,
GBK主要用于中文字符编码,包括简体中文中的常用汉字、符号等。
UTF-8 是一种全球通用的编码方案,能够表示几乎所有的字符,包括世界上所有语言的文字、符号和表情符号。
从存储上来看,GBK一般使用2个字节来存储汉字,但是UTF-8会使用3个字节来保存汉字。所以,使用GBK编码的汉字,用UTF-8来解码,必然不会成功了。

Files 类的readAllLines

使用前文提到的FileInputStream, InputStreamReader, 看起来会比较繁琐。Files 类的readAllLines是一种比较易用的方法,该方法是JDK7推出的,它可以很方便地用一行代码完成整个文件内容的读取。如下所示:

 log.info("result: {}", Files.readAllLines(Paths.get("hello.txt"), Charset.forName("UTF-8")));

打开readAllLines()的源码可以发现,读取的字符都会被放在这个List中,List虽然是动态增长的,但是如果内存无法存储这个增长到很大容量的List, 必然会抛出这个OOM。

 public static List<String> readAllLines(Path path, Charset cs) throws IOException {
        try (BufferedReader reader = newBufferedReader(path, cs)) {
            List<String> result = new ArrayList<>();
            for (;;) {
                String line = reader.readLine();
                if (line == null)
                    break;
                result.add(line);
            }
            return result;
        }
    }

所以,readAllLines的缺点就是,如果文件非常大的时候,读取超出内存大小的大文件时会出现OOM。

Files类的lines()

上文提到的readAllLine()是一次性读取内容到内存中,其实某些场景,比如下载大文件,我们可以一次只读取一部分数据到内存中,然后再进行数据的处理。
lines()方法就是这样的实现。接下来,我们说说使用 lines 方法时需要注意的一些问题。
与 readAllLines 方法返回 List 不同,lines 方法返回的是 Stream,这使得我们在需要时可以不断读取、使用文件中的内容。

// 总共读取2000行
 log.info("lines {}", Files.lines(Paths.get("hello3.txt")).limit(2000).collect(java.util.stream.Collectors.joining("\n")));

但是我们可以想一下这两种方式的差异在哪里,一次读取,只需要一次IO即可,但是多次读取,需要多次打开磁盘的文件,多次IO。虽然不会带来OOM, 但是会频繁的IO。
那么,我们每次读取一小部分数据的时候,就不宜读取地太少,而是按需读取一定大小(如2000)的数据,每次读的数据相对比较大的话,那么IO的次数也就比较少了。
另外,这样处理时,虽然不再有OOM,但是其实也有问题,即读取完文件后没有关闭!
我们通常会认为静态方法的调用不涉及资源释放,因为方法调用结束自然代表资源使用完成,由 API 释放资源,但对于 Files 类的一些返回 Stream 的方法并不是这样。这是一个很容易被忽略的严重问题。
以下例子是模拟 Files.lines 方法分批读取大文件。
首先,我们创建一个demo.txt,写入10行数据。

 String filename = "demo.txt";
        try {
            StringBuilder content = new StringBuilder();
            IntStream.rangeClosed(1, 10)
                    .forEach(i -> content.append("Line ").append(i).append(": This is some sample data.\n"));

            Files.write(Paths.get(filename), content.toString().getBytes(), CREATE, TRUNCATE_EXISTING);
            System.out.println("写入成功!");
        } catch (IOException e) {
            System.err.println("写入文件时出现异常:" + e.getMessage());
        }

然后使用Files.lines 方法读取这个文件 100 万次,每读取一行计数器 +1:

// 读取这个文件 100 万次,每读取一行计数器 +1:
        LongAdder longAdder = new LongAdder();
        IntStream.rangeClosed(1, 1000000).forEach(i -> {
             try {
                Files.lines(Paths.get("demo.txt")).forEach(line -> longAdder.increment());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
    
        });
        log.info("total : {}", longAdder.longValue());

然后就发现,可能会报这样的错误,java.nio.file.FileSystemException: demo.txt: Too many open files
其实,在JDK 文档中有提到,注意使用 try-with-resources 方式来配合,确保流的 close 方法可以调用释放资源。如果报错无法运行,那么请使用try-with-resources!
这也很容易理解,使用流式处理,如果不显式地告诉程序什么时候用完了流,程序又如何知道呢,它也不能帮我们做主何时关闭文件。
修复方式很简单,必须使用 try 来包裹 Stream !

  try (Stream<String> lines = Files.lines(Paths.get("demo.txt"))) {
                lines.forEach(line -> longAdder.increment());
            } catch (IOException e) {
                e.printStackTrace();
            }

查看 lines 方法源码可以发现,Stream 的 close 注册了一个回调,来关闭 BufferedReader 进行资源释放:

public static Stream<String> lines(Path path, Charset cs) throws IOException {
        BufferedReader br = Files.newBufferedReader(path, cs);
        try {
            return br.lines().onClose(asUncheckedRunnable(br));
        } catch (Error|RuntimeException e) {
            try {
                br.close();
            } catch (IOException ex) {
                try {
                    e.addSuppressed(ex);
                } catch (Throwable ignore) {}
            }
            throw e;
        }
    }
private static Runnable asUncheckedRunnable(Closeable c) {
       return () -> {
           try {
               c.close();
           } catch (IOException e) {
               throw new UncheckedIOException(e);
           }
       };
   }

注意读写文件要考虑设置缓冲区

从上述命名上可以看出,使用了BufferedReader 进行字符流读取时,用到了缓冲。这里缓冲 Buffer 的意思是,使用一块内存区域作为直接操作的中转。
比如,读取文件操作就是一次性读取一大块数据(比如 8KB)到缓冲区,后续的读取可以直接从缓冲区返回数据,而不是每次都直接对应文件 IO。写操作也是类似。如果每次写几
十字节到文件都对应一次 IO 操作,那么写一个几百兆的大文件可能就需要千万次的 IO 操作,耗时会非常久。
就比如之前说的Files.lines()分批读取数据,读取的数据先放在一个独立buffer中,buffer相当于个一个中转站。和直接读数据加载到内存的区别是,放在buffer中的话有更多的好处。就比如我现在既需要对这部分数据读取再进行其他处理,或者将这部分数据保存在其他文件,这只是举个例子啊。这个时候,我只需要读取一次放入buffer, 后续对数据的其他操作都直接从buffer中拿就好了。

  private static void bufferOperationWith100Buffer() throws IOException {
        try (FileInputStream fileInputStream = new FileInputStream("src.txt");
             FileOutputStream fileOutputStream = new FileOutputStream("dest.txt")) {
            byte[] buffer = new byte[100];
            int len = 0;
            while ((len = fileInputStream.read(buffer)) != -1) {
                fileOutputStream.write(buffer, 0, len);
            }
        }
    }

上述代码我们使用了一个byte[] 缓冲区,极大提高了数据读取性能。建议进行文件 IO 处理的时候,使用合适的缓冲区!
你可能会说,实现文件读写还要自己 new一个缓冲区出来,太麻烦了,不是有一个 BufferedInputStream 和 BufferedOutputStream 可以实现输入输出流的缓冲处理吗?
是的,它们在内部实现了一个默认 8KB 大小的缓冲区。但是,在使用 BufferedInputStream 和 BufferedOutputStream 时,它们实现了内部缓冲进行逐字节的操作。
接下来,我写一段代码比较下使用下面三种方式读写一个字节的性能:

  1. 直接使用 BufferedInputStream 和 BufferedOutputStream;
  2. 额外使用一个 8KB 缓冲,使用 BufferedInputStream 和 BufferedOutputStream;
  3. 直接使用 FileInputStream 和 FileOutputStream,再使用一个 8KB 的缓冲。
//使用BufferedInputStream和BufferedOutputStream
        private static void bufferedStreamByteOperation() throws IOException {
            try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("demo.txt"));
                    BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("dest.txt"))) {
                int i;
                while ((i = bufferedInputStream.read()) != -1) {
                    bufferedOutputStream.write(i);
                }
            }
        }
        //额外使用一个8KB缓冲,再使用BufferedInputStream和BufferedOutputStream
        private static void bufferedStreamBufferOperation() throws IOException {
            try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("demo.txt"));
                    BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("dest.txt"))) {
                 byte[] buffer = new byte[8192]; // 8KB
                 int len = 0;
                 while ((len = bufferedInputStream.read(buffer)) != -1) {
                    bufferedOutputStream.write(buffer, 0, len);
                 }
            }
        }
        //直接使用FileInputStream和FileOutputStream,再使用一个8KB的缓冲
        private static void largerBufferOperation() throws IOException {
            try (FileInputStream fileInputStream = new FileInputStream("src.txt");
                 FileOutputStream fileOutputStream = new FileOutputStream("dest.txt")) {
                byte[] buffer = new byte[8192];
                int len = 0;
                while ((len = fileInputStream.read(buffer)) != -1) {
                    fileOutputStream.write(buffer, 0, len);
                }
            }
        }

性能:
--------------------------------------------
ns
%
Task name--------------------------------------------
1424649223 086% bufferedStreamByteOperation
117807808 007% bufferedStreamBufferOperation
112153174 007% largerBufferOperation

可以看到,第一种方式虽然使用了缓冲流,但逐字节的操作因为方法调用次数实在太多还是慢;后面两种方式的性能差不多。虽然第三种方式没有使用缓冲流,但使用了 8KB 大小的缓冲区,和缓冲流默认的缓冲区大小相同。

BufferedInputStream 和 BufferedOutputStream 的意义

在实际代码中每次需要读取的字节数很可能不是固定的,有的时候读取几个字节,有的时候读取几百字节,这个时候
有一个固定大小较大的缓冲,也就是使用 BufferedInputStream 和 BufferedOutputStream 做为后备的稳定的二次缓冲,就非常有意义了。
最后我要补充说明的是,对于类似的文件复制操作,如果希望有更高性能,可以使用
FileChannel 的 transfreTo 方法进行流的复制。在一些操作系统(比如高版本的 Linux 和 UNIX)上可以实现 DMA(直接内存访问),也就是数据从磁盘经过总线直接发送到目标文件,无需经过内存和 CPU 进行数据中转:

private static void fileChannelOperation() throws IOException {
 FileChannel in = FileChannel.open(Paths.get("src.txt"), StandardOpenOption
 FileChannel out = FileChannel.open(Paths.get("dest.txt"), CREATE, WRITE);
 in.transferTo(0, in.size(), out);
 }

总结

分享了文件读写操作中最重要的几个方面。
第一,如果需要读写字符流,那么需要确保文件中字符的字符集和字符流的字符集是一致的,否则可能产生乱码。
第二,使用 Files 类的一些流式处理操作,注意使用 try-with-resources 包装 Stream,确保底层文件资源可以释放,避免产生 too many open files 的问题。
第三,进行文件字节流操作的时候,一般情况下不考虑进行逐字节操作,使用缓冲区进行批量读写减少 IO 次数,性能会好很多。一般可以考虑直接使用缓冲输入输出流BufferedXXXStream,追求极限性能的话可以考虑使用 FileChannel 进行流转发。
最后我要强调的是,文件操作因为涉及操作系统和文件系统的实现,JDK 并不能确保所有 IO API 在所有平台的逻辑一致性,代码迁移到新的操作系统或文件系统时,要重新进行功
能测试和性能测试。

标签:文件,读取,读写,try,IO,使用,new,txt
From: https://www.cnblogs.com/xyuanzi/p/18428907

相关文章

  • 【unity进阶知识1】最详细的单例模式的设计和应用,继承和不继承MonoBehaviour的单例模
    文章目录前言一、不使用单例二、普通单例模式1、单例模式介绍实现步骤:单例模式分为饿汉式和懒汉式两种。2、不继承MonoBehaviour的单例模式2.1、基本实现2.2、防止外部实例化对象2.3、最终代码3、继承MonoBehaviour的单例模式3.1、基本实现3.2、自动创建和挂载单例脚本......
  • EtherCAT(以太网控制自动化技术)协议以其高带宽、低延迟特性,在工业自动化领域占据重要地
    一、MR30分布式IO模块概述EtherCAT(以太网控制自动化技术)协议以其高带宽、低延迟特性,在工业自动化领域占据重要地位。明达技术自主研发的MR30分布式IO模块作为EtherCAT协议的杰出应用,集成了多种输入输出功能,通过EtherCAT总线实现与主站的高效通信与控制,为纸巾包装行业带来革新。二、......
  • The 2024 ICPC Asia EC Regionals Online Contest (II)
    A-GamblingonChoosingRegionals题意\(k\)场比赛,每场比赛每个大学至多\(c_i\)个队;总\(n\)个队伍,每队有分数与所属大学两个属性,每只队伍至多参加\(2\)场比赛。求各个队在最坏情况下的最优排名。思路最坏情况就是你打哪场,强队都去哪场,就选\(c_i\)小的场次,能让排名更靠......
  • 日新月异 PyTorch - pytorch 基础: 通过卷积神经网络(Convolutional Neural Networks,
    源码https://github.com/webabcd/PytorchDemo作者webabcd日新月异PyTorch-pytorch基础:通过卷积神经网络(ConvolutionalNeuralNetworks,CNN)做图片分类-通过ResNet50做图片分类的学习(对cifar10数据集做训练和测试),保存训练后的模型,加载训练后的模型并评估指定的......
  • 2024年中国生成式AI行业最佳应用实践|附100页PDF文件下载
    前言8月28日,由弗若斯特沙利文(Frost&Sullivan,简称“沙利文”)主办的第十八届中国增长、科创与领导力峰会暨第三届新投资大会上,沙利文携手头豹研究院共同发布了《2024年中国生成式AI行业最佳应用实践》报告,并揭晓了多项实践方案大奖。其中,商汤科技与海通证券凭借双方联合打造的金融......
  • 【DL基础】torchvision数据集操作
     示例来源:PyTorch深度学习实战(geekbang.org)1、图像裁剪torchvision.transforms提供了多种剪裁方法,例如中心剪裁、随机剪裁、四角和中心剪裁等。我们依次来看下它们的定义。先说中心剪裁,顾名思义,在中心裁剪指定的PILImage或Tensor,其定义如下:torchvision.transforms......
  • 详解Diffusion扩散模型:理论、架构与实现
    本文深入探讨了Diffusion扩散模型的概念、架构设计与算法实现,详细解析了模型的前向与逆向过程、编码器与解码器的设计、网络结构与训练过程,结合PyTorch代码示例,提供全面的技术指导。关注TechLead,复旦AI博士,分享AI领域全维度知识与研究。拥有10+年AI领域研究经验、复旦机器人智......
  • UIOTOS示例:自定义弹窗输出表单数据 | 前端低代码 前端零代码 web组态 无代码 amis gov
    目标对话框作为容器组件,可以隐藏掉默认的窗体头和脚,完全由内嵌页自定义,参见对话框自定义外观。并且也能获取弹窗纯表单数据,如下所示: 步骤内嵌页1.新建略。2.拖放组件拖放三个输入框,标识分别施志伟id、name、phone;两个按钮标识分别设置为cancel和ok 主页面1.新......
  • cnblogs的GitHub同步markdown文件的blog如何识别文章的唯一性(身份ID如何判定)
    本篇blog是写在GitHub的对应的仓库中的。cnblogs会给终身用户提供一个把GitHub仓库中的markdown文件同步到cnblogs上的一个服务,本文就是使用这个服务同步到个人blog地址的:https://cnblogs.com/xyz问题1:何时触发blogs的同步?当仓库中的markdown文件有更新时,cnblogs会自动同......
  • VMWare安装Ubuntu之后与Windows系统共享文件夹的设置步骤
    1.首先在Windows系统中新建一个需要共享的文件夹,并设置文件夹的共享属性,如下图: 2.VMWare软件开启【共享文件夹】功能,如图所示3.进入Ubuntu系统,查看是否存在/mnt/hgfs目录,若是没有,先要以root权限建立该目录sudomkdir/mnt/hgfs4.挂载目录sudovmhgfs-fuse.host:......