尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

33. 【Java】I/O流(上):字节流与字符流

33. 【Java】I/O流(上):字节流与字符流 摘要本文深入讲解Java I/O流的核心概念与实战应用涵盖字节流InputStream/OutputStream和字符流Reader/Writer两大体系。通过具体代码示例详细解析文件读写、缓冲流优化、字符编码处理等关键技术帮助开发者掌握高效、安全的文件操作方式。文章还提供了字节流与字符流的选择指南、常见陷阱规避方法以及一个完整的文本文件复制工具实现为Java文件处理打下坚实基础。关键词Java I/O流, 字节流, 字符流, 文件读写, 缓冲流在之前的所有文章里我们所有的数据都存在于“内存”中——变量、数组、集合、对象。一旦程序结束这些数据就消失了就像一场梦醒来什么都记不住。但你写程序很多时候是为了处理“真实世界”的数据从硬盘读取一个文件、把用户输入保存到磁盘、从网络下载一张图片、上传一份日志……这些数据是持久化的它们不随程序的结束而消失。那么Java 是如何与外部世界“对话”的呢答案就是I/O 流Input/Output Stream。I/O 是 Input/Output 的缩写。你可以把流想象成一根“管道”数据从一端流进来输入或者从一端流出去输出。这根管道连接着你的程序和外部的数据源文件、网络、键盘、显示器等。今天我们先来认识 I/O 流的两大家族字节流和字符流。1. 什么是流—— 数据就像水一样流动在 Java 中“流”是一个抽象概念它代表了一个有序的、连续的数据序列。输入流Input Stream数据从外部“流进”程序。比如你从文件读取数据数据从文件流向程序。输出流Output Stream数据从程序“流出”到外部。比如你把数据写入文件数据从程序流向文件。流的本质是一连串的字节或字符。你可以把它想象成水管里的水——水从源头流向目的地你可以在中途“接”一部分水读取也可以往里面“加”水写入。Java 的 I/O 流分为两大类字节流Byte Stream以字节8 位为单位进行读写。适用于任何类型的数据文本、图片、音频、视频等。字符流Character Stream以字符16 位 Unicode为单位进行读写。专门用于处理文本数据支持字符编码转换。它们各自有四个抽象基类类别输入输出字节流InputStreamOutputStream字符流ReaderWriter所有具体的流类都直接或间接继承自这四个基类。2. 字节流 ——InputStream和OutputStream字节流是 I/O 的“通用语言”。无论什么数据文本、图片、视频在计算机中最终都是以字节序列存在的。所以字节流可以处理任何类型的文件。2.1InputStream—— 字节输入流的鼻祖InputStream是所有字节输入流的父类。它定义了读取字节的基本方法方法说明int read()读取一个字节0~255如果到达末尾返回-1int read(byte[] b)读取多个字节到数组返回实际读取的字节数int read(byte[] b, int off, int len)读取指定长度的字节到数组的指定位置void close()关闭流释放系统资源非常重要最常用的子类是FileInputStream用来从文件读取字节。// 示例用 FileInputStream 读取一个文件importjava.io.FileInputStream;importjava.io.IOException;publicclassFileInputStreamDemo{publicstaticvoidmain(String[]args){FileInputStreamfisnull;try{// 创建 FileInputStream指定文件路径fisnewFileInputStream(test.txt);intdata;// 一次读一个字节直到返回 -1文件末尾while((datafis.read())!-1){// 这里把字节转成字符显示仅适用于纯文本System.out.print((char)data);}}catch(IOExceptione){System.out.println(读取文件出错e.getMessage());}finally{// 确保关闭流if(fis!null){try{fis.close();}catch(IOExceptione){e.printStackTrace();}}}}}2.2OutputStream—— 字节输出流的鼻祖OutputStream是所有字节输出流的父类。它定义了写入字节的基本方法方法说明void write(int b)写入一个字节void write(byte[] b)写入整个字节数组void write(byte[] b, int off, int len)写入数组的一部分void flush()强制将缓冲区的数据写入底层设备void close()关闭流最常用的子类是FileOutputStream用来向文件写入字节。// 示例用 FileOutputStream 写入文件importjava.io.FileOutputStream;importjava.io.IOException;publicclassFileOutputStreamDemo{publicstaticvoidmain(String[]args){StringcontentHello, I/O World!\n这是第二行。;try(FileOutputStreamfosnewFileOutputStream(output.txt)){// 把字符串转成字节数组byte[]bytescontent.getBytes();fos.write(bytes);System.out.println(写入成功);}catch(IOExceptione){System.out.println(写入出错e.getMessage());}// 使用 try-with-resources自动关闭流}}2.3 缓冲字节流 ——BufferedInputStream/BufferedOutputStream直接使用FileInputStream或FileOutputStream时每次读写都会直接操作硬盘效率较低。为了提升性能Java 提供了缓冲流。缓冲流内部维护了一个缓冲区默认 8192 字节。读写数据时会先操作缓冲区满了之后才真正读写硬盘大幅减少了 I/O 次数。// 用缓冲流复制文件importjava.io.*;publicclassBufferedCopyDemo{publicstaticvoidmain(String[]args){try(BufferedInputStreambisnewBufferedInputStream(newFileInputStream(source.jpg));BufferedOutputStreambosnewBufferedOutputStream(newFileOutputStream(copy.jpg))){byte[]buffernewbyte[1024];// 自定义缓冲区intbytesRead;while((bytesReadbis.read(buffer))!-1){bos.write(buffer,0,bytesRead);}System.out.println(文件复制完成);}catch(IOExceptione){System.out.println(复制出错e.getMessage());}}}小贴士使用缓冲流时记得调用flush()确保所有数据都被写出close()也会自动调用flush()。3. 字符流 ——Reader和Writer字节流以字节为单位但人类阅读的是字符。如果你用字节流读取一个文本文件然后把它转成字符显示很可能出现乱码因为不同的编码GBK、UTF-8下一个字符可能占用 1~4 个字节。字符流就是为了解决这个问题而生的。它在字节流的基础上增加了字符编码转换的能力。你只需要指定字符集字符流就会自动完成字节 ↔ 字符的转换。3.1Reader—— 字符输入流的鼻祖Reader是所有字符输入流的父类。它的核心方法是read()但操作的是char字符而不是byte。最常用的子类是FileReader用来读取文本文件。// 用 FileReader 读取文本文件importjava.io.FileReader;importjava.io.IOException;publicclassFileReaderDemo{publicstaticvoidmain(String[]args){try(FileReaderfrnewFileReader(test.txt)){intch;while((chfr.read())!-1){System.out.print((char)ch);}}catch(IOExceptione){System.out.println(读取出错e.getMessage());}}}3.2Writer—— 字符输出流的鼻祖Writer是所有字符输出流的父类。它提供了写入字符的方法。最常用的子类是FileWriter用来写入文本文件。// 用 FileWriter 写入文本importjava.io.FileWriter;importjava.io.IOException;publicclassFileWriterDemo{publicstaticvoidmain(String[]args){try(FileWriterfwnewFileWriter(output.txt,true)){// true 表示追加模式fw.write(这是追加的内容\n);fw.write(另一行内容);System.out.println(写入成功);}catch(IOExceptione){System.out.println(写入出错e.getMessage());}}}FileWriter默认使用系统默认编码。如果你需要指定编码比如 UTF-8建议使用OutputStreamWriter或Files.newBufferedWriter()。3.3 缓冲字符流 ——BufferedReader/BufferedWriter和字节流一样字符流也有缓冲版本而且还有额外的好处BufferedReader提供了readLine()方法可以一次读取一整行文本非常方便。// 用 BufferedReader 逐行读取importjava.io.*;publicclassBufferedReaderDemo{publicstaticvoidmain(String[]args){try(BufferedReaderbrnewBufferedReader(newFileReader(test.txt))){Stringline;while((linebr.readLine())!null){System.out.println(行内容line);}}catch(IOExceptione){System.out.println(读取出错e.getMessage());}}}// 用 BufferedWriter 写入importjava.io.*;publicclassBufferedWriterDemo{publicstaticvoidmain(String[]args){try(BufferedWriterbwnewBufferedWriter(newFileWriter(output.txt))){bw.write(第一行);bw.newLine();// 写入换行符跨平台bw.write(第二行);bw.newLine();bw.write(第三行);}catch(IOExceptione){System.out.println(写入出错e.getMessage());}}}4. 字节流 vs 字符流 —— 怎么选场景推荐流类型读写任何类型的文件图片、音频、视频、二进制文件字节流FileInputStream/FileOutputStream读写纯文本文件并且关心字符编码字符流FileReader/FileWriter或InputStreamReader/OutputStreamWriter需要逐行读取文本BufferedReader支持readLine()需要高性能、大数据量使用缓冲流包装基础流重点不要用字节流去读取文本文件除非你手动处理编码。字符流会帮你自动处理编码让你的代码在不同操作系统上都能正常工作。5. 指定字符编码 —— 避免乱码在不同的操作系统和环境中默认字符编码可能不同Windows 是 GBKLinux 是 UTF-8。如果你用默认编码读写文件换一个环境就可能出现乱码。解决方案显式指定字符编码。使用InputStreamReader和OutputStreamWriter这两个“转换流”它们在字节流和字符流之间搭了一座桥允许你指定字符集。// 指定 UTF-8 编码读取文件importjava.io.*;publicclassEncodingDemo{publicstaticvoidmain(String[]args){try(BufferedReaderbrnewBufferedReader(newInputStreamReader(newFileInputStream(test.txt),UTF-8// 指定编码))){Stringline;while((linebr.readLine())!null){System.out.println(line);}}catch(IOExceptione){System.out.println(读取出错e.getMessage());}}}从 JDK 11 开始Files工具类提供了更简洁的方法importjava.io.IOException;importjava.nio.file.Files;importjava.nio.file.Paths;importjava.util.List;publicclassFilesDemo{publicstaticvoidmain(String[]args)throwsIOException{// 一次性读取所有行适用于小文件ListStringlinesFiles.readAllLines(Paths.get(test.txt));lines.forEach(System.out::println);// 一次性写入所有行ListStringcontentList.of(第一行,第二行,第三行);Files.write(Paths.get(output.txt),content);}}Files类默认使用 UTF-8大大简化了代码。我们下一篇会深入介绍 NIO 和Files工具类的更多用法。6. 常见陷阱与注意事项① 忘记关闭流不关闭流会导致资源泄漏文件句柄、网络连接等。始终用try-with-resources来自动关闭。② 在字符流中用了字节流或者反过来不要混淆——字符流处理文本字节流处理二进制。用错了会导致乱码或数据损坏。③ 缓冲流忘记flush()缓冲流在写入时会先把数据存在内存缓冲区如果没有flush()或close()数据可能没有真正写入硬盘。close()会自动调用flush()所以只要正常关闭就没问题。④ 路径问题文件路径可以是绝对路径C:/data/test.txt或相对路径data/test.txt。相对路径是相对于当前工作目录通常是项目的根目录。在 IDE 中运行路径可能会因为运行配置的不同而变化。建议使用绝对路径或把文件放在src目录下用类加载器读取。⑤ 编码不一致读取时指定的编码与文件实际的编码不一致会出现乱码。保持读写编码一致。7. 综合示例 —— 一个简单的文本文件复制工具我们用今天学的知识写一个可以复制文本文件的工具支持指定编码和追加模式importjava.io.*;importjava.nio.charset.Charset;publicclassTextFileUtil{/** * 复制文本文件 * param srcPath 源文件路径 * param destPath 目标文件路径 * param encoding 字符编码如 UTF-8 * param append 是否追加到目标文件末尾 */publicstaticvoidcopyTextFile(StringsrcPath,StringdestPath,Stringencoding,booleanappend){// 使用 try-with-resources 自动关闭流try(BufferedReaderreadernewBufferedReader(newInputStreamReader(newFileInputStream(srcPath),Charset.forName(encoding)));BufferedWriterwriternewBufferedWriter(newOutputStreamWriter(newFileOutputStream(destPath,append),Charset.forName(encoding)))){Stringline;intlineCount0;while((linereader.readLine())!null){writer.write(line);writer.newLine();lineCount;}System.out.printf(✅ 复制完成共 %d 行。%n,lineCount);}catch(FileNotFoundExceptione){System.out.println(❌ 文件不存在srcPath);}catch(IOExceptione){System.out.println(❌ IO 错误e.getMessage());}}publicstaticvoidmain(String[]args){// 测试复制文件使用 UTF-8覆盖目标copyTextFile(source.txt,destination.txt,UTF-8,false);}}8. 今天的总结今天我们进入了 I/O 流的世界字节流InputStream/OutputStream以字节为单位可处理任何类型的数据。字符流Reader/Writer以字符为单位专为文本设计支持编码转换。缓冲流BufferedInputStream/BufferedOutputStream/BufferedReader/BufferedWriter提升性能并提供了便捷方法如readLine()。转换流InputStreamReader/OutputStreamWriter在字节流和字符流之间搭桥支持指定编码。try-with-resources自动关闭资源防止资源泄漏。选择建议文本用字符流二进制用字节流指定编码避免乱码。今天的内容是 I/O 的基础。你学会了如何从文件读取和写入数据。在下一篇I/O 流下中我们将学习更现代的NIONew I/O——它提供了更高效的文件操作Path、Files工具类、文件遍历等是我们目前最推荐的现代 I/O 方式。动手试试写一个程序用FileInputStream和FileOutputStream复制一张图片二进制文件体会字节流可以处理任何文件。用BufferedReader和BufferedWriter把一段文本按行读取然后把每行前加上行号如“1: 内容”写入新文件。修改上面的copyTextFile方法增加统计字符数的功能并在复制完成后输出总字符数。挑战写一个FileSearch方法接收一个目录路径和一个关键字遍历目录下的所有.txt文件搜索包含该关键字的文件并打印文件名和匹配的行号提示可以用File类和递归。有了 I/O 流你的程序终于可以和外部世界“对话”了。下一篇文章我们会走进 NIO 的世界体验更现代、更强大的文件操作方式。我们下一篇见。 获取本系列示例代码请访问 GitCode。
返回列表