21_Java IO流体系详解
Java IO流体系详解
文章目录
- Java IO流体系详解
- 前言
- 一、IO流的分类
- 二、File类详解
- 三、字节流:InputStream与OutputStream
- 3.1 字节输入流 InputStream
- 3.2 字节输出流 OutputStream
- 3.3 文件拷贝示例
- 四、字符流:Reader与Writer
- 4.1 FileReader与FileWriter
- 4.2 字节流与字符流的桥接
- 五、try-with-resources 自动关闭资源
- 六、IO流使用的最佳实践
- 总结
- ✅ 亮点总结
- 适用场景
- 扩展方向
前言
在Java开发中,IO(Input/Output)流是处理数据输入输出的核心机制。无论是文件读写、网络通信还是内存数据处理,都离不开IO流的支持。Java的IO流体系设计精良,采用装饰器模式构建了一套灵活、可扩展的流处理框架。
Java IO流体系是初学者最容易产生"学完就忘"的知识点之一,原因在于种类太多、API繁杂。但实际上,如果你理解了它的设计模式——装饰器模式——整个体系就变得有条理了。就像乐高积木一样,基础流负责"连接数据源",功能流(缓冲、转换、编码)负责"增强能力",通过层层嵌套组合出任意你需要的功能。本文将从零开始,系统讲解Java IO流的分类、核心类及其使用方法,帮助你建立清晰的IO流心智模型。
一、IO流的分类
Java IO流可以从两个维度进行分类:
- 按数据流向分:输入流(InputStream/Reader)和输出流(OutputStream/Writer)
- 按处理单位分:字节流(以byte为单位)和字符流(以char为单位)
| 分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
|---|---|---|---|---|
| 抽象基类 | InputStream | OutputStream | Reader | Writer |
| 文件操作 | FileInputStream | FileOutputStream | FileReader | FileWriter |
| 缓冲操作 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
设计理念:字节流适合处理二进制数据(图片、视频、音频),字符流适合处理文本数据,内置了字符编码处理。
二、File类详解
File类是java.io包中唯一代表磁盘文件本身的对象,它既可以表示文件,也可以表示目录。需要注意的是,File类只用于表示文件(目录)的信息(名称、大小、路径等),不能用于文件内容的读写。
常见的混淆点:很多初学者误以为new File("test.txt")会创建文件。实际上,构造File对象只是在内存中创建一个代表路径的对象,并不会在磁盘上创建任何东西。磁盘上的文件创建需要通过createNewFile()方法显式完成。此外,File类既可以表示文件也可以表示目录,需要通过isDirectory()和isFile()方法来判断。
importjava.io.File;importjava.io.IOException;importjava.util.Date;publicclassFileDemo{publicstaticvoidmain(String[]args)throwsIOException{// 创建File对象(三种方式)Filefile1=newFile("D:/test/hello.txt");// 绝对路径Filefile2=newFile("D:/test","hello.txt");// 父目录 + 子路径Fileparent=newFile("D:/test");Filefile3=newFile(parent,"hello.txt");// 父File对象 + 子路径System.out.println("文件是否存在:"+file1.exists());System.out.println("文件名:"+file1.getName());System.out.println("父路径:"+file1.getParent());System.out.println("绝对路径:"+file1.getAbsolutePath());System.out.println("文件大小(字节):"+file1.length());System.out.println("最后修改时间:"+newDate(file1.lastModified()));// 创建文件FilenewFile=newFile("D:/test/newfile.txt");if(!newFile.exists()){booleancreated=newFile.createNewFile();System.out.println("文件创建"+(created?"成功":"失败"));}// 创建目录Filedir=newFile("D:/test/subdir");dir.mkdir();// 创建单级目录dir.mkdirs();// 创建多级目录(父目录不存在也会创建)// 遍历目录FiletestDir=newFile("D:/test");String[]fileNames=testDir.list();// 返回文件名数组File[]files=testDir.listFiles();// 返回File对象数组if(files!=null){for(Filef:files){Stringtype=f.isDirectory()?"[目录]":"[文件]";System.out.println(type+" "+f.getName());}}// 删除文件或空目录newFile.delete();}}File类的核心方法总结:
exists()— 判断文件或目录是否存在createNewFile()— 创建新文件mkdir()/mkdirs()— 创建目录delete()— 删除文件或空目录list()/listFiles()— 列出目录内容getName()/getPath()/getAbsolutePath()— 获取路径信息
三、字节流:InputStream与OutputStream
3.1 字节输入流 InputStream
InputStream是字节输入流的抽象基类,定义了读取字节数据的基本方法。作为整个字节输入体系的顶层抽象,它提供了三个层次的读取能力:单个字节读取、批量字节数组读取、以及指定偏移量的读取。实际开发中建议使用后两种,因为批量读取可以减少系统调用次数。
publicabstractclassInputStream{publicabstractintread()throwsIOException;// 读取单个字节publicintread(byte[]b)throwsIOException;// 读取到字节数组publicintread(byte[]b,intoff,intlen);// 读取指定长度publicvoidclose()throwsIOException;// 关闭流}开发中常见的困惑:read()返回int类型而不是byte类型,这是为什么?因为byte的取值范围是-128到127,而read()需要用-1来表示"已读到末尾"。如果返回byte,就没法区分"读到了值为255的字节"和"流结束了"。使用int(0到255+返回-1)就能完美解决这个"二义性"问题。
FileInputStream是InputStream的常用子类,用于从文件中读取字节数据:
importjava.io.FileInputStream;importjava.io.IOException;publicclassFileInputStreamDemo{publicstaticvoidmain(String[]args){FileInputStreamfis=null;try{// 创建文件输入流fis=newFileInputStream("D:/test/hello.txt");// 方式一:逐字节读取intdata;while((data=fis.read())!=-1){System.out.print((char)data);}// 方式二:批量读取到字节数组(效率更高)byte[]buffer=newbyte[1024];intlen;while((len=fis.read(buffer))!=-1){Stringcontent=newString(buffer,0,len);System.out.print(content);}}catch(IOExceptione){e.printStackTrace();}finally{// 必须在finally中关闭流if(fis!=null){try{fis.close();}catch(IOExceptione){e.printStackTrace();}}}}}3.2 字节输出流 OutputStream
OutputStream是字节输出流的抽象基类:
importjava.io.FileOutputStream;importjava.io.IOException;publicclassFileOutputStreamDemo{publicstaticvoidmain(String[]args){FileOutputStreamfos=null;try{// true表示追加模式,不传或false表示覆盖模式fos=newFileOutputStream("D:/test/output.txt",true);// 写入字节数据Stringtext="Hello, Java IO!\r\n";fos.write(text.getBytes());// 写入字节数组fos.write('A');// 写入单个字节// 强制刷出缓冲区到磁盘fos.flush();System.out.println("写入成功!");}catch(IOExceptione){e.printStackTrace();}finally{if(fos!=null){try{fos.close();}catch(IOExceptione){e.printStackTrace();}}}}}3.3 文件拷贝示例
结合输入流和输出流实现文件拷贝:
importjava.io.FileInputStream;importjava.io.FileOutputStream;importjava.io.IOException;publicclassFileCopyDemo{publicstaticvoidcopyFile(StringsrcPath,StringdestPath){try(FileInputStreamfis=newFileInputStream(srcPath);FileOutputStreamfos=newFileOutputStream(destPath)){byte[]buffer=newbyte[4096];intlen;while((len=fis.read(buffer))!=-1){fos.write(buffer,0,len);}System.out.println("文件拷贝完成!");}catch(IOExceptione){e.printStackTrace();}}publicstaticvoidmain(String[]args){copyFile("D:/test/source.jpg","D:/test/dest.jpg");}}小提示:上面使用了try-with-resources语法(Java 7+),实现了流的自动关闭,无需手动编写finally块。
四、字符流:Reader与Writer
字符流用于处理文本数据,自动处理字符编码问题。
4.1 FileReader与FileWriter
importjava.io.FileReader;importjava.io.FileWriter;importjava.io.IOException;publicclassFileReaderWriterDemo{publicstaticvoidmain(String[]args){// 写入文本文件try(FileWriterfw=newFileWriter("D:/test/poem.txt")){fw.write("床前明月光,\r\n");fw.write("疑是地上霜。\r\n");fw.write("举头望明月,\r\n");fw.write("低头思故乡。\r\n");System.out.println("文件写入成功!");}catch(IOExceptione){e.printStackTrace();}// 读取文本文件try(FileReaderfr=newFileReader("D:/test/poem.txt")){char[]buffer=newchar[1024];intlen;while((len=fr.read(buffer))!=-1){System.out.print(newString(buffer,0,len));}}catch(IOExceptione){e.printStackTrace();}}}4.2 字节流与字符流的桥接
当需要处理字节流但想要字符流特性时,可以使用InputStreamReader和OutputStreamWriter(这两个转换流将在后续文章详细讲解)。
五、try-with-resources 自动关闭资源
Java 7引入的try-with-resources机制极大简化了流的关闭操作。任何实现了java.lang.AutoCloseable接口的类都可以使用此语法:
// 传统写法(繁琐)FileInputStreamfis=null;try{fis=newFileInputStream("test.txt");// ... 处理数据}finally{if(fis!=null)fis.close();}// try-with-resources 写法(简洁)try(FileInputStreamfis=newFileInputStream("test.txt");FileOutputStreamfos=newFileOutputStream("out.txt")){// ... 处理数据,资源会自动关闭}catch(IOExceptione){e.printStackTrace();}执行顺序:后声明的资源先关闭,先声明的资源后关闭(类似栈的先进后出)。
六、IO流使用的最佳实践
以下几条实践原则是在实际开发中被反复验证过的经验总结:
- 选择合适的流类型:二进制数据用字节流(Stream),文本数据用字符流(Reader/Writer)。这里的"二进制"包括但不限于:图片、视频、压缩包、序列化对象等任何非纯文本格式。如果用字符流处理二进制数据,编码转换会破坏原始字节。
- 始终关闭流:使用try-with-resources自动管理资源。未关闭的流不仅造成内存泄漏,还可能导致文件句柄耗尽——在Linux系统上每个进程可打开的文件数有上限,满后无法再打开任何文件。
- 使用缓冲区:使用byte[]或char[]缓冲区批量读写,显著提升性能。一个4KB的缓冲区就能将读写效率提升数十倍,因为系统调用的开销远大于内存复制。
- 编码问题:读写文本时注意指定字符编码,避免乱码。这是跨平台部署时最常踩的坑——Windows默认GBK,Linux默认UTF-8,不显式指定编码就会在上线后出现乱码。
- 路径处理:使用
File.separator代替硬编码的路径分隔符,增强跨平台兼容性。或者在Java 7+中使用Paths.get()统一处理。
总结
本文详细介绍了Java IO流体系的核心概念:
- File类:用于表示文件和目录的元信息,是操作文件系统的入口
- 字节流(InputStream/OutputStream):处理二进制数据,适合图片、视频等文件
- 字符流(Reader/Writer):处理文本数据,内置字符编码支持
- try-with-resources:自动关闭资源的现代写法,推荐在所有IO操作中使用
掌握IO流体系是Java开发的基础技能,后续文章将继续深入讲解缓冲流、转换流以及NIO等高级IO技术。
✅ 亮点总结
- IO流按流向(输入/输出)和处理单位(字节/字符)两个维度的二维分类体系,形成四象限架构
- File类作为文件系统入口,封装了创建/删除/遍历/属性读取等完整的元信息操作
- 字节流(FileInputStream/FileOutputStream)处理二进制数据,字符流(FileReader/FileWriter)处理文本并内置编码
- 文件拷贝的完整实现:4KB缓冲区批量读写 + try-with-resources自动关闭,兼顾性能与安全性
- try-with-resources语法(Java 7+)的资源自动管理机制,后声明先关闭的栈式释放顺序
适用场景
- 配置文件读取与解析,如properties、json、yaml等格式文件的加载
- 日志文件的批量写入与按日期归档,组合File类创建目录和字符流写入内容
- 文件上传下载功能的底层实现,字节流处理任意格式、字符流处理文本内容
扩展方向
- 深入学习缓冲流(BufferedInputStream/BufferedReader)的性能优化原理与字符编码转换
- 研究Apache Commons IO和Google Guava的高效IO工具类封装
- 推荐阅读:22_Java缓冲流与转换流
下一篇:Java缓冲流与转换流
