
教程技术博客文档【免费下载链接】YCBlogs技术博客笔记大汇总包括Java基础线程并发数据结构Android技术博客等等常用设计模式常见的算法网络协议知识点部分flutter笔记还包括平时开发中遇到的bug汇总当然也在工作之余收集了大量的面试题长期更新维护并且修正持续完善……开源的文件是markdown格式的转载请注明出处谢谢项目地址https://gitcode.com/gh_mirrors/yc/YCBlogs点击查看免费下载本篇技术指南以 YCBlogs 仓库中 java/03.IO流知识/07.IO字符流案例.md 为骨架系统讲解 Java 字符流中最核心的两个转换流——OutputStreamWriter字符流通向字节流的桥梁与InputStreamReader字节流通向字符流的桥梁的使用方式并完整展开复制文本文件 → 复制单级文件夹 → 按后缀筛选复制并改名三个层层递进的实战案例。读完本文你将掌握字符流全部 5 种写数据方式与 2 种读数据方式、编码参数的指定方法以及如何借助FilenameFilter与renameTo完成批量文件筛选与重命名可直接应用于日常文件处理代码。一、为什么需要字符流字节流操作中文的痛点在进入案例之前先明确字符流出现的原因。按 java/03.IO流知识/02.IO流字符与字节.md 的梳理字符流出现的直接原因就是字节流操作中文不是特别方便。读写单位不同字节流以字节8bit为单位字符流以字符为单位根据码表映射字符一次可能读多个字节。处理对象不同字节流能处理所有类型的数据如图片、音频等而字符流只能处理字符类型的数据比如文本内容。字符流的本质字符流 字节流 编码表。因为数据编码的不同才有了对字符进行高效操作的流对象其本质就是在基于字节流读取时去查询指定的码表。Java 采用 Unicode 编码一个中文字符或英文字符的 Unicode 编码都占 2 个字节但若采用其他编码方式一个字符占用的字节数则各不相同。常见编码下字符与字节的对应关系如下表编码方式英文字母汉字备注GB2312 / GBK1 字节2 字节早期中文系统默认编码UTF-81 字节3~4 字节现代跨平台主流编码UTF-162 字节3~4 字节Unicode 扩展区汉字需 4 字节UTF-324 字节4 字节任何字符均占 4 字节这也是为什么用错误编码读取文件会出现乱码——读写双方码表不一致需要先转换再按 Unicode 进行映射。相关字节/字符概念、getBytes()与编码细节可参见 java/03.IO流知识/02.IO流字符与字节.md。由此引出一个选择流的基本原则来自同一系列文档只要是处理纯文本数据就优先考虑使用字符流除此之外都使用字节流。本文的主角——两个转换流正是字符流与字节流之间沟通的桥梁。二、字符输出流 OutputStreamWriter字符流通向字节流的桥梁OutputStreamWriter是字符输出流Writer的一个实现它的特点是将字符数据按指定编码表转换成字节后写到底层字节流因此被形象地称为字符流通向字节流的桥梁。2.1 构造方式与编码参数原文档给出如下示例/** * 字符输出流: OutputStreamWriter (转换输出流) 字符流通向字节流的桥梁 * 字符输入流: InputStreamReader (转换输入流) 字节流通向字符流的桥梁 */ public class OutputStreamWriterDemo { public static void main(String[] args) throws IOException { // 创建: OutputStreamWriter // 方式一先创建字节流对象再包装成转换流 // FileOutputStream fos new FileOutputStream(a.txt) ; // OutputStreamWriter osw new OutputStreamWriter(fos) ; // 方式二直接嵌套创建使用平台默认编码表 // OutputStreamWriter osw new OutputStreamWriter(new FileOutputStream(a.txt)) ; // 方式三推荐public OutputStreamWriter(OutputStream out, String charsetName) 使用指定的编码表 OutputStreamWriter osw new OutputStreamWriter(new FileOutputStream(a.txt) , UTF-8) ; // 调用方法写数据 osw.write(中国) ; // 释放资源 osw.close() ; } }要点说明构造参数OutputStreamWriter(OutputStream out)使用平台默认编码OutputStreamWriter(OutputStream out, String charsetName)使用指定编码表。第二个构造方法支持UTF-8、GBK、UTF-16等任意 JVM 支持的字符集名称。编码的重要性如果写入时指定UTF-8而读取方用GBK解码就会出现乱码。读写双方必须使用一致的编码表。默认编码的坑原文档注释中提到默认的字符集就是 GBK这是作者当时 Windows 环境的默认值。事实上OutputStreamWriter不指定编码时使用 JVM 默认字符集其值由操作系统语言环境与 JVM 配置决定现代 JDK 默认多为 UTF-8。因此生产代码务必显式指定 charsetName避免跨平台行为不一致。资源释放close()不仅会关闭流还会先 flush 缓冲区的数据而 IO 资源不属于内存资源垃圾回收机制无法回收必须显式关闭详见 java/03.IO流知识/05.IO流体系图.md 中对close()的强调。2.2 底层实现原理从 JDK 源码实现看OutputStreamWriter内部持有一个StreamEncoder对象写入的字符会先经过编码器按指定 charset 编码为字节序列再交给底层的FileOutputStream等字节流写出。这也是转换流名字的由来它并没有自己的物理存储能力一切读写最终都落在被包装的字节流上属于典型的**处理流包装流**角色符合 java/03.IO流知识/01.IO流基础介绍.md 中关于处理流是对已存在的流进行连接或封装的描述。三、字符输入流 InputStreamReader字节流通向字符流的桥梁与OutputStreamWriter对应InputStreamReader是字符输入流Reader的实现负责从底层字节流读取字节再按指定编码表解码成字符被称为字节流通向字符流的桥梁。3.1 构造方式与读取示例public class InputStreamReaderDemo { public static void main(String[] args) throws IOException { // 创建对象InputStreamReader的对象 // 方式一public InputStreamReader(InputStream in) 使用平台默认的编码表 // FileInputStream fis new FileInputStream(a.txt) ; // InputStreamReader isr new InputStreamReader(fis) ; // 方式二直接嵌套创建 // InputStreamReader isr new InputStreamReader(new FileInputStream(a.txt)) ; // 默认字符集随平台而定 // 方式三推荐public InputStreamReader(InputStream in, String charsetName) 使用指定的编码表 InputStreamReader isr new InputStreamReader(new FileInputStream(a.txt) , utf-8) ; // 读取数据一次读取一个字符 int ch 0 ; while((ch isr.read()) ! -1){ System.out.print((char)ch); } // 释放资源 isr.close() ; } }要点说明read() 的返回值语义int read()每次读取一个字符以整数形式返回该字符的码点值当读取到文件末尾时返回-1这正是while循环终止的条件。之所以返回值用int而非char是为了用-1表达流已读完这一特殊状态。编码必须与写入方一致写入用UTF-8读取也必须用UTF-8否则解码出的中文会出现乱码。大小写兼容charsetName 参数大小写不敏感utf-8与UTF-8等价。3.2 底层实现原理从 JDK 源码实现看InputStreamReader内部持有一个StreamDecoder对象它从底层FileInputStream等字节流读取字节序列再按指定 charset 解码为字符。这意味着read()每返回一个字符底层可能对应读取了多个字节如 UTF-8 下的一个汉字对应 3 个字节这正是字符流相对字节流在处理中文时的便利所在。四、字符流的读写方式5 种写数据 2 种读数据原文档系统总结了字符流Writer/Reader的方法清单这是使用字符流的操作手册字符流的 5 种写数据方式来自Writer抽象基类public void write(int c) // 写入单个字符 public void write(char[] cbuf) // 写入一个字符数组 public void write(char[] cbuf,int off,int len) // 写入字符数组的一部分off 起始位置len 长度 public void write(String str) // 写入一个字符串 public void write(String str,int off,int len) // 写入字符串的一部分字符流的 2 种读数据方式来自Reader抽象基类public int read() // 读取单个字符返回其码点值读到末尾返回 -1 public int read(char[] cbuf) // 读取字符到数组中返回实际读取的字符个数末尾返回 -1对照 java/03.IO流知识/05.IO流体系图.md 中对四大抽象基类方法的梳理Writer比OutputStream多出的两个方法正是write(String)与write(String, int, int)——字符流可以直接写出字符串这是字节流所不具备的便利同时Reader与InputStream的读取方法一一对应只是操作的数据单元从字节变成了字符。五、实战一字符流复制文本文件复制文本文件是字符流最典型的应用场景。原文档演示了两种读取粒度核心代码如下public class CopyFileDemo { public static void main(String[] args) throws IOException { // 创建转换输入流对象 InputStreamReader isr new InputStreamReader(new FileInputStream(OutputStreamWriterDemo.java)) ; // 创建转换输出流对象 OutputStreamWriter osw new OutputStreamWriter(new FileOutputStream(copyFile.java)) ; // 复制文件 // 方式一一次读取一个字符复制逐字符读写效率低 // int ch 0 ; // while((ch isr.read()) ! -1){ // osw.write(ch) ; // } // 方式二推荐一次读取一个字符数组复制文件 char[] chs new char[1024] ; int len 0 ; while((len isr.read(chs)) ! -1){ osw.write(chs, 0, len) ; } // 释放资源 osw.close() ; isr.close() ; } }两个关键设计点缓冲区char[] chs new char[1024]一次读取 1024 个字符存入数组read(chs)返回实际读取到的有效字符个数len随后write(chs, 0, len)只写出有效部分。最后一个缓冲区可能不满 1024若直接write(chs)会把上次残留的旧数据一并写出因此必须使用write(chs, 0, len)这种偏移长度写法。效率差异逐字符读写方式一每次读写都要经过一次编码/解码与系统调用开销巨大按字符数组批量读写则大幅减少调用次数。同系列 java/03.IO流知识/06.IO字节流案例.md 中对字节流复制 avi 的实测参考数据很能说明问题基本流一次读一个字节约 88670 毫秒一次读一个字节数组约 130 毫秒——两者相差数百倍。字符流的单字符与字符数组模式同理批量读写始终是首选。六、实战二复制单级文件夹IOUtils 工具类设计复制整个文件夹本质是先建目录再逐文件复制。原文档将其封装为一个可复用的IOUtils工具类public static void main(String[] args) throws IOException { /** * 需求: 把C:\\course这个文件夹复制到D:\\course盘下 * 分析: * 1: 把C:\\course这个目录封装成一个File对象 * 2: 把D:\\course这个目录封装成一个File对象 * 3: 判断D:\\course是否存在,如果存在就创建一个文件夹 * 4: 获取C:\\course这个目录下所有的文件对应的File数组 * 5: 遍历数组,获取元素进行复制 */ // 把C:\\course这个目录封装成一个File对象 File srcFolder new File(C:\\course) ; // 把D:\\course这个目录封装成一个File对象 File destFolder new File(D:\\course) ; // 判断D:\\course是否存在,如果存在就创建一个文件夹 if(!destFolder.exists()){ destFolder.mkdir() ; } // 复制文件夹 IOUtils.copyFolder(srcFolder, destFolder, null) ; } public class IOUtils { public static void copyFolder(File srcFolder , File destFolder , FilenameFilter filenameFilter) throws IOException { File[] files null ; if(filenameFilter null) { files srcFolder.listFiles() ; }else { files srcFolder.listFiles(filenameFilter) ; } // 遍历 for(File f : files) { // 创建目标文件 String destFileName f.getName() ; File destFile new File(destFolder , destFileName) ; // 复制文件 copyFile(f , destFile) ; } } public static void copyFile(File srcFile , File destFile) throws IOException { // 创建流对象用高效缓冲流包装节点流 BufferedInputStream bis new BufferedInputStream(new FileInputStream(srcFile)) ; BufferedOutputStream bos new BufferedOutputStream(new FileOutputStream(destFile)) ; // 一次读取一个字节数组复制文件 byte[] bytes new byte[1024] ; int len 0 ; while((len bis.read(bytes)) ! -1){ bos.write(bytes, 0, len) ; } // 释放资源 bos.close() ; bis.close() ; } }该工具类的设计值得逐层拆解mkdir()与exists()配合目标目录不存在时先创建避免listFiles()后写出错。copyFolder(File, File, FilenameFilter)的第三个参数传null时复制目录下全部文件传入FilenameFilter时只复制满足条件的文件——这正是下一节按后缀筛选复制的复用点体现了工具类的扩展性设计。节点流 处理流的组合copyFile内部用的是BufferedInputStream/BufferedOutputStream字节高效缓冲流包装FileInputStream/FileOutputStream。按 java/03.IO流知识/01.IO流基础介绍.md 的定义直接连接数据源的FileInputStream是节点流在它之上再套接一层的BufferedInputStream是处理流处理流不仅消除不同节点流的实现差异还提供了更快的读写能力——字节流使用缓冲后的效率提升在该系列文档中有实测数据高效流一次读字节数组约 50 毫秒远快于基本流。注意此处为何用字节流复制文件夹时文件类型未知可能是图片、音频等非文本文件此时必须退回到字节流。这再次印证了第一节的原则只有纯文本才用字符流其他一律用字节流。七、实战三复制指定后缀名的文件并修改名称这是原文档的收尾进阶案例把C:\demo下所有.java文件复制到D:\demo再把后缀名统一改为.jad。核心在于FilenameFilter筛选与renameTo重命名public static void main(String[] args) throws IOException { /** * 把C:\\demo这个目录下所有的以.java结尾的文件复制到D:\\demo中,然后将这个文件的后缀名更改为.jad */ // 1: 把C:\\demo这个目录封装成一个File对象 File srcFolder new File(C:\\demo) ; // 2: 把D:\\demo这么目录封装成一个File对象 File destFolder new File(D:\\demo) ; // 3: 判断D:\\demo这个路径是否存在 if(!destFolder.exists()) { destFolder.mkdir() ; } // 调用方法传入匿名 FilenameFilter只复制 .java 文件 IOUtils.copyFolder(srcFolder, destFolder, new FilenameFilter() { Override public boolean accept(File dir, String name) { return new File(dir , name).isFile() name.endsWith(.java) ; } }) ; System.out.println(-----------------------------------------------------); // 获取destFolder下所有的文件对应的File数组 File[] files destFolder.listFiles() ; for(File f : files) { // 创建目标文件名称把 .java 后缀替换成 .jad String destFileName f.getName().replace(.java, .jad) ; // 创建目标文件 File destFile new File(destFolder , destFileName) ; // 调用 renameTo 完成重命名 f.renameTo(destFile) ; } } public class IOUtils { public static void copyFolder(String srcPahtName , String destPathName , FilenameFilter filenameFilter) throws IOException { File srcFolder new File(srcPahtName) ; File destFolder new File(destPathName) ; if(!destFolder.exists()) { destFolder.mkdir() ; } copyFolder(srcFolder , destFolder , filenameFilter) ; } public static void copyFolder(File srcFolder , File destFolder , FilenameFilter filenameFilter) throws IOException { File[] files null ; if(filenameFilter null) { files srcFolder.listFiles() ; }else { files srcFolder.listFiles(filenameFilter) ; } // 遍历 for(File f : files) { // 创建目标文件 String destFileName f.getName() ; File destFile new File(destFolder , destFileName) ; // 复制文件 copyFile(f , destFile) ; } } public static void copyFile(File srcFile , File destFile) throws IOException { // 创建流对象 BufferedInputStream bis new BufferedInputStream(new FileInputStream(srcFile)) ; BufferedOutputStream bos new BufferedOutputStream(new FileOutputStream(destFile)) ; // 一次读取一个字节数组复制文件 byte[] bytes new byte[1024] ; int len 0 ; while((len bis.read(bytes)) ! -1){ bos.write(bytes, 0, len) ; } // 释放资源 bos.close() ; bis.close() ; } }本案例引入两个新知识点FilenameFilter文件过滤器listFiles(FilenameFilter)会只返回accept(File dir, String name)返回true的文件。这里的匿名内部类判断条件为new File(dir, name).isFile() name.endsWith(.java)——既排除目录又只保留.java后缀。因为listFiles()可能返回子目录若不加isFile()判断目录也会被复制。这是筛选时最容易遗漏的点。renameTo(File dest)重命名通过String.replace(.java, .jad)生成新文件名再构造目标File对象调用renameTo完成改名。需要注意renameTo在 Java 中依赖底层文件系统实现不同平台行为可能不同且目标文件已存在时可能失败对批量重命名的健壮性要求更高的场景可考虑Files.move等 NIO 方案但作为学习案例renameTo足以演示复制→改名的完整流程。此外本节的IOUtils还多提供一个字符串参数重载copyFolder(String srcPathName, String destPathName, FilenameFilter)内部先封装成File再调用File版本简化了调用方代码——这也是工具类常见的便捷重载设计模式。八、总结字符流实战要点清单综合原文档 6 个案例可以提炼出以下可直接沿用的实战要点场景推荐做法依据读写纯文本明确是字符数据使用字符流转换流或FileReader/FileWriter、缓冲字符流java/03.IO流知识/02.IO流字符与字节.md读写未知类型文件图片、音视频等使用字节流必要时用缓冲流包装本文第六、七节IOUtils.copyFile涉及中文且要保证不乱码读写两端显式指定同一编码如UTF-8本文第二、三节构造方法复制文件优先一次读写一个数组read(char[])/write(char[],0,len)避免逐字符本文第五节批量筛选文件借助listFiles(FilenameFilter)本文第七节复制整个目录先mkdir()建目标目录再逐文件复制本文第六节流使用完毕必须显式close()IO 资源不受 GC 管理java/03.IO流知识/05.IO流体系图.md进一步延伸字符流家族中还有更便捷的FileReader/FileWriter内部即基于转换流封装以及带特殊功能readLine()按行读取、newLine()换行的BufferedReader/BufferedWriter相关内容详见 java/03.IO流知识/09.Buffer高效字符流.md字节流侧的基础案例与四种复制效率对比见 java/03.IO流知识/06.IO字节流案例.mdIO 流的整体分类、节点流/处理流与装饰器设计模式的关系可回顾 java/03.IO流知识/01.IO流基础介绍.md 与 java/03.IO流知识/05.IO流体系图.md。掌握转换流定位 编码一致性 批量读写 工具类封装这四板斧Java 字符流的日常使用即可游刃有余。赞分享教程技术博客文档【免费下载链接】YCBlogs技术博客笔记大汇总包括Java基础线程并发数据结构Android技术博客等等常用设计模式常见的算法网络协议知识点部分flutter笔记还包括平时开发中遇到的bug汇总当然也在工作之余收集了大量的面试题长期更新维护并且修正持续完善……开源的文件是markdown格式的转载请注明出处谢谢项目地址https://gitcode.com/gh_mirrors/yc/YCBlogs点击查看免费下载相关推荐YCBlogs Java IO 流字符与字节深度解析编码表、解码机制与流选择实战YCBlogs Java IO 流字符与字节深度解析编码表、解码机制与流选择实战 本篇技术指南以 YCBlogs 开源仓库中 02.IO流字符与字节.md h教程技术博客文档Operit GitHub OAuth Broker 云端交换协议PKCE 短期认证事务与一次性领取凭据实战解析Operit GitHub OAuth Broker 云端交换协议PKCE 短期认证事务与一次性领取凭据实战解析 导读 本文围绕 Operit 项目中 doc教程技术博客文档告别陈旧界面用Sun Valley ttk主题三行代码重塑Tkinter应用视觉体验告别陈旧界面用Sun Valley ttk主题三行代码重塑Tkinter应用视觉体验 你是否曾为Tkinter应用的复古界面感到尴尬在2024年的今天UI组件上一篇3分钟快速上手VideoDownloadHelper终极视频下载指南下一篇Faster-Whisper-GUI终极指南如何用免费AI工具将语音秒变文字创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考