Java 业务开发常见错误 100 例
朱晔
贝壳金服资深架构师
52944 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 48 讲
代码篇 (23讲)
Java 业务开发常见错误 100 例
15
15
1.0x
00:00/00:00
登录|注册

14 | 文件IO:实现高效正确的文件读写并非易事

原子性问题
需要注意资源释放问题
使用FileChannel进行流转发
使用缓冲区进行批量读写减少IO次数
需要使用try-with-resources进行资源释放
使用InputStreamReader和FileInputStream
使用指定字符集进行文件读取
字符编码方式
文件读取乱码问题
经验分享
资源释放问题
Java的File类和Files类提供的文件操作
其他文件操作方法
BufferedInputStream和BufferedOutputStream
Files.lines方法
解决方案
问题分析
文件操作中的坑
Files类中其他返回Stream包装对象的方法
文件复制、重命名、删除操作的原子性
文件句柄释放
字符编码一致性
思考与讨论
高效、正确的文件操作
文件IO操作
文件IO操作知识关系脑图

该思维导图由 AI 生成,仅供参考

你好,我是朱晔。今天,我们来聊聊如何实现高效、正确的文件操作。
随着数据库系统的成熟和普及,需要直接做文件 IO 操作的需求越来越少,这就导致我们对相关 API 不够熟悉,以至于遇到类似文件导出、三方文件对账等需求时,只能临时抱佛脚,随意搜索一些代码完成需求,出现性能问题或者 Bug 后不知从何处入手。
今天这篇文章,我就会从字符编码、缓冲区和文件句柄释放这 3 个常见问题出发,和你分享如何解决与文件操作相关的性能问题或者 Bug。如果你对文件操作相关的 API 不够熟悉,可以查看Oracle 官网的介绍

文件读写需要确保字符编码一致

有一个项目需要读取三方的对账文件定时对账,原先一直是单机处理的,没什么问题。后来为了提升性能,使用双节点同时处理对账,每一个节点处理部分对账数据,但新增的节点在处理文件中中文的时候总是读取到乱码。
程序代码都是一致的,为什么老节点就不会有问题呢?我们知道,这很可能是写代码时没有注意编码问题导致的。接下来,我们就分析下这个问题吧。
为模拟这个场景,我们使用 GBK 编码把“你好 hi”写入一个名为 hello.txt 的文本文件,然后直接以字节数组形式读取文件内容,转换为十六进制字符串输出到日志中:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

文件IO操作中的常见问题和解决方法是本文的重点内容。首先,文章强调了字符编码一致性的重要性,并提出了使用InputStreamReader和FileInputStream指定字符集来解决乱码问题。其次,介绍了使用Files类的readAllLines方法可能导致OOM问题,而使用File类的lines方法可以实现按需的流式读取。文章还通过实例分析了使用缓冲Buffer的重要性,并对比了不同使用方式的文件读写性能。此外,提到了静态方法的调用并不代表资源释放,需要注意使用try-with-resources方式来确保流的close方法可以释放资源。总之,本文为读者提供了有益的指导,帮助他们快速了解文件IO操作中的常见问题和解决方法,为实现高效、正确的文件读写提供了指导。文章还提到了一些Java的File类和Files类提供的文件复制、重命名、删除等操作的原子性问题,以及对文件操作的一些思考与讨论。文章内容丰富,涵盖了文件IO操作中的多个方面,对读者进行了全面的指导和启发。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 业务开发常见错误 100 例》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(24)

  • 最新
  • 精选
  • Darren
    今天算是打开了一片新的天地,因为日常的开发设计文件的不太多,竟然不知道有Files这样的牛逼操作,之前对于JDK相关的NIO关注的也不多,真的是打开了一闪窗。 先说下FileChannel 的 transfreTo 方法,这个方法出现在眼前很多次,因为之前看Kafka为什么吞吐量达的原因的时候,提到了2点:批处理思想和零拷贝; 批处理思想:就是对于Kafka内部很多地方来说,不是消息来了就发送,而是有攒一波发送一次,这样对于吞吐量有极大的提升,对于需要实时处理的情况,Kafka就不是很适合的原因; 零拷贝:Kafka快的另外一个原因是零拷贝,避免了内存态到内核态以及网络的拷贝,直接读取文件,发送到网络出去,零拷贝的含义不是没有拷贝,而是没有用户态到核心态的拷贝。 而在提到零拷贝的实现时,Java中说的就是FileChannel 的 transfreTo 方法。 然后回答下问题: 第一个问题: Files的相关方法文档描述: When not using the try-with-resources construct, then directory stream's close method should be invoked after iteration is completed so as to free any resources held for the open directory. 所以是需要手动关闭的。 第二个问题: 没有原子操作,因此是线程不安全的。个人理解,其实即使加上了原子操作,也是鸡肋,不实用的很,原因是:File 类和 Files的相关操作,其实都是调用操作系统的文件系统操作,这个文件除了JVM操作外,可能别的也在操作,因此还不如不保证,完全基于操作系统的文件系统去保证相关操作的正确性。

    作者回复: 👍🏻👍🏻👍🏻👍🏻

    2020-04-13
    10
    83
  • 👽
    这篇专栏内容不算多,但是感觉可扩展的地方很多。 我认为:读写字符流乱码,其实本质上在于环境不一致的问题。其实跟日志路径之类的问题思路一致。服务器保存日志,如果配置绝对路径,C盘下的log文件夹。等部署到服务器上就会出错。除了针对不同的环境使用不同的配置,还可以尝试使用相对路径,亦或者将路径以存数据库的方式持久化。其实本质上,跟编码格式的处理方式一样,尽量屏蔽不同环境之间的差异。 保证文件流释放,同样也可以延伸。不仅仅是文件流,任何涉及资源占用问题的时候,都需要考虑资源是否可以保证被释放。try-whit-resources来解决IO流,其实同样也应用于各种需要释放资源的场景。 关于缓冲区,个人理解,一个典型的应用就是数据库的分页查询。如果将所有数据一次查出,不但消耗资源,甚至有可能内存不够。如果一次只查一个,如果需要查询的是几十条数据,频繁进行数据库访问,性能也较差。所以,采取了折中的方案,分页查询,一次仅查出一部分数据。既不会内存溢出,也保证了响应速度。

    作者回复: 不错

    2020-04-13
    17
  • 梦倚栏杆
    第一个问题:都间接实现了autoCloseable接口,所以都可以使用try-with-resources进行释放。 第二个非原子性,没有锁,也没有异常后的回滚。需要调用方进行事务控制

    作者回复: 不错

    2020-04-12
    13
  • Geek_3b1096
    谢谢老师详细解说文件操作

    作者回复: 不客气,觉得好可以点赞转发

    2020-04-15
    5
  • 汝林外史
    BufferedInputStream的二级缓冲什么时候能用到呢?既然需要自己定义一个缓冲,比如2K,那么肯定也是控制一次读取2K,应该不会有读取超过2K的时候吧?

    作者回复: 你可能会疑惑了,既然这样使用 BufferedInputStream 和 BufferedOutputStream 有什么意义呢?其实,这里我是为了演示所以示例三使用了固定大小的缓冲区,但在实际代码中每次需要读取的字节数很可能不是固定的,有的时候读取几个字节,有的时候读取几百字节,这个时候有一个固定大小较大的缓冲,也就是使用 BufferedInputStream 和 BufferedOutputStream 做为后备的稳定的二次缓冲,就非常有意义了

    2020-04-13
    9
    5
  • Michael
    特别喜欢老师的这种工匠精神,对读者的每一个问题都精心回复

    作者回复: 感谢认可

    2020-04-19
    3
  • Monday
    gbk与utf8区别(本文总结所得): 1、gbk只适合中文编码方式,utf8全世界的编码方式 2、对于中文汉字,gbk使用2个字节,utf8使用3个字节;对于英文字母都是1个字节。这种变长编码方式,怎么区分汉字和英文呢?

    作者回复: 针对你的问题可以看一下http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html 其中『UTF-8 的编码规则很简单,只有二条』一节就明白了

    2020-04-19
    3
    3
  • pedro
    第二个问题,不是原子的,所以需要注意,如果 io 异常,可能会出现复制后的文件不完整,文件未删除成功等问题

    作者回复: 是,其实我在思考题中也会补充更多正文无法详细阐述的坑

    2020-04-11
    3
  • 珅珅君
    你好,关于BufferedInputStream有一点疑问,如果我构造BufferedInputStream的时候设置缓冲流的大小是1kb,这里缓冲区叫A,但是调用read(byte[])的时候额外用的是8kb缓冲,这里缓冲区叫B,那么读取文件的时候,这两种缓冲的大小的工作流程是什么样的。先B后A还是?

    作者回复: 先A后B(可能多次)

    2020-07-27
    2
  • Demon.Lee
    LongAdder longAdder = new LongAdder(); IntStream.rangeClosed(1, 1000000).forEach(i -> { try { try (Stream<String> lines = Files.lines(Paths.get("demo.txt"))) { lines.forEach(line -> longAdder.increment()); } } catch (IOException e) { e.printStackTrace(); } }); log.info("total : {}", longAdder.longValue()); ------------------------------------------------------ 我一开始还奇怪为啥要两个try,catch放里面不就行了么,想了一下才明白,是为了捕获里面释放资源的异常,相当于捕获finally中的异常。 两道题都去翻了源码,第一题我觉得也是需要主动释放,Path也算一种fd吧,不太确定;第二题没有看到锁什么的,不是原子性的,不过创建或删除文件,重复处理,操作系统层会报错,但写内容到文件中就需要注意了。

    作者回复: 直接放到catch里是可以的,这里我因为修改了wrong所以写成这样了。对于释放资源产生的异常,同样可以在catch中捕获,可以看一下try-with-resources语法糖会翻译成怎么样的代码就理解了。

    2020-04-11
    1
收起评论
显示
设置
留言
24
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部