Java IO详解:File、FileInputStream与FileOutputStream

文章目录

引言:Java IO体系与文件操作
在Java应用程序开发中,文件输入输出(I/O)是最基础且最常见的操作之一。无论是读取配置文件、处理用户上传的文档、记录日志信息,还是进行数据持久化,都离不开对文件的操作。Java的I/O体系通过"流"(Stream)的抽象,为开发者提供了一套统一而强大的API来处理各种设备间的数据传递。
本文将深入剖析Java文件IO的三大基石:
- File类:文件和目录路径名的抽象表示,用于文件和目录的创建、删除、查询等操作
- FileInputStream:字节文件输入流,用于从文件中读取原始字节数据
- FileOutputStream:字节文件输出流,用于将原始字节数据写入文件
我们将从源码层面解读其设计原理,探讨核心方法的使用技巧,分析性能优化的关键点,并提供最佳实践指南。全文预计超过8000字,力求让你彻底掌握Java文件IO的精髓。
第一章:IO流基础概念
1.1 什么是流(Stream)
在Java中,流是一个抽象的概念,代表了数据的"流动"。可以将其想象为连接数据源(源端)和程序(目的端)的一条管道,数据在管道中按顺序传输。
输入流(Input Stream):数据从外部源(如文件、网络、键盘)流入程序(内存)。程序从输入流中读取数据。
输出流(Output Stream):数据从程序(内存)流向外部目的地(如文件、网络、屏幕)。程序向输出流中写入数据。
1.2 Java IO流的分类
Java的IO流体系可以从三个维度进行分类:
| 分类维度 | 类别 | 说明 | 典型类 |
|---|---|---|---|
| 数据流向 | 输入流 | 读取数据到程序 | InputStream, Reader |
| 输出流 | 从程序写出数据 | OutputStream, Writer | |
| 操作单位 | 字节流 | 以字节(8位)为单位 | InputStream, OutputStream |
| 字符流 | 以字符(16位)为单位 | Reader, Writer | |
| 角色分工 | 节点流 | 直接从数据源读写 | FileInputStream, FileOutputStream |
| 处理流 | 包装节点流,提供增强功能 | BufferedInputStream, DataInputStream |
字节流 vs 字符流:
- 字节流:处理所有类型的文件(文本、图片、视频、音频等)。因为所有文件在底层都是以字节形式存储的,字节流是通用的。
- 字符流:专门处理文本文件。它内部处理字符编码和解码,方便处理人类可读的文本。
节点流 vs 处理流:
- 节点流:直接连接数据源,是IO操作的基础
- 处理流:在节点流之上进行包装,提供缓冲、转换、对象序列化等高级功能(装饰器模式)
1.3 文件IO的核心类
本文聚焦于文件IO的基础字节流:
- File:代表文件或目录路径,提供文件系统操作(创建、删除、重命名、查询属性等)
- FileInputStream:字节文件输入流,从文件中读取字节数据
- FileOutputStream:字节文件输出流,向文件中写入字节数据
这三者构成了Java进行原始文件操作的基础。理解它们的工作原理,是掌握更高级IO(如缓冲流、对象流、NIO)的前提。
第二章:File类深度剖析
java.io.File类是Java IO体系中唯一代表文件和目录路径名的类,但它不负责文件内容的读写。它更像是一个文件或目录的"名片",记录了路径信息,并提供了对文件元数据(属性)的操作方法。
2.1 类的定义与核心字段
publicclassFileimplementsSerializable,Comparable<File>{// 文件系统相关的操作接口(与具体操作系统交互)privatestaticfinalFileSystem fs =DefaultFileSystem.getFileSystem();// 核心字段:存储文件的路径privatefinalString path;// 路径的规范化状态privatetransientvolatilePathStatus status =null;// 文件路径的规范化字符串privatevolatiletransientString prefixLength;// 构造方法publicFile(String pathname){if(pathname ==null){thrownewNullPointerException();}this.path = fs.normalize(pathname);}// ...}关键分析:
File类实现了Serializable接口,意味着File对象可以被序列化- 实现了
Comparable<File>接口,提供了compareTo方法,可以按路径名字典序比较 - 核心字段
path被final修饰,说明File对象是不可变的——一旦创建,其代表的抽象路径名就不能改变 FileSystem fs是与底层操作系统交互的关键,所有与文件系统相关的操作(如检查文件是否存在、获取文件属性)最终都委托给它
2.2 构造方法:创建File对象
File类提供了多种构造方法,灵活适应不同的使用场景:
publicclassFileConstructorDemo{publicstaticvoidmain(String[] args){// 1. 通过完整路径名字符串创建File file1 =newFile("D:\\data\\document.txt");// Windows路径需转义File file2 =newFile("/home/user/document.txt");// Linux/Mac路径// 2. 通过父路径和子路径字符串创建String parent ="D:\\data";String child ="document.txt";File file3 =newFile(parent, child);// 3. 通过父File对象和子路径字符串创建File parentDir =newFile("D:\\data");File file4 =newFile(parentDir,"document.txt");// 4. 通过URI创建(略)}}重要说明:
- 创建时机:
new File()只是在内存中创建了一个对象,代表一个路径,并不会在硬盘上实际创建文件或目录。文件是否真正存在,需要通过exists()方法验证。 - 相对路径:相对路径相对于当前工作目录(可通过
System.getProperty("user.dir")获取)。在IDEA中,单元测试方法的相对路径相对于当前module,main方法的相对路径相对于当前工程。
路径分隔符:Windows使用反斜杠\,在Java字符串中需要转义为\\;Unix/Linux/Mac使用正斜杠/。更好的做法是使用File.separator常量,它会根据运行平台自动适配:
File file =newFile("D:"+File.separator +"data"+File.separator +"document.txt");2.3 常用API详解
File类的API主要分为四大类:获取基本信息、判断功能、列出目录内容、创建删除操作。
2.3.1 获取文件和目录基本信息
importjava.io.File;importjava.util.Date;publicclassFileGetInfoDemo{publicstaticvoidmain(String[] args){File file =newFile("test.txt");System.out.println("文件名: "+ file.getName());// test.txtSystem.out.println("路径: "+ file.getPath());// test.txt(构造时的路径)System.out.println("绝对路径: "+ file.getAbsolutePath());// 完整绝对路径try{System.out.println("规范路径: "+ file.getCanonicalPath());// 解析.和..的绝对路径}catch(Exception e){ e.printStackTrace();}System.out.println("父目录: "+ file.getParent());// null(如果没有父目录)System.out.println("文件大小: "+ file.length()+" 字节");// 文件实际大小System.out.println("最后修改时间: "+newDate(file.lastModified()));// 毫秒值}}关键点:
length()返回0的情况:文件不存在,或者文件确实为空lastModified()返回的是从1970-01-01 UTC开始的毫秒数getAbsolutePath()与getCanonicalPath()的区别:绝对路径可能包含.或..,规范路径会解析这些相对引用
2.3.2 判断功能
importjava.io.File;publicclassFileCheckDemo{publicstaticvoidmain(String[] args){File file =newFile("test.txt");System.out.println("是否存在: "+ file.exists());// true/falseSystem.out.println("是否是文件: "+ file.isFile());// trueSystem.out.println("是否是目录: "+ file.isDirectory());// falseSystem.out.println("是否可读: "+ file.canRead());// 权限检查System.out.println("是否可写: "+ file.canWrite());// 权限检查System.out.println("是否可执行: "+ file.canExecute());// 对于文件是可执行权限,对于目录是可遍历权限System.out.println("是否隐藏: "+ file.isHidden());// 平台相关}}注意:isFile()和isDirectory()在文件不存在时都返回false,不是抛出异常。
2.3.3 列出目录内容
importjava.io.File;publicclassFileListDemo{publicstaticvoidmain(String[] args){File dir =newFile("D:\\data");if(dir.exists()&& dir.isDirectory()){// 方法1:返回字符串数组(仅名称)String[] fileNames = dir.list();System.out.println("目录中的文件和子目录:");for(String name : fileNames){System.out.println(" "+ name);}// 方法2:返回File对象数组(完整路径)File[] files = dir.listFiles();System.out.println("\nFile对象列表:");for(File f : files){System.out.println(" "+ f.getPath()+(f.isDirectory()?" [目录]":" [文件]"));}// 带文件名过滤器的版本File[] txtFiles = dir.listFiles((d, name)-> name.endsWith(".txt"));System.out.println("\n文本文件:");for(File f : txtFiles){System.out.println(" "+ f.getName());}}}}源码分析:listFiles()最终会调用FileSystem的list()方法,这是一个native方法,与操作系统交互获取目录内容。
2.3.4 创建与删除
importjava.io.File;importjava.io.IOException;publicclassFileCreateDeleteDemo{publicstaticvoidmain(String[] args){// 1. 创建文件File file =newFile("newfile.txt");try{if(file.createNewFile()){System.out.println("文件创建成功: "+ file.getName());}else{System.out.println("文件已存在,无需创建");}}catch(IOException e){System.out.println("创建文件时发生IO错误"); e.printStackTrace();}// 2. 创建单级目录File singleDir =newFile("mydir");if(singleDir.mkdir()){System.out.println("目录创建成功: "+ singleDir.getName());}else{System.out.println("目录创建失败(可能已存在或父目录不存在)");}// 3. 创建多级目录File multiDir =newFile("parent/child/grandchild");if(multiDir.mkdirs()){System.out.println("多级目录创建成功: "+ multiDir.getPath());}else{System.out.println("多级目录创建失败");}// 4. 删除文件或空目录if(file.delete()){System.out.println("文件删除成功");}else{System.out.println("文件删除失败(可能不存在或无权限)");}// 5. 程序退出时自动删除File tempFile =newFile("temp.txt");try{if(tempFile.createNewFile()){ tempFile.deleteOnExit();// JVM退出时删除System.out.println("临时文件将在程序退出时删除");}}catch(IOException e){ e.printStackTrace();}}}重要说明:
createNewFile():原子操作,检查文件是否存在并创建,返回boolean表示是否成功创建mkdir()vsmkdirs():前者要求父目录必须存在,后者会创建所有不存在的父目录delete():直接删除,不走回收站,需谨慎操作deleteOnExit():注册一个钩子,在JVM正常退出时删除文件,适用于临时文件清理
2.3.5 重命名与移动
importjava.io.File;publicclassFileRenameDemo{publicstaticvoidmain(String[] args){File oldFile =newFile("oldname.txt");File newFile =newFile("newname.txt");// 确保原文件存在try{ oldFile.createNewFile();}catch(Exception e){ e.printStackTrace();}// 重命名/移动if(oldFile.renameTo(newFile)){System.out.println("重命名成功");System.out.println("新文件存在: "+ newFile.exists());System.out.println("旧文件存在: "+ oldFile.exists());}else{System.out.println("重命名失败");}}}关键点:
renameTo(File dest)的行为依赖于平台- 在同一文件系统内,相当于重命名(原子操作)
- 在不同文件系统间,可能表现为复制+删除,不是原子操作
- 目标文件不能已存在(某些平台会覆盖)
2.4 应用场景与最佳实践
场景1:递归遍历目录树
importjava.io.File;publicclassDirectoryTraversal{publicstaticvoidtraverse(File dir,String indent){if(!dir.exists()||!dir.isDirectory()){System.out.println(indent + dir.getPath()+" (不是有效目录)");return;}File[] files = dir.listFiles();if(files ==null)return;for(File file : files){if(file.isDirectory()){System.out.println(indent +"[DIR] "+ file.getName());traverse(file, indent +" ");// 递归}else{System.out.println(indent +"[FILE] "+ file.getName()+" ("+ file.length()+" 字节)");}}}publicstaticvoidmain(String[] args){File root =newFile("D:\\data");traverse(root,"");}}场景2:文件过滤器实现
importjava.io.File;importjava.io.FileFilter;importjava.io.FilenameFilter;publicclassFileFilterDemo{publicstaticvoidmain(String[] args){File dir =newFile("D:\\data");// 使用FilenameFilter(过滤文件名)String[] images = dir.list(newFilenameFilter(){@Overridepublicbooleanaccept(File dir,String name){return name.endsWith(".jpg")|| name.endsWith(".png");}});// 使用FileFilter(过滤File对象)File[] largeFiles = dir.listFiles(newFileFilter(){@Overridepublicbooleanaccept(File pathname){return pathname.isFile()&& pathname.length()>1024*1024;// > 1MB}});// Lambda简化版File[] hiddenFiles = dir.listFiles(f -> f.isHidden());}}2.5 File类的局限性
尽管File类是文件操作的基础,但它存在一些局限性:
- 不能访问文件内容:File类只操作元数据,不能读写文件内容
- 操作失败处理:很多方法只返回boolean,不提供详细的失败原因
- 符号链接处理:对符号链接的支持有限
- 文件属性:无法设置文件所有者、权限等高级属性
- 大文件支持:length()返回long,理论上支持大文件,但一些方法如lastModified()精度有限
JDK 7引入了java.nio.file.Path和Files类,提供了更强大的文件操作功能,但在基础学习中,File类仍然是入门文件IO的第一步。
第三章:FileInputStream源码剖析与使用
java.io.FileInputStream是字节文件输入流,用于从文件中读取原始字节数据。它是InputStream抽象类的直接子类,适用于读取二进制文件(如图片、音频、视频)或任何需要按字节处理的文件。
3.1 类定义与继承体系
publicclassFileInputStreamextendsInputStream继承关系:
java.lang.Object └── java.io.InputStream └── java.io.FileInputStream FileInputStream继承了InputStream,因此它拥有所有输入流的基本方法:read()、read(byte[])、close()等,并根据文件读取的特性进行了实现。
3.2 核心字段与构造方法
3.2.1 核心字段
publicclassFileInputStreamextendsInputStream{/* 文件描述符,用于打开文件的句柄 */privatefinalFileDescriptor fd;/* 引用文件的路径,如果流是通过文件描述符创建时该值为null */privatefinalString path;/* 文件通道,用于NIO操作 */privateFileChannel channel =null;/* 关闭锁,确保close操作的线程安全性 */privatefinalObject closeLock =newObject();/* 标记流是否已关闭 */privatevolatileboolean closed =false;// ... 其他代码}字段解读:
- FileDescriptor fd:文件描述符,是操作系统用于管理打开文件的句柄。它包含了打开文件的关键信息,后续所有读写操作都通过它进行。
- String path:记录文件的路径,用于错误信息和跟踪。如果流是通过已有的
FileDescriptor创建的,则此字段为null。 - FileChannel channel:NIO中的通道,提供了与文件关联的通道,可以进行内存映射、文件锁定等高级操作。它是懒加载的,只有在调用
getChannel()时才会创建。 - closeLock:用于同步
close()方法,防止多个线程同时关闭导致的问题。 - closed:
volatile修饰,确保多线程间的可见性。
3.2.2 构造方法
FileInputStream提供了三种重载的构造方法:
// 1. 通过文件路径名字符串创建publicFileInputStream(String name)throwsFileNotFoundException{this(name !=null?newFile(name):null);}// 2. 通过File对象创建(最常用)publicFileInputStream(File file)throwsFileNotFoundException{String name =(file !=null? file.getPath():null);SecurityManager security =System.getSecurityManager();if(security !=null){ security.checkRead(name);// 安全检查:是否有读取权限}if(name ==null){thrownewNullPointerException();}if(file.isInvalid()){thrownewFileNotFoundException("Invalid file path");} fd =newFileDescriptor(); fd.attach(this);// 将当前流关联到文件描述符 path = name;open(name);// 调用native方法打开文件}// 3. 通过已有的FileDescriptor创建publicFileInputStream(FileDescriptor fdObj){SecurityManager security =System.getSecurityManager();if(fdObj ==null){thrownewNullPointerException();}if(security !=null){ security.checkRead(fdObj);} fd = fdObj; path =null; fd.attach(this);// 将当前流关联到已存在的文件描述符}构造过程分析:
- 参数校验和权限检查(
SecurityManager) - 创建(或使用已有的)
FileDescriptor对象 - 调用
attach(this)将文件描述符与当前流关联,便于后续资源释放 - 如果是通过文件路径创建,调用
open(name)本地方法,真正与操作系统交互打开文件 - 如果文件不存在、是目录或无法打开,抛出
FileNotFoundException
核心native方法:
privatenativevoidopen(String name)throwsFileNotFoundException;这个native方法会调用操作系统的API(如Windows的CreateFile,Linux的open)来打开文件,获取文件句柄存储在fd中。
3.3 核心方法详解
3.3.1 read():读取单个字节
publicintread()throwsIOException{returnread0();}privatenativeintread0()throwsIOException;工作原理:
- 每次调用读取一个字节(8位)
- 返回值范围:0到255(无符号字节)
- 如果到达文件末尾,返回-1
- 该方法会阻塞,直到有数据可读、到达文件末尾或发生异常
- 底层通过native方法直接调用操作系统读文件的系统调用
性能考量:每次读取一个字节意味着每个字节都要进行一次系统调用,对于大文件来说效率极低。实际开发中几乎不使用此方法读取大量数据。
3.3.2 read(byte[] b):读取到字节数组
publicintread(byte b[])throwsIOException{returnreadBytes(b,0, b.length);}publicintread(byte b[],int off,int len)throwsIOException{returnreadBytes(b, off, len);}privatenativeintreadBytes(byte b[],int off,int len)throwsIOException;工作原理:
- 尝试读取最多
b.length个字节到数组中 - 返回实际读取的字节数,可能小于请求的长度
- 返回-1表示文件末尾
- native方法
readBytes会尽可能多地读取数据,但受限于文件剩余字节数 - 可以指定偏移量
off,将数据存入数组的指定位置
示例代码:
importjava.io.FileInputStream;importjava.io.IOException;publicclassFileInputStreamReadDemo{publicstaticvoidmain(String[] args){try(FileInputStream fis =newFileInputStream("test.dat")){byte[] buffer =newbyte[1024];int bytesRead;while((bytesRead = fis.read(buffer))!=-1){// 处理读取到的数据,注意只处理bytesRead个字节// buffer中可能只有部分数据是有效的processData(buffer, bytesRead);}}catch(IOException e){ e.printStackTrace();}}privatestaticvoidprocessData(byte[] data,int length){// 处理前length个字节System.out.println("读取到 "+ length +" 字节");}}重要提示:fis.read(buffer)返回的是实际读取的字节数,这个数值可能小于buffer的长度(特别是在接近文件末尾时)。处理数据时必须使用返回的长度,而不是buffer.length。
3.3.3 skip(long n):跳过字节
publicnativelongskip(long n)throwsIOException;工作原理:
- 跳过并丢弃输入流中的n个字节
- 返回实际跳过的字节数(可能小于n)
- 如果n为负数,某些平台支持回退(如例子中
skip(-1)) - 跳过文件末尾不会抛出异常,但后续read会返回-1
示例:
FileInputStream fis =newFileInputStream("data.bin"); fis.skip(10);// 跳过前10个字节int b = fis.read();// 读取第11个字节3.3.4 available():可用字节数估计
publicnativeintavailable()throwsIOException;工作原理:
- 返回估计的剩余可读取字节数(不受阻塞)
- 这是一个估计值,不保证精确
- 通常用于判断是否需要创建缓冲区,但不能依赖它作为文件总长度的准确值
- 文件超过EOF时返回0
常见误用:
// 错误:用available()确定文件大小FileInputStream fis =newFileInputStream("file.txt");byte[] data =newbyte[fis.available()];// 可能太小,也可能太大 fis.read(data);// 可能无法填满缓冲区正确用法:仅用于非阻塞场景的提示,不能替代循环读取。
3.3.5 close():关闭流
publicvoidclose()throwsIOException{synchronized(closeLock){if(closed){return;} closed =true;}if(channel !=null){ channel.close();}// 关闭文件描述符 fd.closeAll(newCloseable(){publicvoidclose()throwsIOException{close0();}});}privatenativevoidclose0()throwsIOException;设计要点:
- 使用
closeLock保证线程安全,防止重复关闭 - 关闭时,如果有关联的
FileChannel,一并关闭 - 通过
fd.closeAll()最终调用native方法close0()释放系统资源 - 推荐使用try-with-resources确保自动关闭
3.3.6 getChannel():获取文件通道
publicFileChannelgetChannel(){synchronized(this){if(channel ==null){ channel =FileChannelImpl.open(fd, path,true,false,this);}return channel;}}作用:返回与此文件输入流关联的唯一的FileChannel对象。这是Java NIO的入口,可以进行更高效的文件操作(如内存映射文件、文件锁定等)。
3.4 线程安全性分析
FileInputStream的实例方法本身不是线程安全的,但它的某些操作具有原子性。
通过多线程测试可以发现:单次read操作本身不会被其他线程抢占而中断,它会完整地读取这次要读取的内容。但是,由于其他线程可以改变输入流的位置(通过skip或read),每个线程读取时开始的位置是不可预知的。
// 来自并发编程网的测试代码publicclassFileThreadTestimplementsRunnable{privateint type;// 0做skip操作,1做读取操作privateint gap;privateFileInputStream in;// ... 构造方法@Overridepublicvoidrun(){byte[] body =newbyte[gap];if(this.type ==0){// 执行skip操作}else{// 执行read操作try{for(int i =0; i <10; i++){ in.read(body);System.out.println(Thread.currentThread().getName()+"-"+newString(body));}}catch(IOException e){ e.printStackTrace();}}}}测试结果表明:
- 每个
read()调用是原子的,会读取完整的一组字节 - 但由于流的位置是共享的,多个线程交替执行会导致读取位置混乱
- 如果需要线程安全,必须在外部进行同步,或每个线程使用自己的流实例
3.5 使用示例与最佳实践
示例1:基本文件读取(try-with-resources)
importjava.io.FileInputStream;importjava.io.IOException;publicclassFileInputStreamExample{publicstaticvoidmain(String[] args){// JDK 7+ try-with-resources 自动关闭流try(FileInputStream fis =newFileInputStream("input.dat")){byte[] buffer =newbyte[4096];// 4KB缓冲区int bytesRead;while((bytesRead = fis.read(buffer))!=-1){// 处理数据System.out.println("读取了 "+ bytesRead +" 字节");// 例如:将字节写入其他流、解析数据等}}catch(IOException e){System.err.println("文件读取错误: "+ e.getMessage());}}}示例2:复制文件(结合FileOutputStream)
importjava.io.FileInputStream;importjava.io.FileOutputStream;importjava.io.IOException;publicclassFileCopyExample{publicstaticvoidcopyFile(String source,String dest)throwsIOException{try(FileInputStream fis =newFileInputStream(source);FileOutputStream fos =newFileOutputStream(dest)){byte[] buffer =newbyte[8192];// 8KB缓冲区int bytesRead;while((bytesRead = fis.read(buffer))!=-1){ fos.write(buffer,0, bytesRead);}}// 自动关闭两个流}publicstaticvoidmain(String[] args){try{copyFile("source.jpg","copy.jpg");System.out.println("文件复制成功");}catch(IOException e){System.err.println("复制失败: "+ e.getMessage());}}}示例3:读取部分数据(指定偏移量)
importjava.io.FileInputStream;importjava.io.IOException;publicclassReadPartialExample{publicstaticvoidmain(String[] args){try(FileInputStream fis =newFileInputStream("data.bin")){byte[] header =newbyte[10];// 读取文件头10字节byte[] body =newbyte[100];// 读取接下来的100字节int headerRead = fis.read(header);if(headerRead ==10){System.out.println("文件头读取成功");}int bodyRead = fis.read(body);System.out.println("读取了 "+ bodyRead +" 字节的正文");}catch(IOException e){ e.printStackTrace();}}}3.6 性能优化建议
- 合理设置缓冲区大小:通常4KB-64KB之间,具体取决于应用场景
- 避免在循环中使用单字节读取:
while ((b = fis.read()) != -1)是性能杀手 - 考虑使用NIO:对于大文件或需要高吞吐量的场景,
FileChannel和内存映射文件性能更好
使用缓冲流:FileInputStream每次读取都会触发系统调用。包装为BufferedInputStream可大幅减少系统调用次数:
try(BufferedInputStream bis =newBufferedInputStream(newFileInputStream("large.dat"))){// 读取效率更高}第四章:FileOutputStream源码剖析与使用
java.io.FileOutputStream是字节文件输出流,用于将原始字节数据写入文件。它是OutputStream抽象类的直接子类,与FileInputStream对应。
4.1 类定义与继承体系
publicclassFileOutputStreamextendsOutputStream继承关系:
java.lang.Object └── java.io.OutputStream └── java.io.FileOutputStream 4.2 核心字段与构造方法
4.2.1 核心字段
publicclassFileOutputStreamextendsOutputStream{/* 文件描述符 */privatefinalFileDescriptor fd;/* 文件路径 */privatefinalString path;/* 是否以追加模式打开 */privatefinalboolean append;/* 文件通道 */privateFileChannel channel;/* 关闭锁 */privatefinalObject closeLock =newObject();/* 是否已关闭 */privatevolatileboolean closed =false;// ...}与FileInputStream不同的字段:
- boolean append:标记是否为追加模式。
true表示写入的数据追加到文件末尾,false表示覆盖文件开头。
4.2.2 构造方法
FileOutputStream提供了5个重载的构造方法:
// 1. 通过文件名创建(覆盖模式)publicFileOutputStream(String name)throwsFileNotFoundException{this(name !=null?newFile(name):null,false);}// 2. 通过文件名创建,指定是否追加publicFileOutputStream(String name,boolean append)throwsFileNotFoundException{this(name !=null?newFile(name):null, append);}// 3. 通过File对象创建(覆盖模式)publicFileOutputStream(File file)throwsFileNotFoundException{this(file,false);}// 4. 通过File对象创建,指定是否追加publicFileOutputStream(File file,boolean append)throwsFileNotFoundException{String name =(file !=null? file.getPath():null);SecurityManager security =System.getSecurityManager();if(security !=null){ security.checkWrite(name);// 安全检查:是否有写入权限}if(name ==null){thrownewNullPointerException();}if(file.isInvalid()){thrownewFileNotFoundException("Invalid file path");}this.fd =newFileDescriptor();this.append = append;this.path = name;// 根据追加模式打开文件open(name, append);}// 5. 通过FileDescriptor创建publicFileOutputStream(FileDescriptor fdObj){SecurityManager security =System.getSecurityManager();if(fdObj ==null){thrownewNullPointerException();}if(security !=null){ security.checkWrite(fdObj);}this.fd = fdObj;this.path =null;this.append =false; fd.attach(this);}核心native方法:
privatenativevoidopen(String name,boolean append)throwsFileNotFoundException;打开模式说明:
append = false(默认):文件指针定位到文件开头。如果文件已存在,原有内容会被新写入的内容覆盖append = true:文件指针定位到文件末尾,新写入的内容追加到原内容之后
4.3 核心方法详解
4.3.1 write(int b):写入单个字节
publicvoidwrite(int b)throwsIOException{write(b, append);}privatenativevoidwrite(int b,boolean append)throwsIOException;工作原理:
- 写入一个字节(参数b的低8位,高24位被忽略)
- 如果流以追加模式打开,写入位置在文件末尾
- 否则在文件开头
- native方法直接调用操作系统写文件系统调用
性能考量:与FileInputStream.read()类似,单字节写入效率极低,不推荐大量使用。
4.3.2 write(byte[] b):写入字节数组
publicvoidwrite(byte b[])throwsIOException{writeBytes(b,0, b.length, append);}publicvoidwrite(byte b[],int off,int len)throwsIOException{writeBytes(b, off, len, append);}privatenativevoidwriteBytes(byte b[],int off,int len,boolean append)throwsIOException;工作原理:
- 将字节数组中的全部或部分数据写入文件
- 建议使用批量写入提高性能
- native方法一次性写入多个字节,减少系统调用次数
4.3.3 close():关闭流
publicvoidclose()throwsIOException{synchronized(closeLock){if(closed){return;} closed =true;}if(channel !=null){ channel.close();} fd.closeAll(newCloseable(){publicvoidclose()throwsIOException{close0();}});}privatenativevoidclose0()throwsIOException;与FileInputStream类似:使用锁保证线程安全,关闭关联的通道,释放系统资源。
4.3.4 getChannel()和getFD()
publicFileChannelgetChannel(){synchronized(this){if(channel ==null){ channel =FileChannelImpl.open(fd, path,false,true, append,this);}return channel;}}publicfinalFileDescriptorgetFD()throwsIOException{if(fd !=null)return fd;thrownewIOException();}4.4 使用示例与最佳实践
示例1:基本文件写入
importjava.io.FileOutputStream;importjava.io.IOException;publicclassFileOutputStreamBasicExample{publicstaticvoidmain(String[] args){String data ="Hello, Java IO!";try(FileOutputStream fos =newFileOutputStream("output.txt")){// 将字符串转换为字节数组写入 fos.write(data.getBytes());// 也可以逐字节写入(不推荐)// for (byte b : data.getBytes()) {// fos.write(b);// }System.out.println("数据写入成功");}catch(IOException e){System.err.println("写入失败: "+ e.getMessage());}}}示例2:追加模式 vs 覆盖模式
importjava.io.FileOutputStream;importjava.io.IOException;publicclassAppendVsOverwrite{publicstaticvoidmain(String[] args){// 覆盖模式try(FileOutputStream fos =newFileOutputStream("test.txt")){ fos.write("First line\n".getBytes());}catch(IOException e){ e.printStackTrace();}// 再次写入(覆盖模式)try(FileOutputStream fos =newFileOutputStream("test.txt")){ fos.write("Second line\n".getBytes());// 原内容被覆盖}catch(IOException e){ e.printStackTrace();}// 追加模式try(FileOutputStream fos =newFileOutputStream("test.txt",true)){ fos.write("Third line\n".getBytes());// 追加到末尾}catch(IOException e){ e.printStackTrace();}}}执行后,test.txt内容为:
Second line Third line 示例3:写入二进制数据
importjava.io.FileOutputStream;importjava.io.IOException;publicclassWriteBinaryExample{publicstaticvoidmain(String[] args){try(FileOutputStream fos =newFileOutputStream("binary.dat")){// 写入int类型(4字节)int value =12345678; fos.write((value >>>24)&0xFF);// 高位字节 fos.write((value >>>16)&0xFF); fos.write((value >>>8)&0xFF); fos.write(value &0xFF);// 低位字节// 或者使用DataOutputStream简化// try (DataOutputStream dos = new DataOutputStream(fos)) {// dos.writeInt(value);// }}catch(IOException e){ e.printStackTrace();}}}4.5 注意事项与常见陷阱
- 自动创建文件:如果输出文件不存在,
FileOutputStream会自动创建它(前提是父目录存在) - 目录 vs 文件:如果指定的路径是一个已存在的目录,会抛出
FileNotFoundException - 权限问题:如果没有写入权限,也会抛出
FileNotFoundException - 覆盖 vs 追加:默认为覆盖模式,需要追加时务必使用带
boolean append参数的构造方法 - 数据持久性:
write()方法返回时,数据不一定已经持久化到磁盘,可能还在操作系统缓存中。需要确保数据真正写入可调用getFD().sync() - 多线程写入:与
FileInputStream类似,多线程共享同一个FileOutputStream会导致数据交错,需要外部同步或使用每个线程独立的流
第五章:综合实战与最佳实践
5.1 文件复制工具完整实现
importjava.io.*;importjava.nio.file.Files;importjava.nio.file.Path;importjava.nio.file.Paths;importjava.nio.file.StandardCopyOption;/** * 文件复制工具 - 展示多种实现方式 */publicclassFileCopyUtil{/** * 方式1:使用FileInputStream/FileOutputStream(基础字节流) */publicstaticvoidcopyByStream(String src,String dest)throwsIOException{File srcFile =newFile(src);File destFile =newFile(dest);// 检查源文件是否存在if(!srcFile.exists()){thrownewFileNotFoundException("源文件不存在: "+ src);}// 确保目标文件父目录存在File parent = destFile.getParentFile();if(parent !=null&&!parent.exists()){ parent.mkdirs();}// 使用try-with-resources自动关闭资源try(FileInputStream fis =newFileInputStream(srcFile);FileOutputStream fos =newFileOutputStream(destFile)){byte[] buffer =newbyte[8192];// 8KB缓冲区int bytesRead;while((bytesRead = fis.read(buffer))!=-1){ fos.write(buffer,0, bytesRead);} fos.flush();// 确保所有数据写入}}/** * 方式2:使用BufferedInputStream/BufferedOutputStream(缓冲流优化) */publicstaticvoidcopyByBufferedStream(String src,String dest)throwsIOException{try(BufferedInputStream bis =newBufferedInputStream(newFileInputStream(src));BufferedOutputStream bos =newBufferedOutputStream(newFileOutputStream(dest))){byte[] buffer =newbyte[8192];int bytesRead;while((bytesRead = bis.read(buffer))!=-1){ bos.write(buffer,0, bytesRead);} bos.flush();}}/** * 方式3:使用FileChannel(NIO,适合大文件) */publicstaticvoidcopyByChannel(String src,String dest)throwsIOException{try(FileInputStream fis =newFileInputStream(src);FileOutputStream fos =newFileOutputStream(dest);FileChannel inChannel = fis.getChannel();FileChannel outChannel = fos.getChannel()){// 直接传输,无需手动缓冲区 inChannel.transferTo(0, inChannel.size(), outChannel);}}/** * 方式4:使用Files工具类(Java 7+,最简单) */publicstaticvoidcopyByFiles(String src,String dest)throwsIOException{Path sourcePath =Paths.get(src);Path destPath =Paths.get(dest);Files.copy(sourcePath, destPath,StandardCopyOption.REPLACE_EXISTING);}/** * 性能测试 */publicstaticvoidperformanceTest(String src,String dest){long start, end;try{// 测试基础流 start =System.currentTimeMillis();copyByStream(src, dest +".stream"); end =System.currentTimeMillis();System.out.println("基础流耗时: "+(end - start)+"ms");// 测试缓冲流 start =System.currentTimeMillis();copyByBufferedStream(src, dest +".buffered"); end =System.currentTimeMillis();System.out.println("缓冲流耗时: "+(end - start)+"ms");// 测试NIO通道 start =System.currentTimeMillis();copyByChannel(src, dest +".channel"); end =System.currentTimeMillis();System.out.println("NIO通道耗时: "+(end - start)+"ms");// 测试Files工具类 start =System.currentTimeMillis();copyByFiles(src, dest +".files"); end =System.currentTimeMillis();System.out.println("Files工具类耗时: "+(end - start)+"ms");}catch(IOException e){ e.printStackTrace();}}publicstaticvoidmain(String[] args){if(args.length <2){System.out.println("用法: java FileCopyUtil <源文件> <目标文件>");return;}try{copyByStream(args[0], args[1]);System.out.println("文件复制成功");}catch(IOException e){System.err.println("复制失败: "+ e.getMessage());}}}5.2 文件加密/解密示例(异或算法)
importjava.io.*;/** * 简单的文件加密/解密工具(使用异或算法) * 相同的程序执行两次即可解密(异或的特性:a XOR key XOR key = a) */publicclassFileCipher{privatestaticfinalbyte DEFAULT_KEY =0x7F;// 加密密钥/** * 加密/解密文件 */publicstaticvoidprocessFile(String src,String dest,byte key)throwsIOException{try(FileInputStream fis =newFileInputStream(src);FileOutputStream fos =newFileOutputStream(dest)){byte[] buffer =newbyte[4096];int bytesRead;while((bytesRead = fis.read(buffer))!=-1){// 对每个字节进行异或运算for(int i =0; i < bytesRead; i++){ buffer[i]^= key;} fos.write(buffer,0, bytesRead);}}}publicstaticvoidmain(String[] args){if(args.length <2){System.out.println("用法: java FileCipher <源文件> <目标文件> [密钥]");return;}String src = args[0];String dest = args[1];byte key = args.length >2?Byte.parseByte(args[2]): DEFAULT_KEY;try{processFile(src, dest, key);System.out.println("文件处理完成");}catch(IOException e){System.err.println("处理失败: "+ e.getMessage());}}}5.3 配置文件读取示例
importjava.io.*;importjava.util.Properties;/** * 读取配置文件示例 */publicclassConfigReader{privateProperties props =newProperties();/** * 从文件加载配置 */publicvoidloadConfig(String configFile)throwsIOException{try(FileInputStream fis =newFileInputStream(configFile)){ props.load(fis);// Properties提供了load(InputStream)方法}}/** * 保存配置到文件 */publicvoidsaveConfig(String configFile)throwsIOException{try(FileOutputStream fos =newFileOutputStream(configFile)){ props.store(fos,"Configuration File");}}/** * 手动解析配置文件(演示FileInputStream用法) */publicvoidmanualParseConfig(String configFile)throwsIOException{try(FileInputStream fis =newFileInputStream(configFile)){byte[] buffer =newbyte[1024];int bytesRead = fis.read(buffer);if(bytesRead >0){String content =newString(buffer,0, bytesRead);String[] lines = content.split("\n");for(String line : lines){ line = line.trim();if(line.isEmpty()|| line.startsWith("#")){continue;// 忽略空行和注释}int eqIndex = line.indexOf('=');if(eqIndex >0){String key = line.substring(0, eqIndex).trim();String value = line.substring(eqIndex +1).trim(); props.setProperty(key, value);}}}}}publicStringgetProperty(String key){return props.getProperty(key);}publicstaticvoidmain(String[] args){ConfigReader reader =newConfigReader();try{ reader.loadConfig("config.properties");System.out.println("db.url = "+ reader.getProperty("db.url"));System.out.println("db.username = "+ reader.getProperty("db.username"));}catch(IOException e){ e.printStackTrace();}}}5.4 资源管理最佳实践:try-with-resources
JDK 7引入的try-with-resources语句是处理IO资源的最佳实践:
// 传统方式(JDK 6及以前)FileInputStream fis =null;try{ fis =newFileInputStream("file.txt");// 读取操作}catch(IOException e){ e.printStackTrace();}finally{if(fis !=null){try{ fis.close();}catch(IOException e){ e.printStackTrace();}}}// try-with-resources方式(JDK 7+)try(FileInputStream fis =newFileInputStream("file.txt");FileOutputStream fos =newFileOutputStream("out.txt")){// 读取和写入操作}catch(IOException e){ e.printStackTrace();}// 自动关闭fis和fos,无需finally优点:
- 代码简洁,避免嵌套的try-catch-finally
- 自动处理关闭顺序(按照资源声明相反的顺序)
- 如果try块和close()都抛出异常,try块的异常会抑制close()的异常
5.5 常见问题与解决方案
问题1:文件被占用(Windows平台)
现象:在Windows上,如果文件已被其他程序打开,再尝试写入会抛出FileNotFoundException。
解决方案:
- 确保程序逻辑正确释放资源
- 使用
FileChannel的tryLock()尝试获取文件锁 - 考虑使用随机访问文件
RandomAccessFile
问题2:路径不存在
现象:写入文件时父目录不存在,抛出FileNotFoundException。
解决方案:
File file =newFile("parent/child/data.txt");File parent = file.getParentFile();if(parent !=null&&!parent.exists()){ parent.mkdirs();// 创建所有不存在的父目录}try(FileOutputStream fos =newFileOutputStream(file)){// 写入数据}问题3:编码问题
现象:使用FileOutputStream写入文本时,出现乱码。
解决方案:
使用字符流FileWriter(可指定编码):
try(FileWriter fw =newFileWriter("file.txt",StandardCharsets.UTF_8)){ fw.write(text);}明确指定字符编码:
String text ="中文内容";try(FileOutputStream fos =newFileOutputStream("file.txt")){ fos.write(text.getBytes(StandardCharsets.UTF_8));}问题4:性能瓶颈
现象:读写大文件时速度很慢。
解决方案:
- 使用缓冲流包装:
new BufferedInputStream(new FileInputStream(...)) - 增加缓冲区大小(4KB-64KB)
- 使用NIO的
FileChannel.transferTo()或内存映射文件 - 考虑异步IO(NIO.2)
第六章:总结与展望
6.1 三大核心类对比
| 特性 | File | FileInputStream | FileOutputStream |
|---|---|---|---|
| 主要作用 | 文件和目录路径名操作 | 从文件读取字节数据 | 向文件写入字节数据 |
| 核心功能 | 创建、删除、重命名、查询属性 | 读取字节数组/单个字节 | 写入字节数组/单个字节 |
| 是否处理内容 | 否(只处理元数据) | 是 | 是 |
| 数据流向 | N/A | 文件 → 程序 | 程序 → 文件 |
| 线程安全 | 是(不可变对象) | 否(需要外部同步) | 否(需要外部同步) |
| 异常处理 | 部分方法返回boolean | IOException | IOException |
| JDK版本 | 1.0 | 1.0 | 1.0 |
6.2 从字节流到高级IO的演进
本文深入讲解了File、FileInputStream和FileOutputStream这三个基础的IO类。在实际开发中,我们通常不会直接使用它们,而是基于它们构建更高级的IO处理:
- 缓冲流:
BufferedInputStream/BufferedOutputStream- 减少系统调用次数 - 数据流:
DataInputStream/DataOutputStream- 读写Java基本数据类型 - 对象流:
ObjectInputStream/ObjectOutputStream- 对象序列化 - 字符流:
FileReader/FileWriter- 专门处理文本文件 - NIO.2:
Files、Path、FileChannel- 更现代、更强大的文件操作
6.3 未来趋势
随着Java的发展,文件IO也在不断演进:
- JDK 7 NIO.2:引入了
Path、Files类,提供了更全面的文件操作API - JDK 8:增强了
Files类的方法,支持Stream API遍历目录 - JDK 11:
Files.readString()和Files.writeString()简化文本文件读写 - 未来:可能进一步增强异步IO、内存访问等特性
6.4 给开发者的建议
- 掌握基础:深刻理解File、FileInputStream、FileOutputStream,它们是所有Java文件IO的基石
- 使用缓冲:除非处理极小的文件,否则始终用缓冲流包装字节流
- 明确资源管理:始终使用try-with-resources确保资源释放
- 区分字节与字符:处理文本优先考虑字符流或指定编码,处理二进制使用字节流
- 了解NIO:对于高性能要求的场景,学习并应用NIO.2 API
- 阅读源码:通过阅读JDK源码,理解设计模式(装饰器模式)和底层实现
附录:常用代码片段速查
读取文件所有字节
byte[] allBytes =Files.readAllBytes(Paths.get("file.dat"));读取文件所有行
List<String> lines =Files.readAllLines(Paths.get("file.txt"),StandardCharsets.UTF_8);写入字符串到文件
Files.write(Paths.get("file.txt"),"Hello".getBytes(StandardCharsets.UTF_8));遍历目录(Java 8 Stream)
Files.list(Paths.get(".")).forEach(System.out::println);递归遍历目录树
Files.walk(Paths.get(".")).filter(Files::isRegularFile).forEach(System.out::println);临时文件创建
Path tempFile =Files.createTempFile("prefix",".tmp");文件复制
Files.copy(Paths.get("source.txt"),Paths.get("dest.txt"),StandardCopyOption.REPLACE_EXISTING);获取文件大小
long size =Files.size(Paths.get("file.txt"));检查文件是否存在
boolean exists =Files.exists(Paths.get("file.txt"));