Java IO——BIO
本文最后更新于:6 个月前
引言
Java 的 IO(输入/输出)系统在软件开发中扮演着关键角色,几乎所有与数据交互的场景都离不开 IO 操作。无论是读取本地文件、写入网络流、处理大文件还是进行多线程间通信,Java 都提供了功能丰富且易于扩展的 IO 类库。本篇文章将从最基础的 File 类开始,逐步介绍 Java IO 中常见的流类别、用法以及 Java IO 的设计模式,帮助读者在学习与实践中更好地理解和掌握 Java 的输入输出机制。
File 类
概述
抽象路径名表示:
File类封装了文件或目录的路径,可以使用相对路径或绝对路径来创建对象。它既可以代表一个具体的文件,也可以代表一个目录。与文件系统交互:
File类提供的方法允许程序查询文件是否存在、判断文件类型、创建或删除文件和目录等操作,但并不直接读写文件内容。跨平台性:
File类封装了不同操作系统的文件系统差异,例如路径分隔符问题,通过 API 屏蔽了平台细节。
常用方法
构造方法
File(String pathname):根据路径名字符串创建 File 对象。File(String parent, String child):根据父目录和子路径创建 File 对象。File(File parent, String child):根据父 File 对象和子路径创建 File 对象。
属性查询方法
boolean exists():判断文件或目录是否存在。boolean isFile():判断是否为文件。boolean isDirectory():判断是否为目录。String getName():返回文件或目录的名称(不包含路径)。String getPath():返回创建 File 对象时使用的路径字符串。String getAbsolutePath():返回文件或目录的绝对路径。long length():返回文件的大小(以字节为单位);对于目录,该方法可能不返回有意义的结* 果。long lastModified():返回文件或目录最后修改的时间(以毫秒计)。
文件和目录操作方法
boolean createNewFile():在指定路径创建一个新的空文件,如果文件已经存在则返回 false。boolean delete():删除文件或目录(目录需为空)。boolean renameTo(File dest):将文件或目录重命名为目标路径。boolean mkdir():创建单级目录;如果父目录不存在则创建失败。boolean mkdirs():创建多级目录,即使父目录不存在也会一并创建。
列出目录内容的方法
String[] list():返回目录中所有文件和目录的名称数组。File[] listFiles():返回目录中所有文件和目录的 File 对象数组,便于进一步操作。
常用场景
- 文件和目录的存在性检查:使用
exists()判断一个文件是否存在,避免在读写操作时出现异常。 - 创建新文件/目录:使用
createNewFile()、mkdir()或mkdirs()创建所需文件或目录。 - 文件重命名和删除:使用
renameTo()实现文件重命名,delete()实现文件或空目录删除。 - 遍历目录内容:通过
list()或listFiles()方法遍历目录中的所有文件,常用于文件管理器或批处理程序。
代码示例
1 | |
注意事项
- 不直接操作文件内容:
File类主要用于表示路径和管理文件属性,读写文件内容需要结合字节流或字符流(如FileInputStream、FileReader等)。 - 平台差异:尽量使用 API 提供的分隔符,如
File.separator,以保证跨平台兼容性。 - 安全性问题:在操作文件时应注意权限问题,确保程序有足够的权限读写或删除文件,避免出现
SecurityException。
IO 流概述及分类
Java IO(Input/Output)是 Java 提供的一套处理输入和输出操作的 API,广泛用于文件操作、网络通信、数据流处理等场景。
流
基本思想:
Java IO 将数据的读写抽象为 “流” 的概念。一个流代表一个数据通道,数据可以顺序地从流中读取或写入。
链式操作与装饰器模式:
很多 IO 类设计遵循装饰器模式。例如,FileInputStream 可以被包装在 BufferedInputStream 中,后者通过内部缓冲区来提高读取性能。这种设计允许开发者通过 “链式” 组合来增强流的功能,而不改变底层流的接口或实现。
分类
同步/异步
- 同步 IO(Synchronous IO):调用者在发起 IO 操作后,必须等待操作完成才能继续执行。例如,Java 传统的
java.io包中的 IO 操作大多数是同步的,线程在读写数据时会阻塞。 - 异步 IO(Asynchronous IO):调用者发起 IO 操作后,不需要等待操作完成,而是由系统通知(如回调、事件驱动)操作完成的结果。Java
NIO(New IO)引入了异步 IO 机制,例如AsynchronousFileChannel。
阻塞/非阻塞
- 阻塞 IO(Blocking IO):调用者在 IO 操作完成之前会一直被阻塞,无法执行其他任务。例如,
InputStream.read()需要等待数据到达,线程无法继续执行。 - 非阻塞 IO(Non-Blocking IO):调用者可以在数据未准备好时立即返回,而不是一直等待。例如,Java
NIO提供的SelectableChannel允许非阻塞模式,线程可以在多个 IO 操作之间切换。
按数据类型(字节流/字符流)
Java IO 主要分为 字节流(Byte Streams) 和字符流(Character Streams):
- 字节流(Byte Streams):用于处理二进制数据,例如图片、音频、视频、文件流等。字节流的基类是:
- 输入流:
InputStream - 输出流:
OutputStream
- 输入流:
- 字符流(Character Streams):用于处理文本数据,支持字符编码转换,适用于读取和写入文本文件。字符流的基类是:
- 输入流:
Reader - 输出流:
Writer
- 输入流:
对比
| 类型 | 基类 | 处理单位 | 适用场景 |
|---|---|---|---|
| 字节流 | InputStream / OutputStream |
8-bit 字节 | 处理二进制数据(如图片、音视频) |
| 字符流 | Reader / Writer |
16-bit 字符 | 处理文本数据(如 .txt 文件) |
缓冲流
IO 操作通常涉及系统调用,每次调用都会带来一定的开销。通过使用缓冲流(如 BufferedInputStream、BufferedReader 等),可以将多次小规模的读写合并成一次大规模的操作,从而显著提升性能。
默认缓冲区一般为 8KB,但在特定场景下(如大文件的顺序读取),适当调整缓冲区大小可能会带来更好的性能表现。
字节流
输入流与输出流
Java IO 提供了两个字节流的基础类:
InputStream(输入流):用于从数据源读取字节数据。OutputStream(输出流):用于向数据目标写入字节数据。
InputStream
InputStream 是所有字节输入流的父类,提供了一系列从数据源(如文件、字节数组、网络)读取字节数据的方法。
核心方法:
int read():读取单个字节。int read(byte[] b):读取多个字节到数组中。int read(byte[] b, int off, int len):从 off 开始读取 len 个字节。long skip(long n):跳过 n 个字节。int available():返回可读取的字节数。void close():关闭流,释放资源。void mark(int readlimit):标记此输入流中的当前位置。void reset():将此流重新定位到上次在此输入流上调用标记方法时的位置。
OutputStream
OutputStream 是所有字节输出流的父类,提供了一系列向目标(如文件、字节数组、网络)写入字节数据的方法。
核心方法:
void write(int b):写入单个字节。void write(byte[] b):写入字节数组。void write(byte[] b, int off, int len):从数组的 off 位置开始写入 len 个字节。void flush():刷新输出流,确保数据写入。void close():关闭流,释放资源。
字节文件流
用于从文件读取/写入字节数据。
字节文件输入流
字节文件输入流 FileInputStream,用于从文件中读取数据。
构造方法:
FileInputStream(File file):通过 File 对象创建输入流。FileInputStream(String name):通过文件路径创建输入流。
示例:使用 FileInputStream 读取文件。
1 | |
特点:
- 适用于读取二进制文件(如
.jpg、.mp3)或文本文件(但不推荐,可能会出现乱码问题,用Reader处理文本更合适)。 - 逐字节读取,效率较低,通常与缓冲流(
BufferedInputStream)一起使用提升性能。
字节文件输出流
字节文件输出流 FileOutputStream,用于向文件中写入数据。
构造方法:
FileOutputStream(File file):通过 File 对象创建输出流。FileOutputStream(String name):通过文件路径创建输出流。FileOutputStream(File file, boolean append):是否以追加模式写入。FileOutputStream(String name, boolean append):通过路径创建并控制是否追加。
示例:使用 FileOutputStream 写入文件。
1 | |
特点:
- 适用于写入二进制文件,如图片、音频等。
flush()方法可以确保数据立即写入,而不是缓存到缓冲区。
字节数组流
字节数组流无需涉及磁盘文件,适合小数据量的读取操作。
字节数组输入流
字节数组输入流 ByteArrayInputStream 允许将字节数组作为输入源,提供类似 InputStream 的读取方法。
构造方法:
ByteArrayInputStream(byte[] buf):使用字节数组创建输入流。ByteArrayInputStream(byte[] buf, int off, int len):指定偏移量和长度创建输入流。
示例:使用 ByteArrayInputStream 读取内存中的数据。
1 | |
特点:
- 适用于处理小数据量的内存操作,避免磁盘 IO。
- 可用于数据缓存、流转换等场景。
字节数组输出流
字节数组输出流 ByteArrayOutputStream 允许将字节数据写入到内存中的字节数组,而不是直接写入文件。
构造方法:
ByteArrayOutputStream():默认初始缓冲区大小 32。ByteArrayOutputStream(int size):指定初始缓冲区大小。
示例:使用 ByteArrayOutputStream 生成字节数组。
1 | |
特点:
- 适用于动态构造字节数组,避免频繁创建
byte[]数组。 - 适合数据缓冲、数据拼接的场景。
字节缓冲流
字节缓冲流用于提升 IO 读写效率,通过内部缓存方式减少底层 IO 访问次数。
字节缓冲输入流
字节缓冲输入流 BufferedInputStream 为输入流提供缓冲,减少 read() 调用次数,提高读取性能。
构造方法:
BufferedInputStream(InputStream in):创建默认缓冲输入流(8KB)。BufferedInputStream(InputStream in, int size):指定缓冲区大小创建缓冲输入流。
示例:使用 BufferedInputStream 读取文件。
1 | |
特点:
- 默认缓冲区大小为 8KB(可自定义)。
- 适用于大文件读取,避免频繁调用底层 IO 操作。
字节缓冲输出流
字节缓冲输出流 BufferedOutputStream 为输出流提供缓冲,减少 write() 的磁盘 IO 频率,提高写入性能。
构造方法:
BufferedOutputStream(OutputStream out):创建默认缓冲输出流(8KB)。BufferedOutputStream(OutputStream out, int size):指定缓冲区大小创建缓冲输出流。
示例:使用 BufferedOutputStream 写入文件。
1 | |
特点:
- 适用于频繁写入的场景,例如日志系统、流式写入文件等。
Closeable 接口与 try-with-resources 语法
在 Java IO 操作中,打开的资源(如文件流、数据库连接、网络流等)需要在使用完后正确关闭,以释放系统资源,防止资源泄露。
Java 7 之前,我们通常在 finally 代码块中调用 close() 方法来关闭流。但 Java 7 引入了 try-with-resources 语法(基于 AutoCloseable 接口),让资源管理更简单、代码更简洁。
Closeable 接口
Closeable 是 Java IO 包 (java.io) 提供的资源管理接口,它只有一个方法:
1 | |
作用:
- 使得实现此接口的类可以被安全地关闭,例如
FileInputStream、BufferedReader等。 close()方法释放资源,防止资源泄露。
继承关系:
1 | |
它继承了 AutoCloseable(Java 7 引入),这使得 Closeable 也可以在 try-with-resources 中使用。
try-with-resources 语法
传统方式关闭资源
在 Java 7 之前,我们需要手动在 finally 代码块中调用 close() 方法:
1 | |
问题:
- 代码冗长,需要手动检查
null,防止NullPointerException。 - 容易出错,如果
try代码块抛出异常,finally仍需执行关闭逻辑。
自动关闭资源
Java 7 引入了 try-with-resources,资源在 try() 里声明,只需要捕获异常,而不需要在 finally 块中手动调用 close() 方法,即可自动关闭资源:
1 | |
优势:
- 代码简洁,无需手动
close()。 - 异常安全,即使
try代码块抛出异常,资源仍会自动关闭。 - 多个资源管理,
try语法可以管理多个Closeable资源。
对比
Java 7 引入 AutoCloseable,它是 Closeable 的超接口:
1 | |
| 接口 | 主要方法 | 抛出的异常 | 适用范围 |
|---|---|---|---|
Closeable |
void close() throws IOException |
IOException |
主要用于 IO 资源,如 InputStream |
AutoCloseable |
void close() throws Exception |
Exception(更广泛) |
适用于所有需要关闭的资源(数据库连接、线程池等) |
Closeable主要用于 IO 相关的资源(InputStream、OutputStream等)。AutoCloseable适用于更广泛的需要关闭资源连接的场景(Connection、ExecutorService等)。
多资源同时关闭
如果有多个资源,可以在 try 语句中同时声明,在方法结束后这些资源的 close() 方法都会被调用,如:
1 | |
工作原理:
try语句块执行完毕后,fis和fos按声明顺序关闭。- 即使发生异常,资源仍然会被关闭。
自定义 Closeable 资源
try-with-resources 适用于所有实现 Closeable 或 AutoCloseable 的类。
我们可以自定义一个可自动关闭的资源:
1 | |
运行程序,控制台打印如下:
1 | |
即使 use() 方法抛出异常,close() 也会被自动调用。
字符流
Java 字符流(Character Streams)用于处理文本数据,以 16-bit(char)为单位进行数据读写,适用于文本文件(如 .txt、.xml、.csv)的操作。字符流支持字符编码转换,相比字节流(Byte Streams)更适合处理 Unicode 及多语言文本。
输入流与输出流
Java 字符流的两个基类:
Reader(字符输入流):用于从数据目标读取字符数据。Writer(字符输出流):用于向数据目标写入字符数据。
Reader
Reader 是所有字符输入流的父类,提供了读取字符的基础方法:
int read():读取单个字符,返回字符的 Unicode 值(int),如果到达流末尾返回 -1。int read(char[] cbuf):读取多个字符到字符数组 cbuf 中。int read(char[] cbuf, int off, int len):从 off 位置开始读取 len 个字符。long skip(long n):跳过 n 个字节。boolean ready():判断流是否可读取。boolean markSupported():判断是否支持 mark 操作。void mark(int readAheadLimit):标记流中的当前位置。void reset():将此流重新定位到上次在此输入流上调用标记方法时的位置。void close():关闭流,释放资源。
Writer
Writer 是所有字符输出流的父类,提供了写入字符的基础方法:
void write(int c):写入单个字符。void write(char[] cbuf):写入字符数组。void write(char[] cbuf, int off, int len):从字符数组的 off 位置开始写入 len 个字符。void write(String str):写入字符串。void write(String str, int off, int len):从字符数组的 off 位置开始写入 len 个字符。Writer append(CharSequence csq):将指定的字符序列附加到此写入程序。Writer append(CharSequence csq, int start, int end):将指定字符序列的子序列[start, end)附加到此写入程序 。Writer append(char c)//将指定字符附加到此写入程序。void flush():刷新流,将缓冲区内容写入目标。void close():关闭流,释放资源。
字符文件流
用于从文件读取/写入字符数据。
字符文件输入流
字符文件输入流 FileReader,用于从文件中读取数据。
构造方法:
FileReader(File file):通过 File 对象创建输入流。FileReader(String name):通过文件路径创建输入流。
示例:使用 FileReader 读取文本文件。
1 | |
特点:
- 适用于文本文件读取(不适合二进制文件)。
- 默认使用平台默认编码(如 UTF-8、GBK),可使用
InputStreamReader指定编码。
字符文件输出流
字符文件输出流 FileWriter,用于向文件中写入数据。
构造方法:
FileWriter(File file):通过 File 对象创建输出流。FileWriter(String name):通过文件路径创建输出流。FileWriter(File file, boolean append):是否以追加模式写入。FileWriter(String name, boolean append):通过路径创建并控制是否追加。
示例:使用 FileWriter 写入文本文件。
1 | |
特点:
- 适用于文本数据写入。
flush()确保数据立即写入,而不是缓存在内存。
字符数组流
适用于内存中的字符数组读写,避免磁盘 IO,适合临时数据存储。
字符数组输入流
字符数组输入流 CharArrayReader,允许将字符数组作为输入源。
构造方法:
CharArrayReader(char[] buf):使用字符数组创建输入流。CharArrayReader(char[] buf, int off, int len):指定偏移量和长度创建输入流。
示例:使用 CharArrayReader 读取字符数组。
1 | |
特点:
- 适用于内存中的字符数据,无需文件 IO。
字符数组输出流
字符数组输出流 CharArrayWriter 允许写入字符数据到内存中的字符数组,类似 ByteArrayOutputStream。
构造方法:
1 | |
示例:使用 CharArrayWriter 生成字符数组。
1 | |
特点:
- 动态存储字符数据,适用于字符串拼接、缓存。
字符缓冲流
字符缓冲流用于提升 IO 读写效率,通过内部缓存方式减少底层 IO 访问次数。
字符缓冲输入流
字符缓冲输入流 BufferedReader,为输入流提供缓冲,提高文本读取性能,并支持按行读取。
构造方法:
BufferedReader(Reader in):创建默认缓冲输入流(8KB)。BufferedReader(Reader in, int size):指定缓冲区大小创建缓冲输入流。
示例:使用 BufferedReader 读取文本文件。
1 | |
特点:
- 默认缓冲区大小为 8KB(可自定义)。
readLine()逐行读取,适用于读取大文件。
字符缓冲输出流
BufferedWriter 为输出流提供缓冲,减少 write() 的 IO 频率,提高写入性能。
构造方法:
BufferedWriter(Writer out):创建默认缓冲输出流(8KB)。BufferedWriter(Writer out, int size):指定缓冲区大小创建缓冲输出流。
示例:使用 BufferedWriter 写入文本文件。
1 | |
特点:
newLine()方法可写入系统换行符(适用于跨平台换行)。- 适用于频繁写入的场景(如日志系统、流式写入文件)。
数据流
DataInputStream 和 DataOutputStream 允许以二进制格式读写 Java 的基本数据类型(如 int、double、boolean)和字符串(UTF-8 编码),保证跨平台兼容性。
数据输出流
数据输出流 DataOutputStream 以二进制格式写入基本数据类型,避免因字符编码或换行符差异造成的数据不兼容问题。常用于文件存储、网络传输,适合序列化数据写入。
常用方法:
DataOutputStream(OutputStream out):传入一个基础的字节输出流构造。void writeInt(int v):写入 int 类型数据。void writeDouble(double v):写入 double 类型数据。void writeBoolean(boolean v):写入 boolean 类型数据。void writeUTF(String str):以 UTF-8 格式写入字符串。void flush():刷新流。void close():关闭流。
示例:写入二进制数据。
1 | |
特点:
writeInt()、writeDouble()直接存储二进制数据,而不是字符串表示的数字,节省存储空间。writeUTF()以 UTF-8 编码存储字符串,保证跨平台兼容性。
数据输入流
数据输入流 DataInputStream 以二进制格式读取数据,确保数据类型一致性。必须与 DataOutputStream 配合使用,否则读取时会发生错误。
常用方法:
DataInputStream(InputStream in):传入一个基础的字节输入流构造。int readInt():读取 int 类型数据。double readDouble():读取 double 类型数据。boolean readBoolean():读取 boolean 类型数据。String readUTF():读取 UTF-8 编码的字符串。void close():关闭流。
示例:读取二进制数据。
1 | |
运行程序,控制台打印如下:
1 | |
特点:
- 读取数据的顺序必须与写入顺序一致,否则可能发生数据格式错误。
打印流
PrintStream 和 PrintWriter 主要用于格式化输出,支持自动 flush(),可输出到控制台、文件、字节流。
打印字节流
打印字节流 PrintStream 继承自 OutputStream,属于字节流,主要用于打印格式化数据到控制台或文件,该流打印时不会抛出 IOException,错误时只会设置内部错误标志,适合日志系统。
常用方法:
PrintStream(OutputStream out):包装字节输出流。PrintStream(String fileName):直接写入文件。PrintStream(OutputStream out, boolean autoFlush):是否自动刷新。void print(String s):输出字符串(不换行)。void println(String s):输出字符串(换行)。void printf(String format, Object... args):格式化输出。boolean checkError():检查流是否有错误。void close():关闭流。
示例:写入文件。
1 | |
特点:
println()自动换行,适合写日志。printf()格式化输出,类似 C 语言的printf()。
打印字符流
打印字符流 PrintWriter 继承自 Writer,属于字符流,适用于文本文件或网络输出。该流提供 print()、println() 方法,打印不会抛出 IOException,错误时需用 checkError() 检查。
常用方法:
PrintWriter(Writer out):包装字符输出流。PrintWriter(String fileName):直接写入文件。PrintWriter(OutputStream out):包装字节流(适用于控制台输出)。PrintWriter(Writer out, boolean autoFlush):是否自动刷新。void print(String s):输出字符串(不换行)。void println(String s):输出字符串(换行)。void printf(String format, Object... args):格式化输出。boolean checkError():检查流是否有错误。void close():关闭流。
示例:写入文本文件。
1 | |
特点:
- 适用于文本数据,不会自动转换为二进制。
- 支持格式化,比
BufferedWriter更灵活。
对象流
Java 对象流(Object Streams)用于序列化(Serialization) 和反序列化(Deserialization),使 Java 对象能够以二进制格式进行存储或传输,例如写入文件、通过网络传输等。对象流基于 ObjectInputStream 和 ObjectOutputStream 实现。
输出流
对象输出流 ObjectOutputStreams 将对象转换为二进制数据 并写入文件或网络流,适用于持久化 Java 对象或远程通信(如 RMI)。
常用方法:
ObjectOutputStream(OutputStream out):需要包装字节流,如 FileOutputStream。void writeObject(Object obj):将对象写入流。void flush():刷新流,确保数据写入。void close():关闭流。
输入流
对象输入流 ObjectInputStream 从二进制流中读取对象 并还原为 Java 实例,需要和 ObjectOutputStream 配合使用,保证数据格式一致。
常用方法:
ObjectInputStream(InputStream in):需要包装字节流,如 FileInputStream。Object readObject():读取对象(需要强制转换)。void close():关闭流。
示例:使用对象流进行序列化和反序列化。
1 | |
运行程序,控制台打印如下:
1 | |
序列化机制
Serializable 接口
作用:
- 允许对象序列化,使其能够写入
ObjectOutputStream并恢复到ObjectInputStream。 - 这是一个标记接口(Marker Interface),没有任何方法,Java 通过反射检查类是否实现了
Serializable。
定义方式:
1 | |
serialVersionUID 的作用:
- 用于唯一标识类的版本,确保反序列化时版本兼容。
- 如果类发生改变(如添加字段),但
serialVersionUID不变,仍然可以反序列化。 - 如果
serialVersionUID变更或未定义,默认会基于类结构计算,类修改后可能导致InvalidClassException。
transient 关键字
作用:
- 被此关键字标识的变量不参与序列化,防止敏感数据(如密码)或非必要数据(如缓存)被写入文件或网络。
transient修饰的字段不会被writeObject()保存,在readObject()时该字段会使用默认值(如null或0)。
示例:
1 | |
运行程序,控制台打印如下:
1 | |
password 字段在反序列化后为 null,因为 transient 使其未被存储。
Java 序列化过程
- 序列化(Serialization):
ObjectOutputStream.writeObject(obj)将对象转换为二进制格式,并存储到文件或发送到网络。 - 反序列化(Deserialization):
ObjectInputStream.readObject()从文件或网络中读取二进制数据,并恢复为原始对象。
限制
- 必须实现
Serializable,否则writeObject()会抛NotSerializableException。 static变量属于类而非实例,因此不会序列化,不会被存储。transient修饰的变量不会序列化,需手动初始化,或在readObject()方法中恢复值。
自定义序列化
如果类需要特殊的序列化逻辑,可以自定义 writeObject() 和 readObject():
1 | |
defaultWriteObject()正常处理需要序列化的字段。writeUTF()手动处理transient字段password。
管道流
Java 管道流(Piped Streams) 主要用于线程间通信,它们允许一个线程写入数据,另一个线程从管道中读取数据,从而实现数据的传递。Java 提供了两种管道流:
- 字节流管道:
PipedInputStream/PipedOutputStream - 字符流管道:
PipedReader/PipedWriter
这些流通常用于生产者-消费者模式,一个线程写入数据,另一个线程读取数据。它们不适用于同一线程中,必须在不同的线程间进行通信。
字节管道流
字节输出管道
字节输出管道 PipedOutputStream 允许将数据写入管道,数据可以被 PipedInputStream 读取。适用于字节数据传输,例如文件数据、二进制流等。
常用方法:
PipedOutputStream():默认构造方法。PipedOutputStream(PipedInputStream snk):连接到指定的管道输入流。void connect(PipedInputStream snk):连接到指定的管道输入流。void write(int b):写入单个字节。void write(byte[] b):写入字节数组。void write(byte[] b, int off, int len):从偏移量 off 开始写入 len 个字节。void flush():刷新流。void close():关闭流。
字节输入管道
字节输入管道 PipedInputStream 允许从管道读取数据,数据必须由 PipedOutputStream 写入,适用于字节数据的读取,不能直接使用在主线程,而需要和 PipedOutputStream 连接。
常用方法:
PipedInputStream():默认构造方法。PipedInputStream(PipedOutputStream src):连接到指定的管道输出流。void connect(PipedOutputStream src):连接到指定的管道输出流。int read():读取单个字节。int read(byte[] b):读取多个字节到数组。int read(byte[] b, int off, int len):从偏移量 off 开始读取 len 个字节。void close():关闭流。
线程通信示例
1 | |
运行程序,控制台打印如下:
1 | |
特点
- 用于线程间通信,生产者写入数据,消费者读取数据。
- 必须在不同线程间使用,否则会导致死锁。
- 默认缓冲区大小为 1024 字节,数据超过后写入线程会阻塞,直到有数据被读取。
字符管道流
字符输出管道
字符输出管道 PipedWriter 允许写入字符数据 到 PipedReader 进行读取,适用于文本数据传输。
常用方法:
PipedWriter():默认构造方法。PipedWriter(PipedReader snk):连接到指定的管道输入流。void connect(PipedReader snk):连接到指定的管道输入流。void write(int c):写入单个字符。void write(char[] cbuf):写入字符数组。void write(char[] cbuf, int off, int len):从偏移量 off 开始写入 len 个字符。void write(String str):写入字符串。void flush():刷新流。void close():关闭流。
字符输入管道
字符输入管道 PipedReader 从管道读取字符数据,数据必须由 PipedWriter 发送,适用于文本数据的读取。
常用方法:
PipedReader():默认构造方法。PipedReader(PipedWriter src):连接到指定的管道输出流。void connect(PipedWriter src):连接到指定的管道输出流。int read():读取单个字符。int read(char[] cbuf):读取多个字符到数组。int read(char[] cbuf, int off, int len):从偏移量 off 开始读取 len 个字符。void close():关闭流。
线程通信示例
1 | |
运行程序,控制台打印如下:
1 | |
特点
- 用于线程间通信,生产者写入数据,消费者读取数据。
- 必须在不同线程间使用,否则会导致死锁。
- 默认缓冲区大小为 1024 字节,数据超过后写入线程会阻塞,直到有数据被读取。
- 适用于文本数据传输,避免手动转换
byte[]和String。
随机访问文件
概述
在 Java 中,RandomAccessFile 是 java.io 包中的一个特殊文件操作类,它既可以像 FileInputStream 那样读取文件,也可以像 FileOutputStream 那样写入文件,同时它支持文件随机访问,允许在文件的任意位置进行读写操作,而不像普通的流操作那样只能顺序处理文件数据。
特点
- 支持文件的随机访问(可在文件的任意位置进行读写)。
- 同时具备输入流和输出流的功能。
- 支持指针(file pointer)定位,可跳转到指定位置进行读写。
- 适用于数据库索引、日志管理、大型文件处理(如 MP4、ISO 文件)。
适用场景
- 日志系统:在日志文件的某个特定位置插入或修改数据。
- 索引文件:如数据库索引,支持快速查找和更新。
- 大文件处理:如视频、音频文件的随机访问。
- 配置文件:修改特定字段,不必重写整个文件。
常用方法
构造方法
RandomAccessFile 需要指定文件路径和访问模式:
RandomAccessFile(String name, String mode):通过文件路径创建。RandomAccessFile(File file, String mode):通过 File 对象创建。
其中,mode(访问模式)常见值:
| 模式 | 说明 |
|---|---|
"r" |
只读模式(read-only),不允许写入 |
"rw" |
读写模式(read-write),文件可读可写 |
"rws" |
读写模式,数据和元数据(如文件大小、修改时间)同步写入 |
"rwd" |
读写模式,仅数据同步写入,不同步元数据 |
读写操作
int read():读取单个字节(返回 0-255,或 -1 表示 EOF)。int read(byte[] b):读取多个字节到数组 b。int read(byte[] b, int off, int len):读取 len 个字节到数组 b,从 off 位置开始。boolean readBoolean():读取 boolean。byte readByte():读取 byte。char readChar():读取 char。double readDouble():读取 double。float readFloat():读取 float。int readInt():读取 int。long readLong():读取 long。short readShort():读取 short。String readUTF():读取 UTF-8 编码的字符串。void write(int b):写入单个字节。void write(byte[] b):写入字节数组。void write(byte[] b, int off, int len):从数组的 off 位置开始写入 len 个字节。void writeBoolean(boolean v):写入 boolean。void writeByte(int v):写入 byte。void writeChar(int v):写入 char。void writeDouble(double v):写入 double。void writeFloat(float v):写入 float。void writeInt(int v):写入 int。void writeLong(long v):写入 long。void writeShort(int v):写入 short。void writeUTF(String str):写入 UTF-8 编码的字符串。
指针操作
long getFilePointer():获取当前文件指针的位置。void seek(long pos):移动文件指针到指定位置。long length():获取文件长度(字节数)。void setLength(long newLength):设置文件长度(可用于截断或扩展文件)。
关闭流
void close():关闭文件。
示例
基本的文件读写
1 | |
运行程序,控制台打印如下:
1 | |
指针跳转
1 | |
运行程序,控制台打印如下:
1 | |
覆盖写入
1 | |
运行程序,控制台打印如下:
1 | |
说明:
RandomAccessFile可以修改文件的任意部分,不会影响其他部分数据。
截断文件
1 | |
运行程序,控制台打印如下:
1 | |
说明:
- 如果想缩短或扩展文件大小,可以使用
setLength()。
setLength(7)截断文件,只保留前 7 字节,参数为 0 则可以清空整个文件。- 可用于清理部分数据或预分配文件大小。
IO 设计模式
装饰器模式
装饰器模式(Decorator Pattern)的核心思想是:在不改变原有类结构的前提下,通过 “包装” 的方式为对象添加新的职责或行为,从而使得功能可以灵活地组合或扩展。
角色划分:
- Component(抽象组件):定义对象的基本接口或抽象类。
- ConcreteComponent(具体组件):实现或继承上述抽象组件。它是最基础的功能提供者。
- Decorator(抽象装饰器):同样实现或继承抽象组件,同时持有一个组件类型的引用,用于对组件进行 “包装” 或 “增强”。
- ConcreteDecorator(具体装饰器):继承装饰器,真正对组件功能进行扩展或增强。
在 Java IO 中,对应的角色可以映射到各种 InputStream、OutputStream 以及它们的子类。例如 InputStream 就是抽象组件,FileInputStream、ByteArrayInputStream 等是具体组件,而 FilterInputStream 及其子类则扮演装饰器角色。
结构
在 Java IO 中最典型的装饰器结构可以归纳为以下几层:
抽象组件(Component):
InputStream:定义输入流的抽象方法,比如read()、close()等。OutputStream:定义输出流的抽象方法,比如write()、close()等。
具体组件(ConcreteComponent):
FileInputStream:从文件中读取数据。ByteArrayInputStream:从内存中的字节数组读取数据。PipedInputStream:从管道读取数据。- ……
这些类是实际与数据源或数据目的地交互的基础流,是最原始、最简单的功能实现。
抽象装饰器(Decorator):
FilterInputStream:它是对InputStream的包装,内部持有一个InputStream类型的引用,并通过委托的方式将方法调用转发给被装饰的对象,同时提供自身的额外或改良实现。FilterOutputStream:对应输出流的装饰器。
具体装饰器(ConcreteDecorator):
BufferedInputStream:在原有流之上添加缓冲功能,提高读操作的效率。DataInputStream:提供对基本数据类型如int、long、float、double等的读取方法。PushbackInputStream:允许将读取的数据推回到缓冲区,以便再次读取。BufferedOutputStream、DataOutputStream、PrintStream等对于输出流类似。
通过这种层层包装,不同装饰器就可以像搭积木一样组合。例如在读取文件时,我们可以将一个 FileInputStream 装饰(包装)到 BufferedInputStream 中,以获得缓冲功能,再进一步包装到 DataInputStream 中,以获得对各种原始数据类型的读取。
典型使用示例
1 | |
在这里,fis 是最基础的文件输入流,而 bis 为其添加了缓冲功能,dis 在 bis 的功能上又添加了读取各种数据类型的能力。通过这种层层包装,运行时就能组合出一个满足当前需求的 “增强版” 流对象。
优势
单一职责和高内聚:
每个具体组件(例如
FileInputStream)只负责跟文件的底层读写打交道,而各个具体装饰器(例如BufferedInputStream、DataInputStream)则负责特定的功能增强。这样有利于保持类的单一职责,也让功能划分更清晰。灵活可扩展:
用户可以根据需求选择合适的装饰器来组合功能,而不必为了支持新的功能就修改已有的类(符合开闭原则)。
运行时动态组合:
装饰器模式支持运行时组合,调用者可以灵活地决定如何组织流,比如想要缓存,可以加
BufferedInputStream,想要数据解析可以再加DataInputStream,二者可以相互独立却又可以无缝配合。
局限性
过度嵌套导致调试困难:
如果装饰器过多,流的包装层次变得复杂,可能会给调试和理解带来难度。
对象数量增多:
每增加一个装饰器就多出一个包装层,Java IO 中常常要写多层构造器,比如
new DataInputStream(new BufferedInputStream(new FileInputStream(...))),对于初学者来说,这种深度嵌套比较费解。接口层缺乏统一的高级抽象:
尽管装饰器给了我们组合的灵活性,但有时也让选择过于分散,需要开发者自己决定到底要使用哪些组合方式。对于某些场景,或许提供更高级别统一封装的类会更直观(比如在 NIO 和一些更高级别的库里提供了更丰富的抽象)。
总结
通过对 File 类及其常用方法的学习,我们能够更灵活地对文件和目录进行管理;借助字节流和字符流两大分支,再配合缓冲流、数据流、打印流等多种装饰/扩展用法,Java IO 为处理文件、网络数据、线程间通信等提供了一套优雅而强大的解决方案;文章还对 Java IO 的设计模式进行了简单介绍。
下一篇我们将进一步探索 Java NIO(New IO)、Channel 与 Buffer、Selector 等高阶特性;。希望本篇文章能够帮助你快速建立对 Java IO 体系的认知,为后续深入学习打下坚实的基础。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!