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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Slf4j
public class FileDemo {
public static void main(String[] args) {
// 使用绝对路径或相对路径创建 File 对象
File file = new File("Demo.txt");

// 判断文件是否存在
if (!file.exists()) {
try {
// 创建新文件
boolean created = file.createNewFile();
System.out.println("文件创建:" + created);
} catch (IOException e) {
log.error("创建文件失败", e);
}
}

// 输出文件的名称和绝对路径
System.out.println("文件名称:" + file.getName());
System.out.println("文件绝对路径:" + file.getAbsolutePath());

// 检查是否为文件或目录
System.out.println("是否为文件:" + file.isFile());
System.out.println("是否为目录:" + file.isDirectory());

// 获取文件大小和最后修改时间
System.out.println("文件大小:" + file.length() + " 字节");
System.out.println("最后修改时间:" + file.lastModified());

// 重命名文件
File newFile = new File("Demo-renamed.txt");
boolean renamed = file.renameTo(newFile);
System.out.println("重命名成功:" + renamed);

// 删除文件
boolean deleted = newFile.delete();
System.out.println("删除文件:" + deleted);
}
}

注意事项

  • 不直接操作文件内容File 类主要用于表示路径和管理文件属性,读写文件内容需要结合字节流或字符流(如 FileInputStreamFileReader 等)。
  • 平台差异:尽量使用 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 操作通常涉及系统调用,每次调用都会带来一定的开销。通过使用缓冲流(如 BufferedInputStreamBufferedReader 等),可以将多次小规模的读写合并成一次大规模的操作,从而显著提升性能。

默认缓冲区一般为 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
public class FileInputStreamDemo {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
int data;
while ((data = fis.read()) != -1) {
System.out.print((char) data); // 逐字节读取并输出
}
} catch (IOException e) {
log.error("读取文件失败", e);
} finally {
try {
if (fis != null) {
fis.close(); // 关闭流
}
} catch (IOException e) {
log.error("关闭文件失败", e);
}
}
}
}

特点

  • 适用于读取二进制文件(如 .jpg.mp3)或文本文件(但不推荐,可能会出现乱码问题,用 Reader 处理文本更合适)。
  • 逐字节读取,效率较低,通常与缓冲流(BufferedInputStream)一起使用提升性能。

字节文件输出流

字节文件输出流 FileOutputStream,用于向文件中写入数据。

构造方法

  • FileOutputStream(File file):通过 File 对象创建输出流。

  • FileOutputStream(String name):通过文件路径创建输出流。

  • FileOutputStream(File file, boolean append):是否以追加模式写入。

  • FileOutputStream(String name, boolean append):通过路径创建并控制是否追加。

示例:使用 FileOutputStream 写入文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
public class FileOutputStreamDemo {
public static void main(String[] args) {
String data = "Hello, Java IO!";
FileOutputStream fos = null;
try {
fos = new FileOutputStream("output.txt");
fos.write(data.getBytes()); // 写入字节数据
fos.flush(); // 确保数据立即写入
} catch (IOException e) {
log.error("写入文件失败", e);
} finally {
try {
if (fos != null) {
fos.close(); // 关闭流
}
} catch (IOException e) {
log.error("关闭文件失败", e);
}
}
}
}

特点

  • 适用于写入二进制文件,如图片、音频等。
  • flush() 方法可以确保数据立即写入,而不是缓存到缓冲区。

字节数组流

字节数组流无需涉及磁盘文件,适合小数据量的读取操作。

字节数组输入流

字节数组输入流 ByteArrayInputStream 允许将字节数组作为输入源,提供类似 InputStream 的读取方法。

构造方法

  • ByteArrayInputStream(byte[] buf):使用字节数组创建输入流。
  • ByteArrayInputStream(byte[] buf, int off, int len):指定偏移量和长度创建输入流。

示例:使用 ByteArrayInputStream 读取内存中的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
public class ByteArrayInputStreamDemo {
public static void main(String[] args) {
byte[] data = "Hello, ByteArrayInputStream!".getBytes();
ByteArrayInputStream bais = null;
try {
bais = new ByteArrayInputStream(data);
int byteData;
while ((byteData = bais.read()) != -1) {
System.out.print((char) byteData);
}
} finally {
try {
if (bais != null) {
bais.close();
}
} catch (IOException e) {
log.error("关闭流失败", e);
}
}
}
}

特点

  • 适用于处理小数据量的内存操作,避免磁盘 IO。
  • 可用于数据缓存、流转换等场景。

字节数组输出流

字节数组输出流 ByteArrayOutputStream 允许将字节数据写入到内存中的字节数组,而不是直接写入文件。

构造方法

  • ByteArrayOutputStream():默认初始缓冲区大小 32。
  • ByteArrayOutputStream(int size):指定初始缓冲区大小。

示例:使用 ByteArrayOutputStream 生成字节数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
public class ByteArrayOutputStreamDemo {
public static void main(String[] args) {
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
baos.write("Hello, ByteArrayOutputStream!".getBytes());
System.out.println(baos); // 转换成字符串输出
} catch (IOException e) {
log.error("写入数据失败", e);
} finally {
try {
if (baos != null) {
baos.close();
}
} catch (IOException e) {
log.error("关闭流失败", e);
}
}
}
}

特点

  • 适用于动态构造字节数组,避免频繁创建 byte[] 数组。
  • 适合数据缓冲、数据拼接的场景。

字节缓冲流

字节缓冲流用于提升 IO 读写效率,通过内部缓存方式减少底层 IO 访问次数。

字节缓冲输入流

字节缓冲输入流 BufferedInputStream 为输入流提供缓冲,减少 read() 调用次数,提高读取性能。

构造方法

  • BufferedInputStream(InputStream in):创建默认缓冲输入流(8KB)。
  • BufferedInputStream(InputStream in, int size):指定缓冲区大小创建缓冲输入流。

示例:使用 BufferedInputStream 读取文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
public class BufferedInputStreamDemo {
public static void main(String[] args) {
BufferedInputStream bis = null;
try {
bis = new BufferedInputStream(new FileInputStream("test.txt"));
int data;
while ((data = bis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
log.error("读取文件失败", e);
} finally {
try {
if (bis != null) {
bis.close();
}
} catch (IOException e) {
log.error("关闭流失败", e);
}
}
}
}

特点

  • 默认缓冲区大小为 8KB(可自定义)。
  • 适用于大文件读取,避免频繁调用底层 IO 操作。

字节缓冲输出流

字节缓冲输出流 BufferedOutputStream 为输出流提供缓冲,减少 write() 的磁盘 IO 频率,提高写入性能。

构造方法

  • BufferedOutputStream(OutputStream out):创建默认缓冲输出流(8KB)。
  • BufferedOutputStream(OutputStream out, int size):指定缓冲区大小创建缓冲输出流。

示例:使用 BufferedOutputStream 写入文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
public class BufferedOutputStreamDemo {
public static void main(String[] args) {
String data = "BufferedOutputStream Demo!";
BufferedOutputStream bos = null;
try {
bos = new BufferedOutputStream(new FileOutputStream("output.txt"));
bos.write(data.getBytes());
bos.flush();
} catch (IOException e) {
log.error("写入文件失败", e);
} finally {
try {
if (bos != null) {
bos.close();
}
} catch (IOException e) {
log.error("关闭流失败", e);
}
}
}
}

特点

  • 适用于频繁写入的场景,例如日志系统、流式写入文件等。

Closeable 接口与 try-with-resources 语法

在 Java IO 操作中,打开的资源(如文件流、数据库连接、网络流等)需要在使用完后正确关闭,以释放系统资源,防止资源泄露。

Java 7 之前,我们通常在 finally 代码块中调用 close() 方法来关闭流。但 Java 7 引入了 try-with-resources 语法(基于 AutoCloseable 接口),让资源管理更简单、代码更简洁。

Closeable 接口

Closeable 是 Java IO 包 (java.io) 提供的资源管理接口,它只有一个方法:

1
void close() throws IOException;

作用

  • 使得实现此接口的类可以被安全地关闭,例如 FileInputStreamBufferedReader 等。
  • close() 方法释放资源,防止资源泄露。

继承关系

1
public interface Closeable extends AutoCloseable

它继承了 AutoCloseable(Java 7 引入),这使得 Closeable 也可以在 try-with-resources 中使用。

try-with-resources 语法

传统方式关闭资源

在 Java 7 之前,我们需要手动在 finally 代码块中调用 close() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
public class OldStyleFileRead {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("Demo.txt");
int data;
while ((data = fis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
log.error("读取文件失败", e);
} finally {
if (fis != null) {
try {
fis.close(); // 关闭流
} catch (IOException e) {
log.error("关闭流失败", e);
}
}
}
}
}

问题

  • 代码冗长,需要手动检查 null,防止 NullPointerException
  • 容易出错,如果 try 代码块抛出异常,finally 仍需执行关闭逻辑。

自动关闭资源

Java 7 引入了 try-with-resources,资源在 try() 里声明,只需要捕获异常,而不需要在 finally 块中手动调用 close() 方法,即可自动关闭资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
public class TryWithResourcesDemo {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("Demo.txt")) { // 资源在 try() 里声明
int data;
while ((data = fis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
log.error("读取文件失败", e);
} // 这里不需要 finally,资源会被自动关闭
}
}

优势

  • 代码简洁,无需手动 close()
  • 异常安全,即使 try 代码块抛出异常,资源仍会自动关闭。
  • 多个资源管理,try 语法可以管理多个 Closeable 资源。

对比

Java 7 引入 AutoCloseable,它是 Closeable 的超接口:

1
2
3
public interface AutoCloseable {
void close() throws Exception;
}
接口 主要方法 抛出的异常 适用范围
Closeable void close() throws IOException IOException 主要用于 IO 资源,如 InputStream
AutoCloseable void close() throws Exception Exception(更广泛) 适用于所有需要关闭的资源(数据库连接、线程池等)
  • Closeable 主要用于 IO 相关的资源(InputStreamOutputStream 等)。
  • AutoCloseable 适用于更广泛的需要关闭资源连接的场景(ConnectionExecutorService 等)。

多资源同时关闭

如果有多个资源,可以在 try 语句中同时声明,在方法结束后这些资源的 close() 方法都会被调用,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
public class MultiResourceDemo {
public static void main(String[] args) {
try (
FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt")
) {
int data;
while ((data = fis.read()) != -1) {
fos.write(data); // 读取一个字节,写入目标文件
}
} catch (IOException e) {
log.error("读取文件失败", e);
}
}
}

工作原理:

  • try 语句块执行完毕后,fisfos 按声明顺序关闭。
  • 即使发生异常,资源仍然会被关闭。

自定义 Closeable 资源

try-with-resources 适用于所有实现 CloseableAutoCloseable 的类。

我们可以自定义一个可自动关闭的资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyResource implements Closeable {
public void use() {
System.out.println("Using resource...");
}

@Override
public void close() {
System.out.println("Resource closed.");
}
}

public class CustomCloseableDemo {
public static void main(String[] args) {
try (MyResource resource = new MyResource()) {
resource.use();
}
}
}

运行程序,控制台打印如下:

1
2
Using resource...
Resource closed.

即使 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
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
public class FileReaderDemo {
public static void main(String[] args) {
try (FileReader fr = new FileReader("source.txt")) {
int data;
while ((data = fr.read()) != -1) {
System.out.print((char) data); // 逐字符读取并输出
}
} catch (IOException e) {
log.error("读取文件失败", e);
}
}
}

特点

  • 适用于文本文件读取(不适合二进制文件)。
  • 默认使用平台默认编码(如 UTF-8、GBK),可使用 InputStreamReader 指定编码。

字符文件输出流

字符文件输出流 FileWriter,用于向文件中写入数据。

构造方法

  • FileWriter(File file):通过 File 对象创建输出流。

  • FileWriter(String name):通过文件路径创建输出流。

  • FileWriter(File file, boolean append):是否以追加模式写入。

  • FileWriter(String name, boolean append):通过路径创建并控制是否追加。

示例:使用 FileWriter 写入文本文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
public class FileWriterDemo {
public static void main(String[] args) {
String data = "Hello, FileWriter!";

try (FileWriter fw = new FileWriter("output.txt")) {
fw.write(data); // 写入字符串
fw.flush(); // 确保数据写入
} catch (IOException e) {
log.error("写入文件失败", e);
}
}
}

特点

  • 适用于文本数据写入。
  • flush() 确保数据立即写入,而不是缓存在内存。

字符数组流

适用于内存中的字符数组读写,避免磁盘 IO,适合临时数据存储。

字符数组输入流

字符数组输入流 CharArrayReader,允许将字符数组作为输入源。

构造方法

  • CharArrayReader(char[] buf):使用字符数组创建输入流。
  • CharArrayReader(char[] buf, int off, int len):指定偏移量和长度创建输入流。

示例:使用 CharArrayReader 读取字符数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
public class CharArrayReaderDemo {
public static void main(String[] args) {
char[] data = "Hello, CharArrayReader!".toCharArray();

try (CharArrayReader car = new CharArrayReader(data)) {
int charData;
while ((charData = car.read()) != -1) {
System.out.print((char) charData);
}
} catch (IOException e) {
log.error("读取字符数组失败", e);
}
}
}

特点

  • 适用于内存中的字符数据,无需文件 IO。

字符数组输出流

字符数组输出流 CharArrayWriter 允许写入字符数据到内存中的字符数组,类似 ByteArrayOutputStream

构造方法

1
2
CharArrayWriter()                          // 默认初始缓冲区大小 32
CharArrayWriter(int size) // 指定初始缓冲区大小

示例:使用 CharArrayWriter 生成字符数组。

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
public class CharArrayWriterDemo {
public static void main(String[] args) {
try (CharArrayWriter caw = new CharArrayWriter()) {
caw.write("Hello, CharArrayWriter!");
char[] charArray = caw.toCharArray();
System.out.println(new String(charArray)); // 转换成字符串输出
} catch (IOException e) {
log.error("写入字符数组失败", e);
}
}
}

特点

  • 动态存储字符数据,适用于字符串拼接、缓存。

字符缓冲流

字符缓冲流用于提升 IO 读写效率,通过内部缓存方式减少底层 IO 访问次数。

字符缓冲输入流

字符缓冲输入流 BufferedReader,为输入流提供缓冲,提高文本读取性能,并支持按行读取。

构造方法

  • BufferedReader(Reader in):创建默认缓冲输入流(8KB)。
  • BufferedReader(Reader in, int size):指定缓冲区大小创建缓冲输入流。

示例:使用 BufferedReader 读取文本文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
public class BufferedReaderDemo {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("Demo.txt"))) {
String line;
while ((line = br.readLine()) != null) { // 按行读取
System.out.println(line);
}
} catch (IOException e) {
log.error("读取文件失败", e);
}
}
}

特点

  • 默认缓冲区大小为 8KB(可自定义)。
  • readLine() 逐行读取,适用于读取大文件。

字符缓冲输出流

BufferedWriter 为输出流提供缓冲,减少 write() 的 IO 频率,提高写入性能。

构造方法

  • BufferedWriter(Writer out):创建默认缓冲输出流(8KB)。
  • BufferedWriter(Writer out, int size):指定缓冲区大小创建缓冲输出流。

示例:使用 BufferedWriter 写入文本文件。

1
2
3
4
5
6
7
8
9
10
11
12
public class BufferedWriterDemo {
public static void main(String[] args) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("buffered_output.txt"))) {
bw.write("BufferedWriter Demo!");
bw.newLine(); // 写入换行符
bw.write("Hello, Java IO!");
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}

特点

  • newLine() 方法可写入系统换行符(适用于跨平台换行)。
  • 适用于频繁写入的场景(如日志系统、流式写入文件)。

数据流

DataInputStreamDataOutputStream 允许以二进制格式读写 Java 的基本数据类型(如 intdoubleboolean)和字符串(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
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
public class DataOutputStreamDemo {
public static void main(String[] args) {
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.bin"))) {
dos.writeInt(100);
dos.writeDouble(99.99);
dos.writeBoolean(true);
dos.writeUTF("Hello, DataOutputStream!");
dos.flush();
} catch (IOException e) {
log.error("写入数据流失败", e);
}
}
}

特点

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
public class DataInputStreamDemo {
public static void main(String[] args) {
try (DataInputStream dis = new DataInputStream(new FileInputStream("data.bin"))) {
int intValue = dis.readInt();
double doubleValue = dis.readDouble();
boolean boolValue = dis.readBoolean();
String strValue = dis.readUTF();

System.out.println("Int: " + intValue);
System.out.println("Double: " + doubleValue);
System.out.println("Boolean: " + boolValue);
System.out.println("String: " + strValue);
} catch (IOException e) {
log.error("读取数据流失败", e);
}
}
}

运行程序,控制台打印如下:

1
2
3
4
Int: 100
Double: 99.99
Boolean: true
String: Hello, DataOutputStream!

特点

  • 读取数据的顺序必须与写入顺序一致,否则可能发生数据格式错误。

打印流

PrintStreamPrintWriter 主要用于格式化输出,支持自动 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
2
3
4
5
6
7
8
9
10
11
@Slf4j
public class PrintStreamDemo {
public static void main(String[] args) {
try (PrintStream ps = new PrintStream(new FileOutputStream("print.txt"))) {
ps.println("Hello, PrintStream!");
ps.printf("Number: %d, Price: %.2f%n", 42, 19.99);
} catch (IOException e) {
log.error("写入文件失败", e);
}
}
}

特点

  • 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
2
3
4
5
6
7
8
9
10
11
@Slf4j
public class PrintWriterDemo {
public static void main(String[] args) {
try (PrintWriter pw = new PrintWriter("print_writer.txt")) {
pw.println("Hello, PrintWriter!");
pw.printf("Score: %d, Accuracy: %.2f%% %n", 95, 97.5);
} catch (IOException e) {
log.error("写入文件失败", e);
}
}
}

特点

  • 适用于文本数据,不会自动转换为二进制。
  • 支持格式化,比 BufferedWriter 更灵活。

对象流

Java 对象流(Object Streams)用于序列化(Serialization) 和反序列化(Deserialization),使 Java 对象能够以二进制格式进行存储或传输,例如写入文件、通过网络传输等。对象流基于 ObjectInputStreamObjectOutputStream 实现。

输出流

对象输出流 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 需要实现 Serializable 接口
class Person implements Serializable {
private static final long serialVersionUID = 1L; // 推荐定义 serialVersionUID
private final String name;
private final int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}

@Slf4j
public class ObjectStreamDemo {
public static void main(String[] args) {
// 创建对象
Person person = new Person("Alice", 25);

// 序列化(写入对象到文件)
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.dat"))) {
oos.writeObject(person);
System.out.println("对象已序列化");
} catch (IOException e) {
log.error("写入文件失败", e);
}

// 反序列化(从文件读取对象)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.dat"))) {
Person restoredPerson = (Person) ois.readObject();
System.out.println("反序列化对象:" + restoredPerson);
} catch (IOException | ClassNotFoundException e) {
log.error("读取文件失败", e);
}
}
}

运行程序,控制台打印如下:

1
2
对象已序列化
反序列化对象:Person{name='Alice', age=25}

序列化机制

Serializable 接口

作用

  • 允许对象序列化,使其能够写入 ObjectOutputStream 并恢复到 ObjectInputStream
  • 这是一个标记接口(Marker Interface),没有任何方法,Java 通过反射检查类是否实现了 Serializable

定义方式

1
2
3
class Person implements Serializable {
private static final long serialVersionUID = 1L;
}

serialVersionUID 的作用:

  • 用于唯一标识类的版本,确保反序列化时版本兼容。
  • 如果类发生改变(如添加字段),但 serialVersionUID 不变,仍然可以反序列化。
  • 如果 serialVersionUID 变更或未定义,默认会基于类结构计算,类修改后可能导致 InvalidClassException

transient 关键字

作用

  • 被此关键字标识的变量不参与序列化,防止敏感数据(如密码)或非必要数据(如缓存)被写入文件或网络。
  • transient 修饰的字段不会被 writeObject() 保存,在 readObject() 时该字段会使用默认值(如 null0)。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class User implements Serializable {
private static final long serialVersionUID = 1L;
private final String username;
private final transient String password; // 不序列化

public User(String username, String password) {
this.username = username;
this.password = password;
}

@Override
public String toString() {
return "User{username='" + username + "', password='" + password + "'}";
}
}

@Slf4j
public class TransientDemo {
public static void main(String[] args) {
User user = new User("Alice", "secret");

// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
oos.writeObject(user);
System.out.println("用户对象已序列化");
} catch (IOException e) {
log.error("序列化用户失败", e);
}

// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
User restoredUser = (User) ois.readObject();
System.out.println("反序列化用户:" + restoredUser);
} catch (IOException | ClassNotFoundException e) {
log.error("反序列化用户失败", e);
}
}
}

运行程序,控制台打印如下:

1
2
用户对象已序列化
反序列化用户:User{username='Alice', password='null'}

password 字段在反序列化后为 null,因为 transient 使其未被存储。

Java 序列化过程

  • 序列化(Serialization)ObjectOutputStream.writeObject(obj) 将对象转换为二进制格式,并存储到文件或发送到网络。
  • 反序列化(Deserialization)ObjectInputStream.readObject() 从文件或网络中读取二进制数据,并恢复为原始对象。

限制

  • 必须实现 Serializable,否则 writeObject() 会抛 NotSerializableException
  • static 变量属于类而非实例,因此不会序列化,不会被存储。
  • transient 修饰的变量不会序列化,需手动初始化,或在 readObject() 方法中恢复值。

自定义序列化

如果类需要特殊的序列化逻辑,可以自定义 writeObject()readObject()

1
2
3
4
5
6
7
8
9
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 让默认字段序列化
oos.writeUTF(encrypt(password)); // 手动加密存储
}

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 读取默认字段
this.password = decrypt(ois.readUTF()); // 读取并解密
}
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Slf4j
public class PipedStreamDemo {
public static void main(String[] args) throws IOException {
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream(pis); // 连接两个管道流

// 生产者线程(写入数据)
Thread producer = new Thread(() -> {
try {
String message = "Hello from producer!";
pos.write(message.getBytes());
pos.close(); // 关闭输出流
} catch (IOException e) {
log.error("写入数据失败", e);
}
});

// 消费者线程(读取数据)
Thread consumer = new Thread(() -> {
try {
int data;
while ((data = pis.read()) != -1) {
System.out.print((char) data);
}
pis.close(); // 关闭输入流
} catch (IOException e) {
log.error("读取数据失败", e);
}
});

producer.start();
consumer.start();
}
}

运行程序,控制台打印如下:

1
Hello from producer!

特点

  • 用于线程间通信,生产者写入数据,消费者读取数据。
  • 必须在不同线程间使用,否则会导致死锁。
  • 默认缓冲区大小为 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Slf4j
public class PipedCharStreamDemo {
public static void main(String[] args) throws IOException {
PipedReader pr = new PipedReader();
PipedWriter pw = new PipedWriter(pr); // 连接两个管道流

// 生产者线程(写入数据)
Thread producer = new Thread(() -> {
try {
String message = "Hello from PipedWriter!";
pw.write(message);
pw.close(); // 关闭输出流
} catch (IOException e) {
log.error("写入数据失败", e);
}
});

// 消费者线程(读取数据)
Thread consumer = new Thread(() -> {
try {
int data;
while ((data = pr.read()) != -1) {
System.out.print((char) data);
}
pr.close(); // 关闭输入流
} catch (IOException e) {
log.error("读取数据失败", e);
}
});

producer.start();
consumer.start();
}
}

运行程序,控制台打印如下:

1
Hello from PipedWriter!

特点

  • 用于线程间通信,生产者写入数据,消费者读取数据。
  • 必须在不同线程间使用,否则会导致死锁。
  • 默认缓冲区大小为 1024 字节,数据超过后写入线程会阻塞,直到有数据被读取。
  • 适用于文本数据传输,避免手动转换 byte[]String

随机访问文件

概述

在 Java 中,RandomAccessFilejava.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
public class RandomAccessFileDemo {
public static void main(String[] args) {
try (RandomAccessFile raf = new RandomAccessFile("Demo.txt", "rw")) {
// 写入数据
raf.writeUTF("Hello, RandomAccessFile!");
raf.writeInt(100);
raf.writeDouble(99.99);
raf.seek(0); // 指针回到开头

// 读取数据
String text = raf.readUTF();
int number = raf.readInt();
double value = raf.readDouble();

System.out.println("读取到的内容:" + text);
System.out.println("读取到的整数:" + number);
System.out.println("读取到的浮点数:" + value);
} catch (IOException e) {
log.error("操作文件失败", e);
}
}
}

运行程序,控制台打印如下:

1
2
3
读取到的内容:Hello, RandomAccessFile!
读取到的整数:100
读取到的浮点数:99.99

指针跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Slf4j
public class RandomAccessFilePointerDemo {
public static void main(String[] args) {
try (RandomAccessFile raf = new RandomAccessFile("pointer.txt", "rw")) {
// 写入数据
raf.writeUTF("Hello");
raf.writeInt(100);
raf.writeDouble(99.99);

// 获取当前指针位置
long pos = raf.getFilePointer();
System.out.println("当前指针位置:" + pos);

// 回到文件开头并读取内容
raf.seek(0);
System.out.println("读取字符串:" + raf.readUTF());

// 跳转到整数的位置
raf.seek(7); // UTF-8 编码的 "Hello" 占 7 字节(额外2字节长度信息)
System.out.println("读取整数:" + raf.readInt());

// 跳转到浮点数的位置
raf.seek(11);
System.out.println("读取浮点数:" + raf.readDouble());
} catch (IOException e) {
log.error("操作文件失败", e);
}
}
}

运行程序,控制台打印如下:

1
2
3
4
当前指针位置:19
读取字符串:Hello
读取整数:100
读取浮点数:99.99

覆盖写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
public class RandomAccessFileOverwriteDemo {
public static void main(String[] args) {
try (RandomAccessFile raf = new RandomAccessFile("overwrite.txt", "rw")) {
// 写入初始数据
raf.writeUTF("Hello World!");

// 跳到第 7 个字节("World!" 位置)
raf.seek(7);
raf.writeUTF("Java");

// 读取数据
raf.seek(0);
System.out.println("修改后的内容:" + raf.readUTF());
} catch (IOException e) {
log.error("操作文件失败", e);
}
}
}

运行程序,控制台打印如下:

1
修改后的内容:Hello Java!

说明

  • RandomAccessFile 可以修改文件的任意部分,不会影响其他部分数据。

截断文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
public class RandomAccessFileTruncateDemo {
public static void main(String[] args) {
try (RandomAccessFile raf = new RandomAccessFile("truncate.txt", "rw")) {
raf.writeUTF("Hello, Java!");
System.out.println("文件长度:" + raf.length());

// 截断文件到 7 字节
raf.setLength(7);
System.out.println("截断后文件长度:" + raf.length());
} catch (IOException e) {
log.error("操作文件失败", e);
}
}
}

运行程序,控制台打印如下:

1
2
文件长度:14
截断后文件长度:7

说明

  • 如果想缩短或扩展文件大小,可以使用 setLength()
  • setLength(7) 截断文件,只保留前 7 字节,参数为 0 则可以清空整个文件。
  • 可用于清理部分数据或预分配文件大小。

IO 设计模式

装饰器模式

装饰器模式(Decorator Pattern)的核心思想是:在不改变原有类结构的前提下,通过 “包装” 的方式为对象添加新的职责或行为,从而使得功能可以灵活地组合或扩展。

角色划分

  1. Component(抽象组件):定义对象的基本接口或抽象类。
  2. ConcreteComponent(具体组件):实现或继承上述抽象组件。它是最基础的功能提供者。
  3. Decorator(抽象装饰器):同样实现或继承抽象组件,同时持有一个组件类型的引用,用于对组件进行 “包装” 或 “增强”。
  4. ConcreteDecorator(具体装饰器):继承装饰器,真正对组件功能进行扩展或增强。

在 Java IO 中,对应的角色可以映射到各种 InputStreamOutputStream 以及它们的子类。例如 InputStream 就是抽象组件,FileInputStreamByteArrayInputStream 等是具体组件,而 FilterInputStream 及其子类则扮演装饰器角色。

结构

在 Java IO 中最典型的装饰器结构可以归纳为以下几层:

  1. 抽象组件(Component)

    • InputStream:定义输入流的抽象方法,比如 read()close() 等。
    • OutputStream:定义输出流的抽象方法,比如 write()close() 等。
  2. 具体组件(ConcreteComponent)

    • FileInputStream:从文件中读取数据。
    • ByteArrayInputStream:从内存中的字节数组读取数据。
    • PipedInputStream:从管道读取数据。
    • ……

    这些类是实际与数据源或数据目的地交互的基础流,是最原始、最简单的功能实现。

  3. 抽象装饰器(Decorator)

    • FilterInputStream:它是对 InputStream 的包装,内部持有一个 InputStream 类型的引用,并通过委托的方式将方法调用转发给被装饰的对象,同时提供自身的额外或改良实现。
    • FilterOutputStream:对应输出流的装饰器。
  4. 具体装饰器(ConcreteDecorator)

    • BufferedInputStream:在原有流之上添加缓冲功能,提高读操作的效率。
    • DataInputStream:提供对基本数据类型如 intlongfloatdouble 等的读取方法。
    • PushbackInputStream:允许将读取的数据推回到缓冲区,以便再次读取。
    • BufferedOutputStreamDataOutputStreamPrintStream 等对于输出流类似。

通过这种层层包装,不同装饰器就可以像搭积木一样组合。例如在读取文件时,我们可以将一个 FileInputStream 装饰(包装)到 BufferedInputStream 中,以获得缓冲功能,再进一步包装到 DataInputStream 中,以获得对各种原始数据类型的读取。

典型使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try (
// 最基础的功能:从文件中读取字节
FileInputStream fis = new FileInputStream("example.txt");

// 装饰器一:在读取功能之上增加缓冲,提高读取效率
BufferedInputStream bis = new BufferedInputStream(fis);

// 装饰器二:在上面再增加读取各种基础数据类型的能力
DataInputStream dis = new DataInputStream(bis)
) {
// 使用 dis 来读取各种数据类型,如 readInt()、readUTF() 等
int number = dis.readInt();
String text = dis.readUTF();
// ...
} catch (IOException e) {
e.printStackTrace();
}

在这里,fis 是最基础的文件输入流,而 bis 为其添加了缓冲功能,disbis 的功能上又添加了读取各种数据类型的能力。通过这种层层包装,运行时就能组合出一个满足当前需求的 “增强版” 流对象。

优势

  1. 单一职责和高内聚

    每个具体组件(例如 FileInputStream)只负责跟文件的底层读写打交道,而各个具体装饰器(例如 BufferedInputStreamDataInputStream)则负责特定的功能增强。这样有利于保持类的单一职责,也让功能划分更清晰。

  2. 灵活可扩展

    用户可以根据需求选择合适的装饰器来组合功能,而不必为了支持新的功能就修改已有的类(符合开闭原则)。

  3. 运行时动态组合

    装饰器模式支持运行时组合,调用者可以灵活地决定如何组织流,比如想要缓存,可以加 BufferedInputStream,想要数据解析可以再加 DataInputStream,二者可以相互独立却又可以无缝配合。

局限性

  1. 过度嵌套导致调试困难

    如果装饰器过多,流的包装层次变得复杂,可能会给调试和理解带来难度。

  2. 对象数量增多

    每增加一个装饰器就多出一个包装层,Java IO 中常常要写多层构造器,比如 new DataInputStream(new BufferedInputStream(new FileInputStream(...))),对于初学者来说,这种深度嵌套比较费解。

  3. 接口层缺乏统一的高级抽象

    尽管装饰器给了我们组合的灵活性,但有时也让选择过于分散,需要开发者自己决定到底要使用哪些组合方式。对于某些场景,或许提供更高级别统一封装的类会更直观(比如在 NIO 和一些更高级别的库里提供了更丰富的抽象)。

总结

通过对 File 类及其常用方法的学习,我们能够更灵活地对文件和目录进行管理;借助字节流和字符流两大分支,再配合缓冲流、数据流、打印流等多种装饰/扩展用法,Java IO 为处理文件、网络数据、线程间通信等提供了一套优雅而强大的解决方案;文章还对 Java IO 的设计模式进行了简单介绍。

下一篇我们将进一步探索 Java NIO(New IO)、Channel 与 Buffer、Selector 等高阶特性;。希望本篇文章能够帮助你快速建立对 Java IO 体系的认知,为后续深入学习打下坚实的基础。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!