(二十二)Java File类与IO流全面解析
一、Java IO体系概述
Java的IO(输入/输出)系统是Java语言中用于处理数据输入输出的核心API,它提供了丰富的类库来支持各种数据源和数据目标的读写操作。Java IO系统主要分为两大体系:
-
传统的IO系统(java.io包):基于流模型,提供同步阻塞式IO
-
NIO系统(java.nio包及其子包):基于通道和缓冲区,提供非阻塞IO和选择器
本文将重点讲解传统的java.io包中的File类和IO流体系。
1.1 IO流的分类
Java IO流可以从多个维度进行分类:
按数据流向分:
-
输入流:从数据源读取数据(InputStream/Reader)
-
输出流:向目标写入数据(OutputStream/Writer)
按处理数据类型分:
-
字节流:以字节为单位操作数据(InputStream/OutputStream)
-
字符流:以字符为单位操作数据(Reader/Writer)
按功能分:
-
节点流:直接从数据源读写数据
-
处理流:对现有流进行包装,提供额外功能
二、File类详解
File类是java.io包中代表文件和目录路径名的抽象表示,用于文件和目录的创建、删除、重命名、属性查询等操作。
2.1 File类核心构造方法
java
// 通过路径名创建File实例
File(String pathname)// 通过父路径和子路径创建
File(String parent, String child)// 通过父File对象和子路径创建
File(File parent, String child)// 通过URI创建
File(URI uri)
2.2 File类常用方法
2.2.1 文件和目录属性查询
java
boolean exists() // 文件或目录是否存在
boolean isFile() // 是否是文件
boolean isDirectory() // 是否是目录
boolean isHidden() // 是否是隐藏文件
long length() // 文件长度(字节数)
long lastModified() // 最后修改时间
String getName() // 获取文件名
String getPath() // 获取相对路径
String getAbsolutePath() // 获取绝对路径
String getCanonicalPath() throws IOException // 获取规范路径
2.2.2 文件和目录操作
java
boolean createNewFile() throws IOException // 创建新文件
boolean mkdir() // 创建单级目录
boolean mkdirs() // 创建多级目录
boolean delete() // 删除文件或空目录
boolean renameTo(File dest) // 重命名文件
2.2.3 目录内容查询
java
String[] list() // 返回目录中的文件和子目录名数组
File[] listFiles() // 返回目录中的文件和子目录File对象数组
String[] list(FilenameFilter filter) // 过滤后的文件名数组
File[] listFiles(FileFilter filter) // 过滤后的File对象数组
2.3 File类使用示例
2.3.1 文件基本操作
java
// 创建File对象
File file = new File("test.txt");// 检查文件是否存在
if (!file.exists()) {// 创建新文件boolean created = file.createNewFile();System.out.println("文件创建" + (created ? "成功" : "失败"));
}// 获取文件信息
System.out.println("文件名:" + file.getName());
System.out.println("绝对路径:" + file.getAbsolutePath());
System.out.println("文件大小:" + file.length() + "字节");
System.out.println("最后修改时间:" + new Date(file.lastModified()));// 删除文件
if (file.delete()) {System.out.println("文件删除成功");
}
2.3.2 目录操作示例
java
// 创建目录
File dir = new File("mydir");
if (!dir.exists()) {boolean created = dir.mkdir(); // 使用mkdirs()可创建多级目录System.out.println("目录创建" + (created ? "成功" : "失败"));
}// 列出目录内容
File[] files = dir.listFiles();
if (files != null) {System.out.println("目录内容:");for (File f : files) {System.out.println(f.getName() + (f.isDirectory() ? " [目录]" : " [文件]"));}
}// 递归遍历目录
public static void listAllFiles(File dir) {if (dir == null || !dir.exists()) return;if (dir.isFile()) {System.out.println(dir.getAbsolutePath());return;}for (File file : dir.listFiles()) {listAllFiles(file);}
}
三、字节流体系
字节流以字节为单位进行数据读写,适合处理二进制数据(如图片、音频、视频等)。
3.1 InputStream抽象类
InputStream是所有字节输入流的父类,定义了读取数据的基本方法:
java
// 读取一个字节,返回字节值(0-255),到达末尾返回-1
int read() throws IOException// 读取多个字节到字节数组b,返回实际读取的字节数
int read(byte[] b) throws IOException// 读取最多len个字节到数组b的off位置
int read(byte[] b, int off, int len) throws IOException// 跳过并丢弃n个字节数据
long skip(long n) throws IOException// 返回可读取的字节数估计值
int available() throws IOException// 关闭流释放资源
void close() throws IOException// 标记当前位置,readlimit指定标记有效范围
void mark(int readlimit)// 重置到最后一次mark的位置
void reset() throws IOException// 测试是否支持mark/reset
boolean markSupported()
3.1.1 FileInputStream
FileInputStream是文件字节输入流,用于从文件中读取数据。
java
// 构造方法
FileInputStream(String name) throws FileNotFoundException
FileInputStream(File file) throws FileNotFoundException
FileInputStream(FileDescriptor fdObj)// 使用示例
try (InputStream in = new FileInputStream("test.dat")) {byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = in.read(buffer)) != -1) {// 处理读取的数据}
} catch (IOException e) {e.printStackTrace();
}
3.1.2 ByteArrayInputStream
ByteArrayInputStream允许将字节数组作为输入源。
java
byte[] data = {65, 66, 67, 68, 69};
try (InputStream in = new ByteArrayInputStream(data)) {int c;while ((c = in.read()) != -1) {System.out.print((char)c); // 输出:ABCDE}
}
3.1.3 BufferedInputStream
BufferedInputStream是缓冲输入流,通过内部缓冲区减少实际IO操作次数,提高读取效率。
java
try (InputStream in = new BufferedInputStream(new FileInputStream("largefile.dat"))) {byte[] buffer = new byte[8192];int bytesRead;while ((bytesRead = in.read(buffer)) != -1) {// 处理数据}
}
3.2 OutputStream抽象类
OutputStream是所有字节输出流的父类,定义了写入数据的基本方法:
java
// 写入一个字节
void write(int b) throws IOException// 写入字节数组b
void write(byte[] b) throws IOException// 写入字节数组b中从off开始的len个字节
void write(byte[] b, int off, int len) throws IOException// 刷新输出流
void flush() throws IOException// 关闭流释放资源
void close() throws IOException
3.2.1 FileOutputStream
FileOutputStream是文件字节输出流,用于向文件写入数据。
java
// 构造方法
FileOutputStream(String name) throws FileNotFoundException
FileOutputStream(String name, boolean append) // append为true表示追加
FileOutputStream(File file)
FileOutputStream(File file, boolean append)
FileOutputStream(FileDescriptor fdObj)// 使用示例
try (OutputStream out = new FileOutputStream("output.dat")) {byte[] data = {72, 101, 108, 108, 111};out.write(data);out.flush(); // 确保数据写入磁盘
}
3.2.2 ByteArrayOutputStream
ByteArrayOutputStream将数据写入内部字节数组缓冲区。
java
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {out.write("Hello".getBytes());byte[] result = out.toByteArray(); // 获取写入的数据System.out.println(new String(result)); // 输出:Hello
}
3.2.3 BufferedOutputStream
BufferedOutputStream是缓冲输出流,通过缓冲减少实际IO操作次数。
java
try (OutputStream out = new BufferedOutputStream(new FileOutputStream("largefile.dat"))) {for (int i = 0; i < 100000; i++) {out.write(i); // 写入操作会被缓冲}out.flush(); // 确保所有缓冲数据写入磁盘
}
3.3 字节流使用示例
3.3.1 文件复制
java
public static void copyFile(String src, String dest) throws IOException {try (InputStream in = new FileInputStream(src);OutputStream out = new FileOutputStream(dest)) {byte[] buffer = new byte[8192];int bytesRead;while ((bytesRead = in.read(buffer)) != -1) {out.write(buffer, 0, bytesRead);}out.flush();}
}
3.3.2 对象序列化
Java对象序列化需要使用ObjectOutputStream和ObjectInputStream。
java
// 序列化对象到文件
class Person implements Serializable {private String name;private int age;// 构造方法、getter、setter等
}Person person = new Person("张三", 25);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.dat"))) {out.writeObject(person);
}// 从文件反序列化对象
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.dat"))) {Person p = (Person) in.readObject();System.out.println(p.getName() + ", " + p.getAge());
}
四、字符流体系
字符流以字符为单位进行数据读写,适合处理文本数据,能够正确处理字符编码问题。
4.1 Reader抽象类
Reader是所有字符输入流的父类,定义了读取字符数据的基本方法:
java
// 读取单个字符,返回字符值,到达末尾返回-1
int read() throws IOException// 读取字符到字符数组cbuf
int read(char[] cbuf) throws IOException// 读取最多len个字符到数组cbuf的off位置
int read(char[] cbuf, int off, int len) throws IOException// 跳过n个字符
long skip(long n) throws IOException// 是否准备好读取
boolean ready() throws IOException// 关闭流
void close() throws IOException// 标记/重置相关方法
void mark(int readAheadLimit) throws IOException
void reset() throws IOException
boolean markSupported()
4.1.1 FileReader
FileReader是文件字符输入流,用于从文件读取文本数据。
java
// 构造方法
FileReader(String fileName) throws FileNotFoundException
FileReader(File file) throws FileNotFoundException
FileReader(FileDescriptor fd)// 使用示例
try (Reader reader = new FileReader("text.txt")) {char[] buffer = new char[1024];int charsRead;while ((charsRead = reader.read(buffer)) != -1) {System.out.print(new String(buffer, 0, charsRead));}
}
4.1.2 BufferedReader
BufferedReader是缓冲字符输入流,提供行读取功能。
java
try (BufferedReader reader = new BufferedReader(new FileReader("text.txt"))) {String line;while ((line = reader.readLine()) != null) { // 按行读取System.out.println(line);}
}
4.1.3 InputStreamReader
InputStreamReader是字节流到字符流的桥梁,可以指定字符编码。
java
try (InputStreamReader reader = new InputStreamReader(new FileInputStream("text.txt"), "UTF-8")) {char[] buffer = new char[1024];int charsRead;while ((charsRead = reader.read(buffer)) != -1) {System.out.print(new String(buffer, 0, charsRead));}
}
4.2 Writer抽象类
Writer是所有字符输出流的父类,定义了写入字符数据的基本方法:
java
// 写入单个字符
void write(int c) throws IOException// 写入字符数组
void write(char[] cbuf) throws IOException// 写入字符数组的部分内容
void write(char[] cbuf, int off, int len) throws IOException// 写入字符串
void write(String str) throws IOException// 写入字符串的部分内容
void write(String str, int off, int len) throws IOException// 追加字符序列
Writer append(CharSequence csq) throws IOException// 追加字符序列的部分内容
Writer append(CharSequence csq, int start, int end) throws IOException// 追加单个字符
Writer append(char c) throws IOException// 刷新流
void flush() throws IOException// 关闭流
void close() throws IOException
4.2.1 FileWriter
FileWriter是文件字符输出流,用于向文件写入文本数据。
java
// 构造方法
FileWriter(String fileName) throws IOException
FileWriter(String fileName, boolean append) // append为true表示追加
FileWriter(File file) throws IOException
FileWriter(File file, boolean append)
FileWriter(FileDescriptor fd)// 使用示例
try (Writer writer = new FileWriter("output.txt")) {writer.write("Hello, World!\n");writer.append("第二行内容");writer.flush();
}
4.2.2 BufferedWriter
BufferedWriter是缓冲字符输出流,提供高效的写入功能。
java
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {writer.write("第一行");writer.newLine(); // 写入换行符writer.write("第二行");
}
4.2.3 OutputStreamWriter
OutputStreamWriter是字符流到字节流的桥梁,可以指定字符编码。
java
try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")) {writer.write("中文字符测试");
}
4.3 字符流使用示例
4.3.1 文本文件复制
java
public static void copyTextFile(String src, String dest) throws IOException {try (BufferedReader reader = new BufferedReader(new FileReader(src));BufferedWriter writer = new BufferedWriter(new FileWriter(dest))) {String line;while ((line = reader.readLine()) != null) {writer.write(line);writer.newLine();}writer.flush();}
}
4.3.2 文件编码转换
java
public static void convertEncoding(String srcFile, String srcEncoding,String destFile, String destEncoding) throws IOException {try (InputStreamReader reader = new InputStreamReader(new FileInputStream(srcFile), srcEncoding);OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(destFile), destEncoding)) {char[] buffer = new char[1024];int charsRead;while ((charsRead = reader.read(buffer)) != -1) {writer.write(buffer, 0, charsRead);}writer.flush();}
}
五、IO流的高级应用
5.1 随机访问文件
RandomAccessFile类支持"随机访问"方式读写文件,可以任意移动文件指针。
java
// 构造方法
RandomAccessFile(String name, String mode) // mode: "r"只读,"rw"读写等
RandomAccessFile(File file, String mode)// 常用方法
long getFilePointer() // 返回当前指针位置
void seek(long pos) // 设置指针位置
int read() // 读取一个字节
void write(int b) // 写入一个字节
int read(byte[] b) // 读取到字节数组
void write(byte[] b) // 写入字节数组
long length() // 返回文件长度
void setLength(long newLength) // 设置文件长度// 使用示例:在文件中间插入数据
try (RandomAccessFile raf = new RandomAccessFile("data.txt", "rw")) {// 将指针移动到文件中间raf.seek(raf.length() / 2);// 保存指针后的内容byte[] remaining = new byte[(int)(raf.length() - raf.getFilePointer())];raf.readFully(remaining);// 移动指针回原位置raf.seek(raf.length() / 2);// 写入新内容raf.write("INSERTED TEXT".getBytes());// 追加之前保存的内容raf.write(remaining);
}
5.2 标准IO重定向
System类提供了三个静态字段用于标准IO操作:
java
System.in // 标准输入流(InputStream)
System.out // 标准输出流(PrintStream)
System.err // 标准错误流(PrintStream)// 重定向示例
try {// 将标准输出重定向到文件PrintStream out = new PrintStream(new FileOutputStream("output.log"));System.setOut(out);// 将标准错误重定向到同一文件System.setErr(out);// 这些输出将被重定向到文件System.out.println("标准输出消息");System.err.println("错误消息");// 恢复标准输出System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out)));
} catch (FileNotFoundException e) {e.printStackTrace();
}
5.3 进程IO
通过Process类可以与操作系统进程交互,获取其输入输出流。
java
try {// 启动进程Process process = Runtime.getRuntime().exec("ping www.baidu.com");// 获取进程输入流(进程的输出)try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {String line;while ((line = reader.readLine()) != null) {System.out.println(line);}}// 等待进程结束int exitCode = process.waitFor();System.out.println("进程退出码:" + exitCode);
} catch (IOException | InterruptedException e) {e.printStackTrace();
}
六、NIO简介
虽然本文主要讨论传统IO,但有必要简要介绍NIO(New IO)的基本概念,以便读者了解Java IO的发展。
6.1 NIO核心组件
-
通道(Channel):类似于流,但可以双向传输数据
-
FileChannel
-
SocketChannel
-
ServerSocketChannel
-
DatagramChannel
-
-
缓冲区(Buffer):数据容器,用于与通道交互
-
ByteBuffer
-
CharBuffer
-
IntBuffer等
-
-
选择器(Selector):用于多路复用IO
6.2 NIO与传统IO对比
特性 | 传统IO | NIO |
---|---|---|
数据流 | 单向流 | 双向通道 |
缓冲 | 部分支持(BufferedXX) | 必须使用缓冲区 |
阻塞模式 | 阻塞IO | 支持非阻塞IO |
多路复用 | 不支持 | 支持(Selector) |
性能 | 一般 | 更高(减少系统调用) |
6.3 简单NIO示例
java
// 使用FileChannel复制文件
try (FileInputStream fis = new FileInputStream("source.txt");FileOutputStream fos = new FileOutputStream("dest.txt");FileChannel source = fis.getChannel();FileChannel dest = fos.getChannel()) {ByteBuffer buffer = ByteBuffer.allocate(1024);while (source.read(buffer) != -1) {buffer.flip(); // 切换为读模式dest.write(buffer);buffer.clear(); // 清空缓冲区}
}
七、IO性能优化
7.1 选择合适的IO类
-
处理文本数据:优先使用字符流
-
处理二进制数据:使用字节流
-
频繁读写:使用缓冲流
-
随机访问:使用RandomAccessFile
7.2 合理设置缓冲区大小
缓冲区大小对IO性能有显著影响,一般建议:
-
磁盘IO:8KB-32KB
-
网络IO:1KB-8KB
java
// 设置缓冲区大小示例
int bufferSize = 32 * 1024; // 32KB
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("largefile.dat"), bufferSize)) {// 读取操作
}
7.3 使用内存映射文件
对于大文件操作,可以使用内存映射文件提高性能。
java
try (RandomAccessFile raf = new RandomAccessFile("hugefile.dat", "rw");FileChannel channel = raf.getChannel()) {// 将文件映射到内存MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());// 直接操作缓冲区while (buffer.hasRemaining()) {byte b = buffer.get();// 处理数据}
}
7.4 减少IO操作次数
-
批量读写代替单字节/字符操作
-
使用BufferedXX类减少实际IO次数
-
合理安排读写顺序(顺序读写通常比随机访问快)
八、常见问题与解决方案
8.1 文件编码问题
问题:读取文本文件时出现乱码
解决方案:
-
明确知道文件编码时,使用InputStreamReader指定编码
-
不确定编码时,可以使用juniversalchardet等库检测编码
java
// 指定UTF-8编码读取文件
try (Reader reader = new InputStreamReader(new FileInputStream("text.txt"), "UTF-8")) {// 读取操作
}
8.2 资源泄露问题
问题:忘记关闭流导致资源泄露
解决方案:
-
使用try-with-resources语法自动关闭资源
-
在finally块中手动关闭
java
// try-with-resources示例(推荐)
try (InputStream in = new FileInputStream("file.txt")) {// 使用流
} catch (IOException e) {e.printStackTrace();
}// 传统方式(不推荐)
InputStream in = null;
try {in = new FileInputStream("file.txt");// 使用流
} catch (IOException e) {e.printStackTrace();
} finally {if (in != null) {try {in.close();} catch (IOException e) {e.printStackTrace();}}
}
8.3 大文件处理问题
问题:处理大文件时内存不足
解决方案:
-
使用缓冲流分块处理
-
使用NIO的FileChannel
-
使用内存映射文件
java
// 分块处理大文件示例
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("largefile.dat"))) {byte[] buffer = new byte[8192]; // 8KB缓冲区int bytesRead;while ((bytesRead = in.read(buffer)) != -1) {// 处理每个块}
}
8.4 文件锁定问题
问题:多线程/多进程同时访问文件导致冲突
解决方案:
-
使用FileLock实现文件锁定
-
使用同步机制控制访问
java
try (RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");FileChannel channel = raf.getChannel()) {// 获取独占锁FileLock lock = channel.lock();try {// 执行文件操作} finally {lock.release();}
}
九、总结
Java的File类和IO流体系提供了强大而灵活的文件操作和IO处理能力。通过本文的学习,我们应该掌握:
-
File类:文件和目录的创建、删除、属性查询等操作
-
字节流:InputStream/OutputStream体系及其实现类,适合处理二进制数据
-
字符流:Reader/Writer体系及其实现类,适合处理文本数据
-
高级特性:随机访问文件、标准IO重定向、进程IO等
-
性能优化:缓冲使用、NIO简介、内存映射文件等技术
-
常见问题:编码问题、资源泄露、大文件处理等解决方案
在实际开发中,应根据具体需求选择合适的IO类和优化策略,同时注意资源管理和异常处理,以构建健壮高效的IO处理代码。