Java IO——BIO
本文最后更新于:3 个月前
引言
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 协议 ,转载请注明出处!