Java IO流系统
面向对象程序设计(Java)
掌握文件操作与数据流处理,构建完整的输入输出体系File类基础概念
File 类是Java中用于表示文件系统中的文件或目录路径名的抽象表示。它提供了一个与平台无关的方式来处理文件和目录。
📦 核心概念:File类的双重身份
File对象可以表示两种类型:
📄 文件
存储数据的实际文件(如:test.txt、image.jpg)
new File("data.txt")
📁 目录
包含其他文件和子目录的文件夹
new File("docs")
⚠️ 注意:File对象本身只是路径的抽象表示,创建File对象并不会在文件系统中创建实际的文件或目录!
🏗️ 构造方法
// 1. 表示文件
File file1 = new File("D:\\workspace\\test.txt");
// 2. 表示目录
File dir1 = new File("D:\\workspace\\docs");
// 3. 使用父目录和子路径
File file2 = new File("D:\\workspace", "test.txt");
// 4. 使用File对象和子路径
File parent = new File("D:\\workspace");
File file3 = new File(parent, "test.txt");
// 5. 路径分隔符的跨平台处理
String path = "docs" + File.separator + "data" + File.separator + "config.txt";
// 判断File对象表示的是文件还是目录
System.out.println(file1.isFile()); // true(如果是文件)
System.out.println(file1.isDirectory()); // false
System.out.println(dir1.isDirectory()); // true(如果是目录)
System.out.println(dir1.isFile()); // false
⚠️ 重要概念
- 抽象表示:File对象不一定对应真实存在的文件
- 路径分隔符:Windows使用
\,Linux/macOS使用/ - 相对路径 vs 绝对路径:相对于当前工作目录 vs 从根目录开始
- 规范路径:解析
.和..后的标准路径
File类只是路径的抽象,不创建实际文件。真正的文件操作需要其他IO类配合。
File类:路径
File类提供了丰富的方法来获取路径信息和文件名称。
public static void main(String[] args) throws IOException {
// 创建测试文件
File file = new File("./test/../docs/data.txt");
file.getParentFile().mkdirs();
file.createNewFile();
// 路径相关方法
// "data.txt"
System.out.println("getName(): " + file.getName());
// "./test/../docs/data.txt"
System.out.println("getPath(): " + file.getPath());
// 完整绝对路径
System.out.println("getAbsolutePath(): " + file.getAbsolutePath());
// 规范化路径
System.out.println("getCanonicalPath(): " + file.getCanonicalPath());
// 父目录相关
// "./test/../docs"
System.out.println("getParent(): " + file.getParent());
File parent = file.getParentFile();
// "docs"
System.out.println("父目录名: " + parent.getName());
// 磁盘空间信息
File root = new File("C:");
long toGB = 1024 * 1024 * 1024;
System.out.println("总空间: " + root.getTotalSpace() / toGB + " GB");
System.out.println("可用空间: " + root.getFreeSpace() / toGB + " GB");
System.out.println("可用空间: " + root.getUsableSpace() / toGB + " GB");
}
🔄 路径转换对比
| 方法 | 特点 |
|---|---|
| getPath() | 返回创建时的原始路径 |
| getAbsolutePath() | 转换为绝对路径,但不解析.. |
| getCanonicalPath() | 解析..和.,返回规范路径(推荐) |
📋 路径相关API详解
| 方法名 | 返回类型 | 功能说明 | 使用示例 |
|---|---|---|---|
getName() |
String | 获取文件名或目录名 | file.getName() → "data.txt" |
getPath() |
String | 获取创建时的原始路径 | file.getPath() → "./test/../docs/data.txt" |
getAbsolutePath() |
String | 获取绝对路径(不解析..) | file.getAbsolutePath() → "D:/workspace/docs/data.txt" |
getCanonicalPath() |
String | 获取规范路径(解析..和.) | file.getCanonicalPath() → "D:/workspace/docs/data.txt" |
getParent() |
String | 获取父目录路径字符串 | file.getParent() → "./test/../docs" |
getParentFile() |
File | 获取父目录File对象 | file.getParentFile().getName() → "docs" |
💡 路径处理技巧:
- 规范路径:优先使用
getCanonicalPath(),它能正确解析相对路径 - 跨平台:使用
File.separator而不是硬编码分隔符 - 异常处理:
getCanonicalPath()可能抛出IOException
File类:状态
检查文件状态、获取文件属性是文件操作的第一步。
public static void analyzeFile(String filePath) {
File file = new File(filePath);
// 存在性检查
if (!file.exists()) {
System.out.println("文件不存在: " + filePath);
return;
}
// 类型判断
System.out.println("是文件: " + file.isFile());
System.out.println("是目录: " + file.isDirectory());
System.out.println("是绝对路径: " + file.isAbsolute());
System.out.println("是隐藏文件: " + file.isHidden());
// 权限检查
System.out.println("可读: " + file.canRead());
System.out.println("可写: " + file.canWrite());
System.out.println("可执行: " + file.canExecute());
// 文件属性
System.out.println("文件大小: " + file.length() + " 字节");
// 时间戳处理
long lastModified = file.lastModified();
Date date = new Date(lastModified);
System.out.println("最后修改时间: " +
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
// 如果是目录,显示文件数量
if (file.isDirectory()) {
String[] files = file.list();
System.out.println("目录下的文件数量: "
+ (files != null ? files.length : 0));
}
}
⚡ 实际应用场景
- 文件备份前检查:验证文件存在性和可读性
- 磁盘空间管理:大文件处理前检查剩余空间
- 文件监控:通过时间戳检测文件变化
- 权限验证:确保程序有足够权限操作文件
📋 状态检查API详解
| 方法名 | 返回类型 | 功能说明 | 使用示例 |
|---|---|---|---|
exists() |
boolean | 检查文件或目录是否存在 | file.exists() → true/false |
isFile() |
boolean | 判断是否为文件 | file.isFile() → true/false |
isDirectory() |
boolean | 判断是否为目录 | file.isDirectory() → true/false |
isAbsolute() |
boolean | 判断是否为绝对路径 | file.isAbsolute() → true/false |
isHidden() |
boolean | 判断是否为隐藏文件 | file.isHidden() → true/false |
canRead() |
boolean | 检查是否可读 | file.canRead() → true/false |
canWrite() |
boolean | 检查是否可写 | file.canWrite() → true/false |
canExecute() |
boolean | 检查是否可执行 | file.canExecute() → true/false |
length() |
long | 获取文件大小(字节) | file.length() → 1024L |
lastModified() |
long | 获取最后修改时间(毫秒) | file.lastModified() → 1640995200000L |
setLastModified(long) |
boolean | 设置最后修改时间 | file.setLastModified(System.currentTimeMillis()) |
setReadable(boolean) |
boolean | 设置可读权限 | file.setReadable(true) |
setWritable(boolean) |
boolean | 设置可写权限 | file.setWritable(false) |
setExecutable(boolean) |
boolean | 设置可执行权限 | file.setExecutable(true) |
💡 状态检查技巧:
- 空指针安全:调用任何方法前先用
exists()检查文件是否存在 - 性能考虑:
length()和lastModified()需要访问文件系统,频繁调用会影响性能 - 权限管理:在执行文件操作前先检查相应权限,避免异常
- 时间处理:
lastModified()返回毫秒时间戳,需要转换为可读格式
提示:在执行文件操作前,一定要先调用exists()等方法检查文件状态!
File类:变更
File类提供了创建、删除、重命名等文件操作方法。
📁 创建和删除操作
public static void fileOperationsDemo() throws IOException {
// 1. 创建文件
File file = new File("temp.txt");
if (file.createNewFile()) {
System.out.println("文件创建成功: " + file.getName());
} else {
System.out.println("文件已存在或创建失败");
}
// 2. 创建目录
File dir = new File("mydir");
if (dir.mkdir()) {
System.out.println("单级目录创建成功");
}
// 3. 创建多级目录
File multiDir = new File("path/to/deep/directory");
if (multiDir.mkdirs()) {
System.out.println("多级目录创建成功");
}
// 4. 删除文件
if (file.delete()) {
System.out.println("文件删除成功");
}
// 5. 程序退出时删除
File tempFile = File.createTempFile("temp", ".txt");
tempFile.deleteOnExit(); // JVM退出时自动删除
// 6. 重命名/移动文件
File oldFile = new File("old.txt");
File newFile = new File("new.txt");
if (oldFile.renameTo(newFile)) {
System.out.println("文件重命名成功");
}
}
⚠️ 常见陷阱
- delete()不能删除非空目录,需要递归删除
- renameTo()不能跨文件系统移动文件
✍️ 练习题
任务:在你的project下面,建立一个名为resource的文件夹,然后在该文件夹下创建1.txt,
2.txt, ..., 100.txt共100个文本文档。
🔍 点击查看参考答案
public class CreateFilesDemo {
public static void main(String[] args) {
File resourceDir = new File("resource");
if (!resourceDir.exists()) {
if (resourceDir.mkdirs()) {
System.out.println("resource目录创建成功");
}
}
for (int i = 1; i <= 100; i++) {
File file = new File(resourceDir, i + ".txt");
try {
if (file.createNewFile()) {
System.out.println(i + ".txt 创建成功");
}
} catch (IOException e) {
System.out.println("创建文件失败: " + e.getMessage());
}
}
}
}
📋 文件变更API详解
| 方法名 | 返回类型 | 功能说明 | 使用示例 |
|---|---|---|---|
createNewFile() |
boolean | 创建新文件(不存在时) | file.createNewFile() → true/false |
mkdir() |
boolean | 创建单级目录 | dir.mkdir() → true/false |
mkdirs() |
boolean | 创建多级目录 | dir.mkdirs() → true/false |
delete() |
boolean | 删除文件或空目录 | file.delete() → true/false |
deleteOnExit() |
void | JVM退出时删除 | tempFile.deleteOnExit() |
renameTo(File) |
boolean | 重命名或移动文件 | oldFile.renameTo(newFile) |
createTempFile(String,String) |
File | 创建临时文件(静态方法) | File.createTempFile("temp",".txt") |
createTempFile(String,String,File) |
File | 在指定目录创建临时文件 | File.createTempFile("temp",".txt",dir) |
💡 文件变更技巧:
- 异常处理:
createNewFile()会抛出IOException,需要处理 - 目录删除:删除非空目录需要递归删除所有子文件和子目录
- 重命名限制:
renameTo()不能跨文件系统移动,大文件移动建议用IO流 - 临时文件:使用
createTempFile()创建临时文件,避免命名冲突
File类提供了文件系统操作的基础,但实际的文件读写需要使用IO流类。
File类:遍历
File类提供了丰富的目录遍历和过滤方法,可以灵活地获取文件列表。
🔍 目录遍历和过滤
// 1. 列出目录内容
File directory = new File(".");
String[] files = directory.list();
File[] fileObjects = directory.listFiles();
// 2. 使用过滤器
// 只显示.txt文件
String[] txtFiles = directory.list((dir, name) -> name.endsWith(".txt"));
// 只显示目录
File[] dirsOnly = directory.listFiles(File::isDirectory);
// 自定义过滤器:显示大于1MB的文件
File[] largeFiles = directory.listFiles(file ->
file.isFile() && file.length() > 1024 * 1024
);
✍️ 练习题
任务:给定一个根目录(例如C:\users\Administrator\IdeaProjects\untitled1),实现一个简单的tree命令,用于列出所有根目录下的文件、文件夹的层级结构:
resource:
|----images/
|---- 3DImages/
|---- 1.jpg
|---- 2.jpg
🔍 点击查看参考答案
public class TreeCommand {
public static void main(String[] args) {
File rootDir = new File("C:\\users\\Administrator\\IdeaProjects\\untitled1");
printTree(rootDir, "");
}
private static void printTree(File dir, String prefix) {
if (!dir.exists() || !dir.isDirectory()) {
System.out.println("目录不存在或不是一个目录");
return;
}
System.out.println(prefix + dir.getName() + ":");
File[] files = dir.listFiles();
if (files == null) {
return;
}
for (int i = 0; i < files.length; i++) {
boolean isLast = (i == files.length - 1);
String newPrefix = prefix + "| ";
String connector = isLast ? "|---- " : "|---- ";
if (files[i].isDirectory()) {
printTree(files[i], newPrefix);
} else {
System.out.println(newPrefix + connector + files[i].getName());
}
}
}
}
📊 遍历方法对比
| 方法 | 返回类型 | 特点 | 适用场景 |
|---|---|---|---|
list() |
String[] | 返回文件名数组 | 只需要文件名时 |
listFiles() |
File[] | 返回File对象数组 | 需要进一步操作文件时 |
list(FilenameFilter) |
String[] | 按名称过滤 | 按扩展名等名称规则过滤 |
listFiles(FileFilter) |
File[] | 按File属性过滤 | 按大小、类型等属性过滤 |
⚠️ 常见陷阱
- listFiles()可能返回null,需要空值检查
- 过滤器中可能出现异常,需要妥善处理
- 遍历大目录时性能问题,考虑使用NIO的Files.walk()
📋 遍历API详解
| 方法名 | 返回类型 | 功能说明 | 使用示例 |
|---|---|---|---|
list() |
String[] | 列出目录下的文件名 | dir.list() → ["a.txt", "b.txt"] |
list(FilenameFilter) |
String[] | 按过滤器列出文件名 | dir.list((d,n)→n.endsWith(".txt")) |
listFiles() |
File[] | 列出目录下的File对象 | dir.listFiles() → [File[], ...] |
listFiles(FileFilter) |
File[] | 按过滤器列出File对象 | dir.listFiles(File::isDirectory) |
💡 遍历技巧:
- 空值检查:
listFiles()在目录不存在或无权限时返回null - 性能考虑:大目录遍历建议使用NIO的Files.walk()
- Lambda表达式:过滤器可以使用方法引用或Lambda表达式简化代码
- 链式过滤:可以结合Stream API进行更复杂的过滤
目录遍历是文件处理的基础操作,结合过滤器可以实现灵活的文件筛选。
File vs Path/Files:选型与迁移
从 Java 7 开始,java.nio.file 提供了更现代的文件系统 API。新代码更推荐 Path + Files。
| 需求 | File(java.io) | Path/Files(java.nio.file) |
|---|---|---|
| 路径表示 | new File("a/b.txt") |
Path.of("a", "b.txt") |
| 存在性 | file.exists() |
Files.exists(path) |
| 创建目录 | dir.mkdirs() |
Files.createDirectories(path) |
| 拷贝/移动 | 需要额外工具/手写流 | Files.copy / Files.move |
| 遍历目录 | listFiles() |
Files.walk() / Files.walkFileTree() |
| 读写文本 | FileReader/BufferedReader | Files.newBufferedReader / Files.readString |
最常用的迁移姿势
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
public class MigrationDemo {
public static void main(String[] args) throws Exception {
File legacy = new File("data.txt");
Path p = legacy.toPath();
if (Files.exists(p)) {
System.out.println(Files.size(p));
}
File back = p.toFile();
System.out.println(back.getAbsolutePath());
}
}
建议:对外部库/旧接口仍可用 File,但自己的业务代码优先用 Path 表达路径。
IO流概述
Java IO采用流(Stream)的概念来处理数据的输入输出。
流就像数据传输的管道,数据从源头流向目的地。
🌊 流的基本概念
- 单向性:输入流只能读取,输出流只能写入
- 顺序性:数据按顺序传输,支持随机访问的特殊类除外
- 缓冲机制:提高I/O效率的关键
- 装饰器模式:通过组合增强功能
💧 流的比喻理解
数据源
文件、网络、内存
输入流
读取数据
程序处理
转换数据
输出流
写入数据
目的地
文件、网络、内存
流是Java IO的核心概念,理解流的特性和分类是掌握Java IO体系的基础。字节流和字符流的选择取决于数据类型:文本数据使用字符流(Text IO),二进制数据使用字节流(Binary IO),对象状态保存使用序列化(Object IO)。
IO流层次结构
Java IO四大抽象类构成了完整的流体系,通过继承和装饰器模式实现丰富功能。
🏗️ 四大抽象类体系概览
字节输入流
InputStream所有字节输入流的抽象基类
字节输出流
OutputStream所有字节输出流的抽象基类
字符输入流
Reader所有字符输入流的抽象基类
字符输出流
Writer所有字符输出流的抽象基类
字节流层次结构
字节流是Java IO的基础,直接处理二进制数据
所有其他流类型都是在字节流基础上构建的。
字符流层次结构
字符流专门用于处理文本数据,自动处理字符编码转换,是处理文本的理想选择。
📖 Reader 层次结构
BufferedReader
- 缓冲字符输入流
InputStreamReader
- 字节流到字符流的桥梁
FileReader
- 文件字符输入流
...
StringReader
- 字符串输入流
CharArrayReader
- 字符数组输入流
...
✍️ Writer 层次结构
BufferedWriter
- 缓冲字符输出流
OutputStreamWriter
- 字符流到字节流的桥梁
FileWriter
- 文件字符输出流
...
StringWriter
- 字符串输出流
CharArrayWriter
- 字符数组输出流
PrintWriter
- 格式化字符输出流
...
字节流与字符流对比
深入理解字节流和字符流的区别与联系,根据实际需求选择合适的流类型。
🎯 字节流 vs 字符流对比
📊 示例:整数 199 的存储方式对比
整数 199 = 十六进制 0xC7
📝 文本文件存储
作为三个字符序列存储:
"1" → 0x31 (49)"9" → 0x39 (57)"9" → 0x39 (57)
占用 3 字节
💾 二进制文件存储
直接存储字节值:
0xC7 (199)
占用 1 字节
结论:写入 "1" "9" "9" 的字节开销比直接写入 C7 要多 2 倍!
| 特性 | 字节流 | 字符流 |
|---|---|---|
| 处理单位 | 字节(8位) | 字符(16位) |
| 适用场景 | 二进制文件(图片、音视频) | 文本文件(.txt, .csv, .json) |
| 编码处理 | 无需考虑 | 需要处理字符编码 |
| 典型子类 | FileInputStream, BufferedInputStream | FileReader, BufferedReader |
字节流是Java IO的基础,字符流是字节流的封装。
应用场景与流类型选择
理解字节流与字符流的应用场景,根据数据类型选择合适的IO工具。
🌊 流类型与应用场景映射
Text IO
主要使用:字符流
底层实现:字节流+编码转换
典型类:FileReader, BufferedReader
Binary IO
主要使用:字节流
底层实现:直接字节操作
典型类:FileInputStream, DataInputStream
Object IO
基于:字节流
实现机制:序列化
典型类:ObjectOutputStream, ObjectInputStream
🎯 选择决策树
第一步:确定数据类型
- 文本数据 → 考虑字符流
- 二进制数据 → 使用字节流
- Java对象 → 考虑序列化
第二步:选择具体实现
- 文本文件 → FileReader/BufferedReader
- 二进制文件 → FileInputStream/DataInputStream
- 对象保存 → ObjectOutputStream/ObjectInputStream
核心理解:字符流和字节流是Java IO的基础分类,Text/Binary/Object
IO是它们在不同应用场景的具体体现。
字符流本质上是字节流+编码转换,Object IO本质上是字节流的序列化应用。
字节流基础 - InputStream
InputStream 是所有字节输入流的抽象基类,定义了读取字节数据的基本规范。
📥 InputStream 抽象类的核心方法
| 函数头 | 功能 | 备注 |
|---|---|---|
public abstract int read() |
读取单个字节 | 返回 0-255,流末尾返回 -1;效率较低,通常用于读取少量数据 |
public int read(byte[] b) |
读取字节数组 | 批量读取,比单字节读取效率更高,会填充整个数组或直到流末尾 |
public int read(byte[] b, int off, int len) |
读取指定长度的字节数组 | 精确控制读取位置和长度,常用于处理协议数据或文件格式 |
public long skip(long n) |
跳过指定字节数 | 用于快速跳过不需要的数据,比读取并丢弃更高效;返回实际跳过的字节数 |
public int available() |
返回可读取的字节数 | 仅是估计值,不应依赖其精确性,用于优化读取策略 |
public void close() |
关闭流,释放系统资源 | 必须调用此方法释放文件句柄等资源,建议使用 try-with-resources |
public void mark(int readlimit) |
标记当前位置 | 并非所有流都支持标记功能,使用前应调用 markSupported() 检查 |
public void reset() |
重置到最后一次 mark() 的位置 | 如果没有调用过 mark() 或标记已失效,会抛出 IOException |
字节流基础 - OutputStream
OutputStream 是所有字节输出流的抽象基类,负责将字节数据写入目的地。
📤 OutputStream 抽象类的核心方法
| 函数头 | 功能 | 备注 |
|---|---|---|
public abstract void write(int b) |
写入单个字节 | 最基础的写入方法,每次只写一个字节,取参数低8位;效率较低 |
public void write(byte[] b) |
写入字节数组 | 将整个数组内容写入,等同于 write(b, 0, b.length) |
public void write(byte[] b, int off, int len) |
写入字节数组的一部分 | 精确控制写入内容,常用于分块写入大文件 |
public void flush() |
刷新缓冲区,强制写入 | 对于带缓冲的流(如 BufferedOutputStream)非常重要,确保数据真正落盘 |
public void close() |
关闭流,释放资源 | 关闭前通常会自动调用 flush(),但建议显式调用 flush() |
文件字节流 - FileOutputStream
FileInputStream 和 FileOutputStream 是用于文件读写字节数据的基础流类,是字节流体系中的节点流。
前面介绍的 InputStream 和 OutputStream
是抽象基类,它们定义了读写操作的基本规范,但不能直接实例化使用。实际操作中,我们需要使用这些抽象类的具体实现类来完成真正的数据读写。
FileInputStream 和 FileOutputStream
就是这两个抽象类的重要实现,它们明确指定了字节流的来源和去向都是文件。作为节点流,它们直接与文件系统交互,是构建其他功能更强大的处理流(如缓冲流、对象流等)的基础。
💡 形象理解:就像把处理数据的"水管"插进了"文件"—— FileInputStream 是出水管,从文件中取出数据;FileOutputStream 是进水管,把数据送入文件。
📤 FileOutputStream 文件写入
public static void writeFileBytes(String filePath) {
try (FileOutputStream fos = new FileOutputStream(filePath)) {
// 写入字符串数据
String text = "Hello, Java IO!";
fos.write(text.getBytes());
// 写入二进制数据
byte[] binaryData = {
(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE, // 魔数
0x01, 0x02, 0x03, 0x04, // 版本信息
0x42, 0x42, 0x42, 0x42 // 示例数据
};
fos.write(binaryData);
System.out.println("文件写入完成: " + filePath);
} catch (IOException e) {
System.err.println("写入文件失败: " + e.getMessage());
}
}
🔧 FileOutputStream 常用构造方法
// 1. 使用文件路径(覆盖模式)
FileOutputStream fos1 = new FileOutputStream("output.txt");
// 2. 使用文件路径(追加模式)
FileOutputStream fos2 = new FileOutputStream("log.txt", true);
// 3. 使用File对象
File file = new File("data.txt");
FileOutputStream fos3 = new FileOutputStream(file);
// 4. 使用File对象(追加模式)
FileOutputStream fos4 = new FileOutputStream(file, true);
⚡ 性能考虑
- 缓冲区大小:合适的缓冲区大小(通常1024-8192字节)能显著提升性能
- 直接内存访问:对于大文件,考虑使用NIO的FileChannel
- 批量操作:使用byte[]数组而非单字节操作
文件字节流 - FileInputStream
FileInputStream 用于从文件中读取字节数据,是文件字节流的输入端。配合上一节学习的 FileOutputStream,我们可以完成文件的读写操作。
📥 FileInputStream 文件读取
public static void readFileBytes(String filePath) {
try (FileInputStream fis = new FileInputStream(filePath)) {
byte[] buffer = new byte[1024];
int bytesRead;
System.out.println("开始读取文件: " + filePath);
while ((bytesRead = fis.read(buffer)) != -1) {
// 处理读取到的字节
System.out.println("读取了 " + bytesRead + " 字节");
// 示例:将字节转换为十六进制显示
for (int i = 0; i < bytesRead; i++) {
System.out.printf("%02X ", buffer[i]);
if ((i + 1) % 16 == 0) {
System.out.println();
}
}
}
} catch (IOException e) {
System.err.println("读取文件失败: " + e.getMessage());
}
}
🔧 FileInputStream 常用构造方法
// 1. 使用文件路径
FileInputStream fis1 = new FileInputStream("data.txt");
// 2. 使用File对象
File file = new File("data.txt");
FileInputStream fis2 = new FileInputStream(file);
🎯 实际应用示例:文件复制
public static void copyFile(String source, String destination) {
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(destination)) {
byte[] buffer = new byte[4096]; // 4KB缓冲区
int bytesRead;
long totalBytes = 0;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
totalBytes += bytesRead;
}
System.out.println("复制完成,共 " + totalBytes + " 字节");
} catch (IOException e) {
System.err.println("文件复制失败: " + e.getMessage());
}
}
DataOutputStream - 数据输出流
DataOutputStream 是 FilterOutputStream 的子类,用于将Java基本数据类型以与机器无关的方式写入到底层输出流中。它自动将Java数据类型转换为字节序列。
💾 数据存储原理
Java数据类型在内存中有特定的表示方式,DataOutputStream 会按照Java规范将这些数据类型转换为字节序列,确保数据可以在不同平台间正确传输和恢复。
🏗️ 类层次结构
OutputStream
└── FilterOutputStream
└── DataOutputStream
📝 核心写入方法
// 构造方法:包装一个输出流
DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.dat"));
// 写入各种Java基本数据类型
dos.writeBoolean(true); // 写入布尔值(1字节)
dos.writeByte(127); // 写入字节(1字节)
dos.writeChar('A'); // 写入字符(2字节,UTF-16)
dos.writeShort(1000); // 写入短整型(2字节)
dos.writeInt(2147483647); // 写入整型(4字节)
dos.writeLong(9223372036854775807L); // 写入长整型(8字节)
dos.writeFloat(3.14f); // 写入单精度浮点数(4字节)
dos.writeDouble(3.14159265359); // 写入双精度浮点数(8字节)
dos.writeUTF("Hello Java"); // 写入UTF-8编码的字符串
🔄 完整示例:写入和读取复杂数据
public class DataStreamExample {
public static void main(String[] args) throws IOException {
String filePath = "student.dat";
// 写入数据
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream(filePath))) {
dos.writeUTF("张三"); // 姓名
dos.writeInt(20); // 年龄
dos.writeDouble(89.5); // 语文成绩
dos.writeDouble(92.0); // 数学成绩
dos.writeBoolean(true); // 是否及格
}
// 读取数据(顺序必须与写入顺序一致)
try (DataInputStream dis = new DataInputStream(
new FileInputStream(filePath))) {
String name = dis.readUTF();
int age = dis.readInt();
double chinese = dis.readDouble();
double math = dis.readDouble();
boolean passed = dis.readBoolean();
System.out.println("姓名: " + name);
System.out.println("年龄: " + age);
System.out.println("语文: " + chinese);
System.out.println("数学: " + math);
System.out.println("及格: " + passed);
}
}
}
🎯 实际应用场景
- 配置文件存储:保存程序配置参数
- 数据序列化:将对象的基本属性持久化
- 网络传输:在Socket通信中传输结构化数据
- 临时数据缓存:将中间结果保存到文件
⚠️ 重要注意事项
- 及时刷新:重要数据写入后调用
flush() - 顺序一致性:读取时必须按照写入的顺序读取
- 格式约定:不同程序之间需要约定数据格式
- 大端字节序:Java使用大端字节序(Big-Endian)
DataInputStream - 数据输入流
DataInputStream 是 FilterInputStream 的子类,用于以与机器无关的方式读取Java基本数据类型。它可以将底层字节流中的字节"整理"成有意义的Java数据类型。
🔍 为什么需要 DataInputStream?
基本字节输入流(如 FileInputStream)只能读取单个字节或字节数组。如果要读取整数、浮点数、字符串等Java数据类型,就需要一个过滤器类来包装字节输入流,自动处理字节到数据类型的转换。
🏗️ 类层次结构
InputStream
└── FilterInputStream
└── DataInputStream
📝 核心读取方法
// 构造方法:包装一个输入流
DataInputStream dis = new DataInputStream(new FileInputStream("data.dat"));
// 读取各种Java基本数据类型
boolean flag = dis.readBoolean(); // 读取布尔值
byte b = dis.readByte(); // 读取字节
char c = dis.readChar(); // 读取字符(2字节,UTF-16)
short s = dis.readShort(); // 读取短整型(2字节)
int i = dis.readInt(); // 读取整型(4字节)
long l = dis.readLong(); // 读取长整型(8字节)
float f = dis.readFloat(); // 读取单精度浮点数(4字节)
double d = dis.readDouble(); // 读取双精度浮点数(8字节)
String str = dis.readUTF(); // 读取UTF-8编码的字符串
🔄 读取与写入的对应关系
| 读取方法 | 对应写入方法 | 字节数 |
|---|---|---|
| readBoolean() | writeBoolean() | 1 |
| readByte() | writeByte() | 1 |
| readInt() | writeInt() | 4 |
| readDouble() | writeDouble() | 8 |
| readUTF() | writeUTF() | 2 + 字符串长度 |
⚠️ 重要注意事项
- 读取顺序必须一致:数据的读取顺序必须与写入顺序完全一致
- 数据对齐:每个方法都会从当前位置读取指定字节数
- 异常处理:当到达文件末尾时会抛出
EOFException - 机器无关:数据格式独立于底层硬件平台
BufferedOutputStream - 缓冲字节写入流
BufferedOutputStream 通过内部缓冲区大幅提升写入性能,减少磁盘IO次数,是装饰器模式的典型应用。
✍️ 基本写入示例
public static void bufferedOutputStreamDemo() {
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("output.txt"))) {
String data = "Hello, BufferedOutputStream!";
byte[] bytes = data.getBytes();
bos.write(bytes);
bos.flush(); // 确保数据写入文件
System.out.println("数据写入完成");
} catch (IOException e) {
System.err.println("写入失败: " + e.getMessage());
}
}
📊 写入性能对比
public static void writePerformanceTest(String filePath, int size) {
byte[] data = new byte[size];
Arrays.fill(data, (byte) 'A');
int iterations = 100;
// 测试1:不使用缓冲流
long startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
try (FileOutputStream fos = new FileOutputStream(filePath)) {
fos.write(data);
}
}
long noBufferTime = System.currentTimeMillis() - startTime;
// 测试2:使用缓冲流
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(filePath))) {
bos.write(data);
}
}
long bufferTime = System.currentTimeMillis() - startTime;
System.out.println("无缓冲写入: " + noBufferTime + "ms");
System.out.println("缓冲写入: " + bufferTime + "ms");
System.out.println("性能提升: " + (noBufferTime / bufferTime) + "倍");
}
🔄 追加写入模式
public static void appendModeDemo() {
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("log.txt", true))) {
String logEntry = "[INFO] 日志记录内容\n";
bos.write(logEntry.getBytes());
bos.flush();
System.out.println("日志已追加");
} catch (IOException e) {
System.err.println("追加写入失败: " + e.getMessage());
}
}
🔧 缓冲区大小选择
// 默认缓冲区大小:8192字节(8KB)
BufferedOutputStream bos1 = new BufferedOutputStream(
new FileOutputStream("output.txt"));
// 自定义缓冲区大小
BufferedOutputStream bos2 = new BufferedOutputStream(
new FileOutputStream("output.txt"), 16384); // 16KB
| 场景 | 建议缓冲区大小 |
|---|---|
| 小文件(1MB以下) | 4KB - 8KB |
| 中等文件(1MB-100MB) | 8KB - 32KB |
| 大文件(100MB以上) | 32KB - 64KB |
| 网络IO | 64KB - 128KB |
⚠️ BufferedOutputStream 注意事项
- 及时刷新:关键数据要调用flush(),防止程序崩溃导致数据丢失
- 缓冲区大小权衡:过大占用内存,过小影响性能
- 装饰器顺序:缓冲流应该装饰其他流,而不是被装饰
- 关闭流时:try-with-resources会自动调用flush(),但显式调用更安全
BufferedOutputStream通过缓冲机制大幅提升写入性能,减少磁盘IO次数,是高写入场景的必备工具。
BufferedInputStream - 缓冲字节读取流
BufferedInputStream 通过内部缓冲区大幅提升读取性能,减少磁盘IO次数,支持标记重置功能。
📖 基本读取示例
public static void bufferedInputStreamDemo() {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("input.txt"))) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
String content = new String(buffer, 0, bytesRead);
System.out.print(content);
}
System.out.println("\n读取完成");
} catch (IOException e) {
System.err.println("读取失败: " + e.getMessage());
}
}
📊 读取性能对比
public static void readPerformanceTest(String filePath) {
int iterations = 1000;
// 测试1:不使用缓冲流,逐字节读取
long startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
try (FileInputStream fis = new FileInputStream(filePath)) {
while (fis.read() != -1) {}
}
}
long noBufferTime = System.currentTimeMillis() - startTime;
// 测试2:使用缓冲流,逐字节读取(从缓冲区)
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(filePath))) {
while (bis.read() != -1) {}
}
}
long bufferTime = System.currentTimeMillis() - startTime;
System.out.println("无缓冲读取: " + noBufferTime + "ms");
System.out.println("缓冲读取: " + bufferTime + "ms");
System.out.println("性能提升: " + (noBufferTime / bufferTime) + "倍");
}
🔧 标记与重置功能
public static void markAndResetDemo() {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("data.txt"))) {
byte[] buffer = new byte[10];
// 读取前10字节
bis.read(buffer);
System.out.println("首次读取: " + new String(buffer));
// 标记当前位置,允许读取最多1000字节后重置
bis.mark(1000);
// 继续读取
bis.read(buffer);
System.out.println("继续读取: " + new String(buffer));
// 重置到标记位置
bis.reset();
System.out.println("已重置到标记位置");
// 重新读取相同的内容
bis.read(buffer);
System.out.println("重置后读取: " + new String(buffer));
} catch (IOException e) {
System.err.println("标记重置失败: " + e.getMessage());
}
}
📈 批量读取优化
public static void batchReadDemo(String filePath) {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(filePath), 65536)) {
byte[] buffer = new byte[8192]; // 8KB读取缓冲区
int bytesRead;
long totalBytes = 0;
while ((bytesRead = bis.read(buffer)) != -1) {
totalBytes += bytesRead;
// 处理读取到的数据
}
System.out.println("共读取 " + totalBytes + " 字节");
} catch (IOException e) {
System.err.println("读取失败: " + e.getMessage());
}
}
🔍 查看可用字节数
public static void availableDemo(String filePath) {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(filePath))) {
int available = bis.available();
System.out.println("缓冲区中可读字节数: " + available);
// 读取部分数据后再次查看
byte[] buffer = new byte[100];
bis.read(buffer);
available = bis.available();
System.out.println("读取后可读字节数: " + available);
} catch (IOException e) {
System.err.println("操作失败: " + e.getMessage());
}
}
⚠️ BufferedInputStream 注意事项
- 标记限制:mark()后的读取量不能超过readAheadLimit
- 缓冲区大小权衡:默认8192字节,可根据需求调整
- 装饰器顺序:缓冲流应该装饰其他流,而不是被装饰
- available()方法:返回缓冲区中可读的字节数,不是文件总大小
BufferedInputStream通过缓冲机制大幅提升读取性能,支持标记重置功能,是高读取场景的必备工具。
缓冲字节流 - 最佳实践
掌握缓冲流的正确使用方法和性能优化技巧,让程序获得最佳的IO性能。
🎯 实际应用:高效文件复制(比前面更快)
public static void efficientFileCopy(String source, String target) {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(source), 65536); // 64KB读取缓冲区
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(target), 65536)) { // 64KB写入缓冲区
byte[] buffer = new byte[8192]; // 8KB工作缓冲区
int bytesRead;
long totalBytes = 0;
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
totalBytes += bytesRead;
}
bos.flush(); // 确保所有缓冲数据写入磁盘
System.out.println("复制完成: " + totalBytes + " 字节");
} catch (IOException e) {
System.err.println("文件复制失败: " + e.getMessage());
}
}
💡 缓冲流最佳实践
| 场景 | 建议 | 缓冲区大小 |
|---|---|---|
| 小文件处理 | 使用默认缓冲区 | 8KB |
| 大文件复制 | 大缓冲区+批量操作 | 32KB-64KB |
| 网络IO | 较大缓冲区 | 64KB-128KB |
| 实时数据流 | 较小缓冲区,及时flush | 4KB-8KB |
🚨 常见陷阱与解决方案
陷阱1:忘记flush()
问题:使用BufferedOutputStream时,数据可能还在缓冲区中未写入磁盘
解决:关键数据后调用flush(),或在try-with-resources结束时自动flush
陷阱2:缓冲区过大
问题:过大的缓冲区占用过多内存,可能导致GC压力
解决:根据文件大小和系统内存合理选择缓冲区大小
陷阱3:装饰器顺序错误
问题:错误的装饰器顺序可能导致缓冲失效
解决:确保缓冲流装饰基础流,而不是被其他流装饰
正确使用缓冲流可以显著提升IO性能。关键是选择合适的缓冲区大小,养成良好的使用习惯,避免常见陷阱。
字符流基础 - Reader
Reader 是所有字符输入流的抽象基类,专门用于读取文本数据,自动处理字符编码转换。
📖 Reader 抽象类的核心方法
| 函数头 | 功能 | 备注 |
|---|---|---|
public int read() |
读取单个字符 | 返回 0-65535,流末尾返回 -1;效率较低,通常用于读取少量数据 |
public int read(char[] cbuf) |
读取字符到数组 | 批量读取字符,比单字符读取效率更高,会填充整个数组或直到流末尾 |
public abstract int read(char[] cbuf, int off, int len) |
读取指定长度的字符到数组 | 精确控制读取位置和长度,常用于处理协议数据或文件格式 |
public long skip(long n) |
跳过指定数量的字符 | 用于快速跳过不需要的字符,比读取并丢弃更高效;返回实际跳过的字符数 |
public boolean ready() |
判断是否可以读取 | 返回 true 表示不阻塞即可读取,并非所有流都支持 |
public void mark(int readAheadLimit) |
标记当前位置 | readAheadLimit 指定在标记失效前最多可读取的字符数;并非所有流都支持 |
public void reset() |
重置到最后一次 mark() 的位置 | 如果没有调用过 mark() 或标记已失效,会抛出 IOException |
public boolean markSupported() |
是否支持标记功能 | 返回 true 表示该流支持 mark() 和 reset() 方法 |
public abstract void close() |
关闭流,释放资源 | 必须调用此方法释放文件句柄等资源,建议使用 try-with-resources |
字符流基础 - Writer
Writer 是所有字符输出流的抽象基类,用于将字符数据写入目标,底层通常涉及字符到字节的编码转换。
✍️ Writer 抽象类的核心方法
| 函数头 | 功能 | 备注 |
|---|---|---|
public void write(int c) |
写入单个字符 | 写入字符的低16位,效率较低,通常用于写入少量数据 |
public void write(char[] cbuf) |
写入字符数组 | 批量写入字符,比单字符写入效率更高,等同于 write(cbuf, 0, cbuf.length) |
public abstract void write(char[] cbuf, int off, int len) |
写入字符数组的一部分 | 从指定偏移量开始写入指定长度的字符,精确控制写入内容 |
public void write(String str) |
写入字符串 | 直接写入字符串,非常方便,底层会转换为字符数组处理 |
public void write(String str, int off, int len) |
写入字符串的一部分 | 从指定偏移量开始写入指定长度的字符,常用于截取写入 |
public Writer append(CharSequence csq) |
追加字符序列 | 支持链式调用,返回 Writer 本身,方便连续写入 |
public abstract void flush() |
刷新缓冲区,强制写入 | 将缓冲区的字符编码并写入底层流,对于带缓冲的流非常重要 |
public abstract void close() |
关闭流,释放资源 | 关闭前会自动调用 flush(),释放文件句柄等系统资源 |
字符流是处理文本数据的最佳选择,自动处理编码转换,支持Unicode字符。它本质上是"字节流 + 编码表"的封装,专门用于Text IO场景。
文件字符流 - FileWriter
FileWriter 是专门用于向文件写入字符数据的便捷类,是字符流体系中的节点流。
✍️ FileWriter 文件写入
public static void writeTextFile(String filePath) {
try (FileWriter writer = new FileWriter(filePath)) {
// 写入单行文本
writer.write("Java IO字符流示例\n");
// 写入多行文本
writer.write("这是第二行内容\n");
writer.write("中文字符测试:你好世界!\n");
// 写入字符数组
char[] charArray = {'H', 'e', 'l', 'l', 'o', ' ', 'C', 'h', 'a', 'r', 's'};
writer.write(charArray);
// 写入字符串的一部分
String longText = "这是一个很长的字符串,我们只写入其中的一部分内容";
writer.write(longText, 0, 10); // 只写入前10个字符
writer.flush(); // 确保数据写入文件
System.out.println("文件写入完成: " + filePath);
} catch (IOException e) {
System.err.println("写入文件失败: " + e.getMessage());
}
}
🔧 FileWriter 常用构造方法
// FileWriter构造方法
FileWriter fw1 = new FileWriter("output.txt"); // 覆盖模式
FileWriter fw2 = new FileWriter("output.txt", true); // 追加模式
FileWriter fw3 = new FileWriter(new File("output.txt")); // 使用File对象
FileWriter fw4 = new FileWriter(new File("output.txt"), true); // 追加模式
🌍 编码注意事项
// FileWriter的编码限制
try {
// 问题:FileWriter使用系统默认编码,可能不准确
FileWriter writer = new FileWriter("chinese.txt"); // Windows可能用GBK,Linux可能用UTF-8
// 解决方案:使用OutputStreamWriter指定编码
Writer writer = new OutputStreamWriter(
new FileOutputStream("output.txt"), StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
⚠️ FileWriter 重要注意事项
- 编码问题:FileWriter使用系统默认编码,跨平台可能不一致
- 文件追加:FileWriter构造时指定true参数启用追加模式
- 异常处理:始终使用try-with-resources确保流正确关闭
- 及时刷新:重要数据写入后调用 flush() 确保落盘
FileWriter是处理文本文件写入的便捷选择。对于需要特定编码的场景,建议使用OutputStreamWriter。
PrintWriter - 优雅的文本写入流
PrintWriter 是 Java 为我们准备的一套"专业木工工具套装"。它不仅仅是写入,更是"打印"(Print),能让我们像在控制台输出一样,优雅、格式化地操作文件。
💡 为什么需要 PrintWriter?
FileWriter 的问题:换行要手动写 \n,写入数值要先转字符串,还得处理没完没了的 IOException。
PrintWriter 的优势:像 System.out 一样简单,支持 print/println/printf,不强制捕获异常。
📘 PrintWriter 核心特性
- 包装流:不直接连接文件,而是包装其他的 Writer 或 OutputStream
- 方法丰富:支持 print()、println()、printf() 等多种输出方式
- 异常静默:内部捕获异常,代码更清爽
- 自动换行:println() 自动适配操作系统的换行符
🏗️ 三种常见构造方式
// 1. 直接关联文件(覆盖原文件)
PrintWriter out1 = new PrintWriter("data.txt");
// 2. 包装已有的Writer(实现追加模式)
PrintWriter out2 = new PrintWriter(new FileWriter("log.txt", true));
// 3. 包装字节流并开启自动刷新
PrintWriter out3 = new PrintWriter(outputStream, true); // true表示println时自动flush
📊 FileWriter vs PrintWriter 对比
| 功能 | FileWriter | PrintWriter(推荐) |
|---|---|---|
| 写入并换行 | fw.write("Hello\n"); |
out.println("Hello"); |
| 混合类型写入 | fw.write(String.valueOf(99.5)); |
out.print(99.5); |
| 格式化输出 | 无 | out.printf("学号:%d, 分数:%.1f", id, score); |
💻 代码示例:制作学生成绩单
import java.io.*;
public class PrintWriterDemo {
public static void main(String[] args) {
try (PrintWriter out = new PrintWriter(new FileWriter("report.txt"))) {
out.println("========= 期末成绩单 =========");
out.println("姓名\t数学\t英语\t总分");
String name = "张三";
int math = 95;
int english = 88;
// 使用 printf 格式化对齐
out.printf("%s\t%d\t%d\t%d\n", name, math, english, (math + english));
out.println("打印时间: " + new java.util.Date());
System.out.println("成绩单生成成功!");
} catch (IOException e) {
System.err.println("发生错误: " + e.getMessage());
}
}
}
⚠️ 重要注意事项
- Flush 的重要性:PrintWriter 内部有缓冲区,程序结束前必须调用 close() 或 flush(),否则数据可能还在内存中
- 异常检查:虽然不强制 catch 异常,但可通过
out.checkError()判断是否发生错误 - 追加与覆盖:直接 new PrintWriter(File) 会清空原文件,想追加必须配合
FileWriter(file, true) - 自动刷新:构造时传入 true 参数,println 后自动 flush,适合日志输出场景
PrintWriter 是文本写入的首选工具,让文件操作像控制台输出一样简单优雅。
文件字符流 - FileReader
FileReader 是专门用于从文件读取字符数据的便捷类,是字符流体系中的节点流。
📖 FileReader 文件读取
public static void readTextFile(String filePath) {
try (FileReader reader = new FileReader(filePath)) {
char[] buffer = new char[1024];
int charsRead;
StringBuilder content = new StringBuilder();
System.out.println("开始读取文本文件: " + filePath);
while ((charsRead = reader.read(buffer)) != -1) {
// 将读取的字符追加到StringBuilder
content.append(buffer, 0, charsRead);
System.out.println("读取了 " + charsRead + " 个字符");
}
System.out.println("文件内容:");
System.out.println(content.toString());
} catch (IOException e) {
System.err.println("读取文件失败: " + e.getMessage());
}
}
🔧 FileReader 常用构造方法
// FileReader构造方法
FileReader fr1 = new FileReader("input.txt"); // 使用文件路径
FileReader fr2 = new FileReader(new File("input.txt")); // 使用File对象
🌍 编码注意事项
// FileReader的编码限制
try {
// 问题:FileReader使用系统默认编码,可能不准确
FileReader reader = new FileReader("chinese.txt"); // Windows可能用GBK,Linux可能用UTF-8
// 解决方案:使用InputStreamReader指定编码
Reader reader = new InputStreamReader(
new FileInputStream("chinese.txt"), StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
🎯 实际应用示例
// 配置文件读取器
public static Map readConfigFile(String configPath) {
Map config = new HashMap<>();
try (FileReader reader = new FileReader(configPath)) {
char[] buffer = new char[1024];
StringBuilder content = new StringBuilder();
// 读取整个文件内容
int charsRead;
while ((charsRead = reader.read(buffer)) != -1) {
content.append(buffer, 0, charsRead);
}
// 解析配置项
String[] lines = content.toString().split("\n");
for (String line : lines) {
line = line.trim();
if (line.contains("=") && !line.startsWith("#")) {
String[] parts = line.split("=", 2);
config.put(parts[0].trim(), parts[1].trim());
}
}
} catch (IOException e) {
System.err.println("读取配置文件失败: " + e.getMessage());
}
return config;
}
⚠️ FileReader 重要注意事项
- 编码问题:FileReader使用系统默认编码,跨平台可能不一致
- 缓冲区大小:合适的字符缓冲区能提升性能(通常1024-8192字符)
- 异常处理:始终使用try-with-resources确保流正确关闭
- 流末尾判断:read()方法返回-1表示到达流末尾
FileReader是处理文本文件读取的便捷选择。对于需要特定编码的场景,建议使用InputStreamReader。
BufferedWriter - 缓冲字符写入流
BufferedWriter 为字符写入提供缓冲功能,大幅提升文本写入性能,支持跨平台换行符处理。
✍️ BufferedWriter 核心功能
public static void bufferedWriterDemo() {
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("output.txt"))) {
// 写入多行文本
writer.write("第一行内容");
writer.newLine(); // 换行符,跨平台兼容
writer.write("第二行内容");
writer.newLine();
// 批量写入
String[] lines = {
"Java IO缓冲字符流示例",
"支持多种写入方式",
"自动处理换行符"
};
for (String line : lines) {
writer.write(line);
writer.newLine();
}
writer.flush(); // 确保数据写入文件
System.out.println("文件写入完成");
} catch (IOException e) {
System.err.println("写入文件失败: " + e.getMessage());
}
}
📝 写入方法详解
public static void writeMethodsDemo() {
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("demo.txt"))) {
// 方法1:写入单个字符
writer.write('J');
writer.write('a');
writer.write('v');
writer.write('a');
// 方法2:写入字符数组
char[] chars = {' ', 'I', '/', 'O'};
writer.write(chars);
// 方法3:写入字符数组的部分
writer.write(chars, 2, 3); // 从索引2开始,写入3个字符
// 方法4:写入字符串
writer.write("\n字符串写入");
// 方法5:写入字符串的部分
writer.write("部分字符串", 0, 2); // 写入前两个字符
writer.newLine();
writer.flush();
} catch (IOException e) {
System.err.println("写入失败: " + e.getMessage());
}
}
💾 缓冲区机制
public static void bufferMechanismDemo() {
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("buffer.txt"), 8192)) {
// 数据先写入内部缓冲区(8192字符)
for (int i = 0; i < 10000; i++) {
writer.write("Line " + i + ": 数据内容\n");
}
// 缓冲区满时自动刷新到文件
// 但在关键点手动刷新确保数据安全
writer.flush();
System.out.println("写入完成,共10000行");
} catch (IOException e) {
System.err.println("写入失败: " + e.getMessage());
}
}
🔄 追加写入模式
public static void appendModeDemo() {
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("log.txt", true))) {
// FileWriter的第二个参数true表示追加模式
String timestamp = java.time.LocalDateTime.now().toString();
writer.write("[" + timestamp + "] 日志记录内容");
writer.newLine();
writer.flush();
System.out.println("日志已追加");
} catch (IOException e) {
System.err.println("追加写入失败: " + e.getMessage());
}
}
⚠️ BufferedWriter 注意事项
- 换行符处理:使用newLine()而非"\n",确保跨平台兼容
- 及时刷新:在关键数据后调用flush(),防止程序崩溃导致数据丢失
- 缓冲区大小:默认8192字符,可根据需求调整(过小影响性能,过大占用内存)
- 追加模式:使用FileWriter(file, true)实现追加写入
BufferedWriter通过缓冲机制大幅提升写入性能,newLine()方法确保跨平台换行符兼容性。
BufferedReader - 缓冲字符读取流
BufferedReader 为字符读取提供缓冲功能,大幅提升文本处理性能,支持便捷的按行读取和标记重置功能。
📖 按行读取 - 核心优势
public static void bufferedReaderDemo() {
try (BufferedReader reader = new BufferedReader(
new FileReader("multilines.txt"))) {
String line;
int lineNumber = 0;
// 按行读取:BufferedReader的核心优势
while ((line = reader.readLine()) != null) {
lineNumber++;
System.out.println("第" + lineNumber + "行: " + line);
}
System.out.println("总共读取了 " + lineNumber + " 行");
} catch (IOException e) {
System.err.println("读取文件失败: " + e.getMessage());
}
}
🔧 标记与重置功能
public static void markAndResetDemo() {
try (BufferedReader reader = new BufferedReader(
new FileReader("sample.txt"))) {
// 读取第一行
String line1 = reader.readLine();
System.out.println("第一行: " + line1);
// 标记当前位置
reader.mark(1024); // 标记,允许读取1024字符后重置
// 继续读取几行
System.out.println("后续内容:");
for (int i = 0; i < 3 && reader.ready(); i++) {
System.out.println(" " + reader.readLine());
}
// 重置到标记位置
reader.reset();
System.out.println("重置后重新读取:");
// 再次读取标记后的内容
String line = reader.readLine();
while (line != null) {
System.out.println(" " + line);
line = reader.readLine();
}
} catch (IOException e) {
System.err.println("标记重置操作失败: " + e.getMessage());
}
}
🚀 性能对比示例
public static void performanceComparison(String filePath) {
int iterations = 100;
// 测试1:不使用缓冲流
long startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
try (FileReader reader = new FileReader(filePath)) {
StringBuilder content = new StringBuilder();
char[] buffer = new char[1024];
int charsRead;
while ((charsRead = reader.read(buffer)) != -1) {
content.append(buffer, 0, charsRead);
}
}
}
long noBufferTime = System.currentTimeMillis() - startTime;
// 测试2:使用缓冲流
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
try (BufferedReader reader = new BufferedReader(
new FileReader(filePath))) {
StringBuilder content = new StringBuilder();
char[] buffer = new char[1024];
int charsRead;
while ((charsRead = reader.read(buffer)) != -1) {
content.append(buffer, 0, charsRead);
}
}
}
long bufferTime = System.currentTimeMillis() - startTime;
System.out.println("无缓冲字符流: " + noBufferTime + "ms");
System.out.println("缓冲字符流: " + bufferTime + "ms");
System.out.println("性能提升: " + (noBufferTime / bufferTime) + "倍");
}
⚠️ BufferedReader 注意事项
- 标记限制:mark()后的读取量不能超过readAheadLimit
- 性能权衡:缓冲区过小影响性能,过大占用内存(默认8192字符)
- ready()方法:判断流是否可读,避免阻塞
- 流关闭:始终使用try-with-resources确保流正确关闭
BufferedReader的核心优势在于按行读取和标记重置功能,大大简化了文本处理逻辑。
缓冲字符流 - 实际应用
掌握缓冲字符流在实际项目中的应用场景,处理日志、配置、CSV等文本文件的最佳实践。
🔧 实际应用:CSV文件按行处理
// CSV文件处理器
public static void processCSVFile(String csvPath) {
try (BufferedReader reader = new BufferedReader(
new FileReader(csvPath))) {
String headerLine = reader.readLine(); // 读取表头
String[] headers = headerLine.split(",");
List
🚨 常见应用陷阱
陷阱1:大文件内存溢出
问题:一次性读取大文件到内存可能导致OutOfMemoryError
解决:使用BufferedReader逐行处理,避免一次性加载
陷阱2:编码不一致
问题:文件编码与读取编码不匹配导致乱码
解决:使用InputStreamReader指定正确编码
陷阱3:异常处理不当
问题:读取过程中异常可能导致资源泄漏
解决:使用try-with-resources确保流正确关闭
缓冲字符流是文本处理的利器,按行读取功能大大简化了文本处理逻辑,是处理日志、配置、CSV等文本文件的理想选择。
* 转换流 - 基础用法
InputStreamReader 和 OutputStreamWriter 是字节流与字符流之间的桥梁,负责编码转换,是处理不同字符编码的关键。
📖 InputStreamReader 字节流转字符流
// 基本用法:指定编码读取文件
public static void readWithEncoding(String filePath, String encoding) {
try (InputStreamReader reader = new InputStreamReader(
new FileInputStream(filePath), encoding)) {
char[] buffer = new char[1024];
int charsRead;
StringBuilder content = new StringBuilder();
while ((charsRead = reader.read(buffer)) != -1) {
content.append(buffer, 0, charsRead);
}
System.out.println("使用 " + encoding + " 编码读取的内容:");
System.out.println(content.toString());
} catch (IOException e) {
System.err.println("读取失败: " + e.getMessage());
}
}
✍️ OutputStreamWriter 字符流转字节流
// 基本用法:指定编码写入文件
public static void writeWithEncoding(String filePath, String encoding) {
try (OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream(filePath), encoding)) {
writer.write("多语言文本示例\n");
writer.write("English: Hello World\n");
writer.write("中文: 你好世界\n");
writer.write("日本語: こんにちは世界\n");
writer.write("한국어: 안녕하세요 세계\n");
writer.write("Español: Hola Mundo\n");
writer.flush();
System.out.println("使用 " + encoding + " 编码写入完成");
} catch (IOException e) {
System.err.println("写入失败: " + e.getMessage());
}
}
🌍 常用编码示例
public static void encodingExamples() {
String filePath = "multilang.txt";
// UTF-8 编码(推荐)
readWithEncoding(filePath, StandardCharsets.UTF_8.name());
// GBK 编码(中文Windows常用)
readWithEncoding(filePath, "GBK");
// ISO-8859-1 编码(西欧)
readWithEncoding(filePath, "ISO-8859-1");
// UTF-16 编码
readWithEncoding(filePath, "UTF-16");
}
⚠️ 重要注意事项
- 编码一致性:读写时使用相同的编码,避免乱码
- 异常处理:编码可能不支持,需捕获UnsupportedEncodingException
- 性能考虑:转换流会进行编码转换,影响性能
- BOM处理:注意处理UTF-8/16的BOM标记
转换流是处理字符编码的核心工具,正确使用转换流能确保字符数据的准确性。
* 转换流 - 编码检测与处理
掌握文件编码的自动检测技术和智能读取方法,解决不同编码文件的兼容性问题。
🔍 自动检测文件编码
// 自动检测文件编码(简单示例)
public static String detectEncoding(String filePath) {
try (FileInputStream fis = new FileInputStream(filePath)) {
byte[] bom = new byte[4];
int bytesRead = fis.read(bom);
if (bytesRead >= 3 && bom[0] == (byte) 0xEF &&
bom[1] == (byte) 0xBB && bom[2] == (byte) 0xBF) {
return "UTF-8";
}
if (bytesRead >= 2 && bom[0] == (byte) 0xFE &&
bom[1] == (byte) 0xFF) {
return "UTF-16BE";
}
if (bytesRead >= 2 && bom[0] == (byte) 0xFF &&
bom[1] == (byte) 0xFE) {
return "UTF-16LE";
}
// 默认使用系统编码或UTF-8
return StandardCharsets.UTF_8.name();
} catch (IOException e) {
return StandardCharsets.UTF_8.name();
}
}
🧠 智能编码读取
// 智能编码读取
public static void smartRead(String filePath) {
String detectedEncoding = detectEncoding(filePath);
System.out.println("检测到编码: " + detectedEncoding);
try (InputStreamReader reader = new InputStreamReader(
new FileInputStream(filePath), detectedEncoding)) {
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("智能读取失败: " + e.getMessage());
}
}
🔄 编码转换工具
// 文件编码转换工具
public static void convertEncoding(String sourceFile, String targetFile,
String sourceEncoding, String targetEncoding) {
try (InputStreamReader reader = new InputStreamReader(
new FileInputStream(sourceFile), sourceEncoding);
OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream(targetFile), targetEncoding)) {
char[] buffer = new char[4096];
int charsRead;
System.out.println("开始编码转换: " + sourceEncoding + " -> " + targetEncoding);
while ((charsRead = reader.read(buffer)) != -1) {
writer.write(buffer, 0, charsRead);
}
writer.flush();
System.out.println("编码转换完成: " + targetFile);
} catch (IOException e) {
System.err.println("编码转换失败: " + e.getMessage());
}
}
📦 批量编码转换
// 批量编码转换
public static void batchConvertEncoding(String sourceDir, String targetDir,
String sourceEncoding, String targetEncoding) {
File sourceDirectory = new File(sourceDir);
File targetDirectory = new File(targetDir);
if (!targetDirectory.exists()) {
targetDirectory.mkdirs();
}
File[] files = sourceDirectory.listFiles((dir, name) ->
name.endsWith(".txt") || name.endsWith(".csv"));
if (files != null) {
for (File file : files) {
String sourcePath = file.getPath();
String targetPath = new File(targetDir, file.getName()).getPath();
convertEncoding(sourcePath, targetPath, sourceEncoding, targetEncoding);
}
}
}
🌐 常见编码问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 中文乱码 | 编码不匹配 | 指定UTF-8或GBK编码 |
| BOM字符显示 | UTF-8 BOM处理 | 跳过前3个字节或使用UTF-8-sig |
| 部分字符显示异常 | 字符集不包含 | 使用Unicode编码(UTF-8/16) |
| 跨平台编码问题 | 系统默认编码不同 | 始终指定具体编码 |
编码检测和转换是处理多语言文本的关键技术,能显著提升程序的兼容性和健壮性。
* 转换流 - 最佳实践
掌握转换流在实际开发中的最佳实践,处理网络流、文件操作等复杂场景。
💡 推荐的文本文件处理模式
public static void bestPracticeTextProcessing(String filePath) {
// 1. 检测或指定编码
String encoding = detectEncoding(filePath);
// 2. 使用转换流 + 缓冲流的组合
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream(filePath), encoding))) {
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
lineNumber++;
// 处理每一行文本
processTextLine(line, lineNumber);
}
} catch (IOException e) {
System.err.println("文本处理失败: " + e.getMessage());
}
}
🌐 网络流的编码处理
public static void handleNetworkStream(InputStream inputStream, String encoding) {
try (InputStreamReader reader = new InputStreamReader(inputStream, encoding);
BufferedReader bufferedReader = new BufferedReader(reader)) {
String response = bufferedReader.readLine();
System.out.println("服务器响应: " + response);
// 处理多行响应
while ((response = bufferedReader.readLine()) != null) {
if (response.trim().isEmpty()) break; // 空行表示响应结束
processResponseLine(response);
}
} catch (IOException e) {
System.err.println("网络流处理失败: " + e.getMessage());
}
}
📁 文件编码转换工具类
public class EncodingConverter {
public static void convertFileToUTF8(String sourcePath, String targetPath) {
try {
String sourceEncoding = detectEncoding(sourcePath);
try (InputStreamReader reader = new InputStreamReader(
new FileInputStream(sourcePath), sourceEncoding);
OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream(targetPath), "UTF-8")) {
char[] buffer = new char[8192];
int charsRead;
while ((charsRead = reader.read(buffer)) != -1) {
writer.write(buffer, 0, charsRead);
}
writer.flush();
System.out.println("文件已转换为UTF-8: " + targetPath);
}
} catch (IOException e) {
System.err.println("转换失败: " + e.getMessage());
}
}
public static void convertDirectoryToUTF8(String sourceDir, String targetDir) {
File source = new File(sourceDir);
File target = new File(targetDir);
if (!target.exists()) {
target.mkdirs();
}
File[] files = source.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
String targetPath = new File(targetDir, file.getName()).getPath();
convertFileToUTF8(file.getPath(), targetPath);
}
}
}
}
}
🚨 常见陷阱与解决方案
陷阱1:编码转换失败
问题:某些字符在目标编码中不存在
解决:使用Unicode编码(UTF-8/16)作为目标编码
陷阱2:BOM处理不当
问题:UTF-8 BOM在前端显示为乱码
解决:检测并跳过BOM字节,或使用无BOM的UTF-8
陷阱3:性能问题
问题:大文件编码转换性能较差
解决:使用大缓冲区和批量处理
🎯 实际应用场景
🌍 国际化应用
- 多语言界面显示
- 本地化文件处理
- 字符集自动适配
📡 网络编程
- HTTP响应处理
- Socket通信编码
- 协议数据解析
📁 文件处理
- 历史文件迁移
- 编码格式统一
- 批量文件转换
🔧 数据处理
- CSV文件导入
- XML/JSON解析
- 数据库字符集处理
转换流是处理多语言文本和编码转换的核心工具。在网络编程、文件处理等场景中,正确使用转换流能确保字符数据的准确性。
装饰器模式 - 核心概念
Java IO体系是装饰器模式的经典实现。
层层包装流对象,动态添加功能,而不改变原有类的结构。
☕ 生活场景:咖啡店点餐
现在我们使用代码来模拟咖啡店点餐的过程
如果使用继承,每增加一种配料组合,都需要创建一个新类,代码爆炸!
// 如果不用装饰器模式,而是用继承,会这样写:
// 基础咖啡
class SimpleCoffee {
public String getDescription() { return "黑咖啡"; }
public double getCost() { return 10.0; }
}
// 加牛奶的咖啡
class CoffeeWithMilk extends SimpleCoffee {
public String getDescription() { return "黑咖啡 + 牛奶"; }
public double getCost() { return 13.0; }
}
// 加糖浆的咖啡
class CoffeeWithSyrup extends SimpleCoffee {
public String getDescription() { return "黑咖啡 + 糖浆"; }
public double getCost() { return 12.0; }
}
// 加奶泡的咖啡
class CoffeeWithFoam extends SimpleCoffee {
public String getDescription() { return "黑咖啡 + 奶泡"; }
public double getCost() { return 15.0; }
}
// 问题来了!如果用户要"黑咖啡 + 牛奶 + 糖浆"?
class CoffeeWithMilkAndSyrup extends SimpleCoffee {
public String getDescription() { return "黑咖啡 + 牛奶 + 糖浆"; }
public double getCost() { return 15.0; }
}
// "黑咖啡 + 牛奶 + 奶泡"?
class CoffeeWithMilkAndFoam extends SimpleCoffee {
public String getDescription() { return "黑咖啡 + 牛奶 + 奶泡"; }
public double getCost() { return 18.0; }
}
// "黑咖啡 + 糖浆 + 奶泡"?
class CoffeeWithSyrupAndFoam extends SimpleCoffee {
public String getDescription() { return "黑咖啡 + 糖浆 + 奶泡"; }
public double getCost() { return 17.0; }
}
// "黑咖啡 + 牛奶 + 糖浆 + 奶泡"?
class CoffeeWithMilkAndSyrupAndFoam extends SimpleCoffee {
public String getDescription() { return "黑咖啡 + 牛奶 + 糖浆 + 奶泡"; }
public double getCost() { return 20.0; }
}
// 如果有4种配料,组合数量 = 2^4 - 1 = 15种!
// 如果有10种配料,组合数量 = 2^10 - 1 = 1023种!
// 代码爆炸!根本无法维护!
而装饰器模式就像咖啡店点咖啡:基础咖啡可以一层层加配料,每加一层就获得新功能。
// 咖啡接口(抽象组件)
interface Coffee {
String getDescription();
double getCost();
}
// 基础咖啡(具体组件)
class SimpleCoffee implements Coffee {
public String getDescription() {
return "黑咖啡";
}
public double getCost() {
return 10.0;
}
}
// 咖啡装饰器(抽象装饰器)
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
public String getDescription() {
return coffee.getDescription();
}
public double getCost() {
return coffee.getCost();
}
}
// 牛奶装饰(具体装饰器)
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return coffee.getDescription() + " + 牛奶";
}
public double getCost() {
return coffee.getCost() + 3.0;
}
}
// 糖浆装饰(具体装饰器)
class SyrupDecorator extends CoffeeDecorator {
public SyrupDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return coffee.getDescription() + " + 糖浆";
}
public double getCost() {
return coffee.getCost() + 2.0;
}
}
// 奶泡装饰(具体装饰器)
class FoamDecorator extends CoffeeDecorator {
public FoamDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return coffee.getDescription() + " + 奶泡";
}
public double getCost() {
return coffee.getCost() + 5.0;
}
}
// 使用装饰器:点一杯复杂的咖啡
public static void coffeeShopExample() {
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " = ¥" + coffee.getCost());
// 输出: 黑咖啡 = ¥10.0
// 加牛奶
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " = ¥" + coffee.getCost());
// 输出: 黑咖啡 + 牛奶 = ¥13.0
// 加糖浆
coffee = new SyrupDecorator(coffee);
System.out.println(coffee.getDescription() + " = ¥" + coffee.getCost());
// 输出: 黑咖啡 + 牛奶 + 糖浆 = ¥15.0
// 加奶泡
coffee = new FoamDecorator(coffee);
System.out.println(coffee.getDescription() + " = ¥" + coffee.getCost());
// 输出: 黑咖啡 + 牛奶 + 糖浆 + 奶泡 = ¥20.0
// 也可以直接创建复杂咖啡
Coffee fancyCoffee = new FoamDecorator(
new SyrupDecorator(
new MilkDecorator(
new SimpleCoffee()
)
)
);
}
装饰器模式的核心是包装和组合,通过层层包装动态添加功能,而不改变原有类的结构。
装饰器模式 - IO流应用
Java IO体系是装饰器模式的经典实现。通过层层包装流对象,动态添加缓冲、数据转换、压缩等功能。
💎 装饰器模式的灵魂
关键设计:抽象装饰器(CoffeeDecorator)实现抽象组件(Coffee)正是这一模式的精髓所在。
// 抽象组件
interface Coffee {
String getDescription();
double getCost();
}
// 抽象装饰器:核心灵魂所在
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee; // "怀中藏":持有被装饰者(组合)
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
public String getDescription() {
return coffee.getDescription(); // "顺手帮":调用被装饰者
}
public double getCost() {
return coffee.getCost();
}
}
🎯 为什么要让装饰器实现组件接口?
- 身份一致性:因为
MilkDecorator实现了Coffee接口,所以"加了牛奶的咖啡"在类型上依然是一杯咖啡 - 无限嵌套(递归):正因为装饰器本身也是一个
Coffee,所以它可以被另一个装饰器再次包装
// 俄罗斯套娃效应:每一层外面包一层,但最外面看起来始终还是个套娃
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee); // 得到的是 Coffee
coffee = new SyrupDecorator(coffee); // 得到的还是 Coffee
coffee = new FoamDecorator(coffee); // 得到的还是 Coffee
// 或者直接嵌套创建
Coffee fancy = new FoamDecorator(
new SyrupDecorator(
new MilkDecorator(new SimpleCoffee())
)
);
🎭 装饰器模式的"二重身份"
| 身份 | 实现方式 | 目的 | 对应咖啡代码 |
|---|---|---|---|
| 是一个(is-a) | 实现接口/继承父类 | 获得类型,让外界认得我 | class MilkDecorator implements Coffee |
| 有一个(has-a) | 组合(成员变量) | 扩展功能,调用被包装对象 | protected Coffee coffee; |
🔗 连接到 Java I/O
| 角色 | 咖啡例子 | Java I/O 例子 |
|---|---|---|
| 抽象组件 | Coffee |
Writer (抽象类) |
| 具体组件 | SimpleCoffee |
FileWriter |
| 抽象装饰器 | CoffeeDecorator |
FilterWriter |
| 具体装饰器 | MilkDecorator |
BufferedWriter / PrintWriter |
// BufferedWriter 继承了 Writer,同时内部又持有一个 Writer 对象
// 一层套一层,每一层都增强了功能,但对外依然是一个 Writer
Writer writer = new PrintWriter(
new BufferedWriter(
new FileWriter("test.txt")
)
);
🎓 核心三部曲(记忆口诀)
1️⃣ 同根生
装饰器类和原始类必须实现相同的接口(或继承相同的父类)
2️⃣ 怀中藏
装饰器类中要维护一个私有的被装饰者成员变量
3️⃣ 顺手帮
装饰器在调用被装饰者的方法前后,添加自己的额外逻辑
💡 教学思考:双份牛奶怎么写?
// 用继承方式:需要新建一个 DoubleMilkCoffee 类
class DoubleMilkCoffee extends SimpleCoffee {
public String getDescription() { return "黑咖啡 + 双份牛奶"; }
public double getCost() { return 16.0; }
}
// 用装饰器方式:直接嵌套两个 MilkDecorator
Coffee doubleMilk = new MilkDecorator(
new MilkDecorator(new SimpleCoffee())
);
// 动态扩展的魅力就是装饰器模式存在的意义!
🔄 IO流的层层装饰
// 装饰器模式的典型应用:多层包装
public static void decoratorPatternExample() {
// 基础组件:文件字节流
FileInputStream fileStream = new FileInputStream("data.txt");
// 第一层装饰:缓冲功能
BufferedInputStream bufferedStream = new BufferedInputStream(fileStream);
// 第二层装饰:数据类型转换
DataInputStream dataStream = new DataInputStream(bufferedStream);
// 第三层装饰:如果需要,还可以继续包装
// PushbackInputStream pushbackStream = new PushbackInputStream(dataStream);
// 现在可以使用所有装饰过的功能
int intValue = dataStream.readInt(); // DataInputStream的功能
bufferedStream.mark(1000); // BufferedInputStream的功能
fileStream.available(); // FileInputStream的功能
}
🏗️ 装饰器模式的特点
🔄 动态组合
运行时动态添加功能,无需修改原有代码
👁️ 透明性
装饰后的对象仍可当作原类型使用
🔧 功能增强
不修改原有类结构,通过包装增强功能
📦 可组合性
多个装饰器可以任意组合使用
🎯 Java IO中的装饰器体系
| 角色类型 | 字节流 (Byte) | 字符流 (Char) | 说明 |
|---|---|---|---|
| 抽象组件 | InputStream, OutputStream | Reader, Writer | 定义核心读写接口 |
| 具体组件 | FileInputStream, ByteArrayInputStream, SocketInputStream | FileReader, StringReader, CharArrayReader | 提供基础数据源(文件/内存/网络) |
| 抽象装饰器 | FilterInputStream, FilterOutputStream | FilterReader, FilterWriter | 装饰器基类,持有组件引用 |
| 具体装饰器 | BufferedInputStream, DataInputStream, GZIPInputStream | BufferedReader, LineNumberReader, PushbackReader | 添加缓冲/数据格式/压缩等功能 |
通过装饰器模式,Java IO实现了高度的灵活性和可扩展性,开发者可以自由组合各种流来满足不同的需求。
装饰器模式 - 自定义装饰器
学习如何创建自定义装饰器,为IO流添加特定功能,扩展Java IO的能力。
🏗️ 自定义装饰器示例
// 自定义装饰器:统计读取的字节数
public class CountingInputStream extends FilterInputStream {
private long bytesRead = 0;
public CountingInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int data = super.read();
if (data != -1) {
bytesRead++;
}
return data;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int count = super.read(b, off, len);
if (count != -1) {
bytesRead += count;
}
return count;
}
public long getBytesRead() {
return bytesRead;
}
public void resetCount() {
bytesRead = 0;
}
}
💡 更多自定义装饰器示例
// 1. 加密装饰器
public class EncryptingOutputStream extends FilterOutputStream {
private final byte[] key;
public EncryptingOutputStream(OutputStream out, String key) {
super(out);
this.key = key.getBytes(StandardCharsets.UTF_8);
}
@Override
public void write(int b) throws IOException {
byte encrypted = (byte) (b ^ key[0]);
super.write(encrypted);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
for (int i = 0; i < len; i++) {
write(b[off + i]);
}
}
}
// 2. 压缩装饰器
public class CompressingOutputStream extends FilterOutputStream {
private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public CompressingOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
buffer.write(b, off, len);
}
@Override
public void flush() throws IOException {
byte[] compressed = compress(buffer.toByteArray());
super.write(compressed);
super.flush();
buffer.reset();
}
private byte[] compress(byte[] data) {
// 简单的压缩实现(示例)
return data; // 实际应用中使用GZIP等算法
}
}
🎯 使用自定义装饰器
// 使用统计装饰器
public static void useCountingDecorator() {
try (CountingInputStream countingStream =
new CountingInputStream(new FileInputStream("largefile.dat"))) {
byte[] buffer = new byte[1024];
while (countingStream.read(buffer) != -1) {
// 处理数据
}
System.out.println("总共读取了 " + countingStream.getBytesRead() + " 字节");
} catch (IOException e) {
e.printStackTrace();
}
}
// 组合多个装饰器
public static void useMultipleDecorators(String filePath) {
try (InputStream input = new CountingInputStream(
new EncryptingInputStream(
new BufferedInputStream(
new FileInputStream(filePath))))) {
byte[] buffer = new byte[1024];
while (input.read(buffer) != -1) {
// 数据经过:文件读取 → 缓冲 → 加密 → 统计
}
} catch (IOException e) {
e.printStackTrace();
}
}
🚨 自定义装饰器注意事项
注意1:保持接口一致性
要点:确保装饰器不会改变原有接口的行为约定
注意2:正确处理异常
要点:装饰器中的异常处理应该保持一致性和透明性
注意3:资源管理
要点:装饰器不应该干扰底层的资源管理机制
自定义装饰器是扩展Java IO功能的强大方式,可以根据业务需求添加特定的处理逻辑。
对象序列化 Object IO
Java序列化机制将对象转换为字节流,便于存储或传输。Serializable接口和ObjectOutputStream/ObjectInputStream是实现对象序列化的核心。
🏷️ Serializable接口
要使一个类的对象可序列化,必须实现Serializable接口。这是一个“标记接口”,没有任何方法需要实现。
// 实现Serializable接口
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private double gpa;
private transient String password; // transient字段不会被序列化
public Student(String name, int age, double gpa, String password) {
this.name = name;
this.age = age;
this.gpa = gpa;
this.password = password;
}
@Override
public String toString() {
return String.format("Student{name='%s', age=%d, gpa=%.2f, password='%s'}",
name, age, gpa, password);
}
}
// 序列化对象
public static void serializeObject(Student student, String filePath) {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(filePath))) {
oos.writeObject(student);
System.out.println("对象序列化成功: " + student);
} catch (IOException e) {
System.err.println("序列化失败: " + e.getMessage());
}
}
// 反序列化对象
public static Student deserializeObject(String filePath) {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(filePath))) {
Student student = (Student) ois.readObject();
System.out.println("对象反序列化成功: " + student);
return student;
} catch (IOException | ClassNotFoundException e) {
System.err.println("反序列化失败: " + e.getMessage());
return null;
}
}
对象序列化是持久化对象状态的重要机制,广泛用于缓存、会话管理、分布式系统中。注意serialVersionUID的管理和transient关键字的使用。
RandomAccessFile与文件指针
RandomAccessFile支持随机访问文件的任意位置,通过文件指针实现读写操作,是处理大型文件的利器。
📍 文件指针操作
public static void randomAccessDemo(String filePath) {
try (RandomAccessFile raf = new RandomAccessFile(filePath, "rw")) {
// 获取当前文件指针位置
long position = raf.getFilePointer();
System.out.println("初始位置: " + position);
// 写入一些数据
raf.writeUTF("Hello, RandomAccessFile!");
raf.writeInt(42);
raf.writeDouble(3.14159);
// 移动文件指针到文件开头
raf.seek(0);
System.out.println("回到开头: " + raf.getFilePointer());
// 读取字符串
String message = raf.readUTF();
System.out.println("读取字符串: " + message);
// 跳转到整数位置
raf.seek(raf.getFilePointer()); // 当前位置已在整数前
// 读取整数
int number = raf.readInt();
System.out.println("读取整数: " + number);
// 获取文件长度
long length = raf.length();
System.out.println("文件长度: " + length);
// 移动到文件末尾
raf.seek(length);
raf.writeUTF("追加的内容");
} catch (IOException e) {
System.err.println("随机访问文件失败: " + e.getMessage());
}
}
⚠️ 使用注意事项
- 指针位置:seek()方法的参数不能为负数
- 文件长度:seek超过文件末尾会自动扩展文件
- 线程安全:RandomAccessFile不是线程安全的
- 性能考虑:频繁的seek操作可能影响性能
RandomAccessFile是文件随机访问的理想选择,适用于数据库、日志文件、大文件处理等需要精确定位的场景。
Scanner类:文本扫描与解析
🤔 为什么有了 Reader 还需要 Scanner?
Reader 只是数据的"搬运工",它只负责把字符读进内存,不关心内容是什么。如果你想从文本中读取一个整数,你需要自己处理空格分割、字符串转换等繁琐工作。
Scanner 是数据的"翻译官",它不仅能读取数据,还能理解数据。它内置了强大的正则表达式引擎,能自动跳过分隔符(默认空格/换行),并直接将文本解析为 Java 类型(int, double 等)。
🆚 效率对比:读取两个整数 "100 200"
| 使用 BufferedReader (繁琐) | 使用 Scanner (优雅) |
|---|---|
|
|
📖 Scanner基本用法与陷阱
⚠️ 常见陷阱:构造器参数类型
初学者常犯的错误:直接传入文件路径字符串。
- ❌
new Scanner("data.txt"):扫描字符串内容 "data.txt" 本身(只读出"data.txt"这8个字符)。 - ✅
new Scanner(new File("data.txt")):扫描文件 data.txt 的内容。 - ✅
new Scanner(System.in):扫描标准输入(键盘)。
// 1. 扫描文件内容(必须包装为 File 对象)
try (Scanner fileScanner = new Scanner(new File("scores.txt"))) {
while (fileScanner.hasNextInt()) {
System.out.println("读取整数: " + fileScanner.nextInt());
}
}
// 2. 扫描字符串内容(常用于解析特定格式的文本行)
String input = "100,200,300";
Scanner strScanner = new Scanner(input);
strScanner.useDelimiter(","); // 设置分隔符
while (strScanner.hasNextInt()) {
System.out.println("解析数字: " + strScanner.nextInt());
}
🎯 实际应用:解析混合数据
public static void parseDataTypes(String filePath) {
try (Scanner scanner = new Scanner(new File(filePath))) {
// Scanner 会自动跳过空白符(空格、Tab、换行)
while (scanner.hasNext()) {
if (scanner.hasNextInt()) {
int number = scanner.nextInt();
System.out.println("整数: " + number);
} else if (scanner.hasNextDouble()) {
double decimal = scanner.nextDouble();
System.out.println("小数: " + decimal);
} else {
String text = scanner.next(); // 读取下一个单词
System.out.println("文本: " + text);
}
}
} catch (FileNotFoundException e) {
System.err.println("文件未找到: " + e.getMessage());
}
}
🎯 实际应用:CSV文件解析
public static void parseCSVFile(String filePath) {
try (Scanner scanner = new Scanner(new File(filePath))) {
// 设置分隔符为逗号
scanner.useDelimiter(",");
int lineNumber = 0;
while (scanner.hasNextLine()) {
lineNumber++;
String line = scanner.nextLine();
// 跳过空行和注释行
if (line.trim().isEmpty() || line.startsWith("#")) {
continue;
}
// 解析CSV行
Scanner lineScanner = new Scanner(line);
lineScanner.useDelimiter(",");
List fields = new ArrayList<>();
while (lineScanner.hasNext()) {
fields.add(lineScanner.next().trim());
}
System.out.println("第" + lineNumber + "行: " + fields);
lineScanner.close();
}
} catch (FileNotFoundException e) {
System.err.println("CSV文件未找到: " + e.getMessage());
}
}
🔧 Scanner配置选项
| 方法 | 用途 | 示例 |
|---|---|---|
| useDelimiter() | 设置分隔符 | scanner.useDelimiter(";") |
| useLocale() | 设置地区 | scanner.useLocale(Locale.US) |
| hasNextPattern() | 正则表达式匹配 | scanner.hasNext("\\d+") |
| findInLine() | 行内查找 | scanner.findInLine("error") |
💡 Scanner vs BufferedReader
// Scanner: 简单易用,适合格式化数据
public void scannerExample() {
try (Scanner scanner = new Scanner(new File("data.txt"))) {
while (scanner.hasNextInt()) {
int num = scanner.nextInt();
processNumber(num);
}
}
}
// BufferedReader: 性能更好,适合大文件
public void bufferedReaderExample() {
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
int num = Integer.parseInt(line.trim());
processNumber(num);
}
}
}
Scanner是文本解析的便捷工具,特别适合处理配置文件、CSV数据等格式化文本。对于大文件,BufferedReader通常是更好的选择。
* 性能优化 - NIO与高级技术
对于大文件和高性能场景,传统IO可能不够用。NIO和内存映射提供了更高效的解决方案。
🎯 NIO vs 传统IO对比
import java.nio.channels.*;
import java.nio.ByteBuffer;
public class NIOComparison {
// 传统IO方式
public static void traditionalCopy(String source, String target) {
long startTime = System.nanoTime();
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(target)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
long endTime = System.nanoTime();
System.out.printf("传统IO耗时: %.2f ms%n", (endTime - startTime) / 1_000_000.0);
}
// NIO方式
public static void nioCopy(String source, String target) {
long startTime = System.nanoTime();
try (FileChannel sourceChannel = FileChannel.open(Paths.get(source), StandardOpenOption.READ);
FileChannel targetChannel = FileChannel.open(Paths.get(target),
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.allocateDirect(8192); // 直接缓冲区
while (sourceChannel.read(buffer) != -1) {
buffer.flip(); // 切换到读模式
targetChannel.write(buffer);
buffer.clear(); // 清空缓冲区,准备写入
}
} catch (IOException e) {
e.printStackTrace();
}
long endTime = System.nanoTime();
System.out.printf("NIO耗时: %.2f ms%n", (endTime - startTime) / 1_000_000.0);
}
}
💾 内存映射文件技术
public class MemoryMappedFile {
public static void memoryMappedCopy(String source, String target) {
long startTime = System.nanoTime();
try (RandomAccessFile sourceFile = new RandomAccessFile(source, "r");
RandomAccessFile targetFile = new RandomAccessFile(target, "rw");
FileChannel sourceChannel = sourceFile.getChannel();
FileChannel targetChannel = targetFile.getChannel()) {
long fileSize = sourceChannel.size();
// 内存映射源文件
MappedByteBuffer sourceBuffer = sourceChannel.map(
FileChannel.MapMode.READ_ONLY, 0, fileSize);
// 内存映射目标文件
MappedByteBuffer targetBuffer = targetChannel.map(
FileChannel.MapMode.READ_WRITE, 0, fileSize);
// 直接内存复制
targetBuffer.put(sourceBuffer);
} catch (IOException e) {
e.printStackTrace();
}
long endTime = System.nanoTime();
System.out.printf("内存映射耗时: %.2f ms%n", (endTime - startTime) / 1_000_000.0);
}
// 大文件处理
public static void processLargeFile(String filePath, long chunkSize) {
try (RandomAccessFile file = new RandomAccessFile(filePath, "r");
FileChannel channel = file.getChannel()) {
long fileSize = channel.size();
long position = 0;
while (position < fileSize) {
long remaining = fileSize - position;
long currentChunk = Math.min(chunkSize, remaining);
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_ONLY, position, currentChunk);
// 处理当前块
processBuffer(buffer);
position += currentChunk;
// 释放内存映射(帮助垃圾回收)
Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
if (cleaner != null) {
cleaner.clean();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
🔄 NIO的关键优势
⚡ 直接缓冲区
使用直接内存,减少数据拷贝
- ByteBuffer.allocateDirect()
- 避免JVM堆内存拷贝
- 提高大文件处理效率
🔄 非阻塞IO
支持异步IO操作
- Selector多路复用
- Channel异步操作
- 高并发网络编程
📊 适用场景对比
| 场景 | 传统IO | NIO | 推荐 |
|---|---|---|---|
| 小文件操作 | ✅ 简单易用 | ❌ 过度复杂 | 传统IO |
| 大文件读写 | ⚠️ 性能一般 | ✅ 高效处理 | NIO |
| 随机访问 | ❌ 需要seek | ✅ 内存映射 | 内存映射 |
| 高并发 | ❌ 线程阻塞 | ✅ 非阻塞 | NIO.2 |
NIO和内存映射为高性能IO提供了强大的工具,特别适合大文件、随机访问和高并发场景。
* 性能优化 - 最佳实践清单
掌握IO性能优化的核心要点,建立完整的性能优化检查清单,让程序性能更上一层楼。
📊 性能优化检查清单
| 优化项 | 建议 | 预期收益 | 优先级 |
|---|---|---|---|
| 使用缓冲流 | 文件操作默认使用 | 10-100x性能提升 | ⭐⭐⭐⭐⭐ |
| 合适的缓冲区大小 | 8KB-64KB | 20-50%性能提升 | ⭐⭐⭐⭐ |
| 批量操作 | 避免单字节操作 | 显著性能提升 | ⭐⭐⭐⭐ |
| NIO用于大文件 | >100MB文件 | 20-30%性能提升 | ⭐⭐⭐ |
| 内存映射 | 超大文件随机访问 | 显著性能提升 | ⭐⭐ |
| 资源管理 | try-with-resources | 避免资源泄漏 | ⭐⭐⭐⭐⭐ |
🔧 性能监控工具
public class IOPerformanceMonitor {
// 简单的性能计时器
public static long timeExecution(Runnable task) {
long startTime = System.nanoTime();
task.run();
return System.nanoTime() - startTime;
}
// 文件复制性能测试
public static void benchmarkFileCopy(String source, String target) {
long traditionalTime = timeExecution(() -> traditionalCopy(source, target));
long bufferedTime = timeExecution(() -> bufferedCopy(source, target));
long nioTime = timeExecution(() -> nioCopy(source, target));
System.out.println("=== 文件复制性能对比 ===");
System.out.printf("传统IO: %.2f ms%n", traditionalTime / 1_000_000.0);
System.out.printf("缓冲IO: %.2f ms (%.1fx faster)%n",
bufferedTime / 1_000_000.0, (double) traditionalTime / bufferedTime);
System.out.printf("NIO: %.2f ms (%.1fx faster)%n",
nioTime / 1_000_000.0, (double) traditionalTime / nioTime);
}
// 吞吐量测试
public static void throughputTest(String filePath) {
try (FileInputStream fis = new FileInputStream(filePath)) {
byte[] buffer = new byte[8192];
long totalBytes = 0;
long startTime = System.currentTimeMillis();
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
totalBytes += bytesRead;
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
double throughput = (totalBytes / 1024.0 / 1024.0) / (duration / 1000.0);
System.out.printf("吞吐量: %.2f MB/s%n", throughput);
} catch (IOException e) {
e.printStackTrace();
}
}
}
🚨 常见性能陷阱
陷阱1:过度优化
问题:过早优化,增加代码复杂度
解决:先保证正确性,再优化性能
陷阱2:忽视异常处理
问题:异常处理逻辑影响性能
解决:合理设计异常处理,避免性能热点
陷阱3:内存泄漏
问题:未正确关闭IO资源
解决:使用try-with-resources确保资源释放
IO性能优化需要综合考虑场景、数据量、硬件等因素。选择合适的工具和策略,可以让你的程序获得显著的性能提升。
记住:先正确,再优化!
Java IO核心知识总结
通过本课程学习,我们掌握了Java IO体系的核心概念。让我们系统回顾关键知识点。
📚 核心知识体系
🏗️ 基础概念
- 字节流 vs 字符流
- 输入流 vs 输出流
- 节点流 vs 处理流
- 装饰器模式应用
- 字符编码处理
🛠️ 核心工具
- File类操作
- 缓冲流优化
- 转换流编码
- 对象序列化
- 随机访问文件
⚡ 性能优化
- 缓冲区大小选择
- NIO vs 传统IO
- 内存映射文件
- 批量操作策略
- 资源管理优化
🛡️ 最佳实践
- try-with-resources
- 异常处理策略
- 编码一致性
- 线程安全考虑
- 跨平台兼容
🎯 流类型选择指南
Binary 使用字节流的场景
- 二进制文件(图片、音频、视频)
- 可执行文件和压缩包
- 网络数据传输
- 不需要编码转换的数据
Text 使用字符流的场景
- 文本文件处理
- 需要字符编码的场景
- 按行读取文本内容
- 国际化文本处理
上手练习
通过实际练习来巩固Java IO知识。从基础练习开始,逐步提升难度,帮助你掌握各种IO操作。
📝 基础练习
练习1:文件复制工具
实现一个支持进度显示的文件复制工具:
- 显示复制进度百分比
- 支持大文件复制(>1GB)
- 提供复制速度统计
- 支持暂停/恢复功能
练习2:文本分析器
创建一个文本文件分析工具:
- 统计字符、单词、行数
- 计算词频并输出TOP10
- 检测文件编码
- 支持多种文件格式
🎯 进阶练习
练习3:简单数据库
使用RandomAccessFile实现一个简单的键值数据库:
- 支持字符串键值对存储
- 实现增删改查操作
- 支持索引查询
- 提供数据备份功能
练习4:文件分割合并工具
开发一个文件分割和合并工具:
- 按大小或数量分割文件
- 支持任意类型文件
- 提供完整性校验
- 支持批量操作
* 挑战练习
挑战更高难度的练习,并获取学习建议和指导,进一步提升Java IO编程能力。
🚀 挑战练习
练习5:多线程文件下载器
实现一个支持断点续传的多线程下载器:
- 支持HTTP/HTTPS协议
- 多线程分段下载
- 断点续传功能
- 下载速度限制
练习6:实时日志监控系统
开发一个实时日志监控和分析系统:
- 监控多个日志文件
- 实时解析日志内容
- 关键词告警功能
- Web界面展示
💻 实现参考与代码框架
// 文件复制工具框架
public class FileCopier {
private final int bufferSize;
private final boolean showProgress;
public FileCopier(int bufferSize, boolean showProgress) {
this.bufferSize = bufferSize;
this.showProgress = showProgress;
}
public void copy(String source, String target) throws IOException {
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(target);
BufferedInputStream bis = new BufferedInputStream(fis, bufferSize);
BufferedOutputStream bos = new BufferedOutputStream(fos, bufferSize)) {
long fileSize = new File(source).length();
long totalBytes = 0;
byte[] buffer = new byte[8192];
int bytesRead;
long startTime = System.currentTimeMillis();
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
totalBytes += bytesRead;
if (showProgress) {
showProgress(totalBytes, fileSize, startTime);
}
}
}
}
private void showProgress(long current, long total, long startTime) {
double percent = (double) current / total * 100;
long elapsed = System.currentTimeMillis() - startTime;
double speed = current / (elapsed / 1000.0) / (1024 * 1024); // MB/s
System.out.printf("\r进度: %.1f%% 速度: %.2f MB/s", percent, speed);
}
}
// 文本分析器框架
public class TextAnalyzer {
private final Map wordFrequency = new HashMap<>();
private int charCount = 0;
private int wordCount = 0;
private int lineCount = 0;
public void analyze(String filePath) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = reader.readLine()) != null) {
lineCount++;
charCount += line.length();
// 简单的单词分割
String[] words = line.split("\\s+");
for (String word : words) {
if (!word.isEmpty()) {
wordCount++;
wordFrequency.merge(word.toLowerCase(), 1, Integer::sum);
}
}
}
}
}
public void printStatistics() {
System.out.println("字符数: " + charCount);
System.out.println("单词数: " + wordCount);
System.out.println("行数: " + lineCount);
System.out.println("\n词频TOP10:");
wordFrequency.entrySet().stream()
.sorted(Map.Entry.comparingByValue().reversed())
.limit(10)
.forEach(entry -> System.out.printf("%s: %d%n",
entry.getKey(), entry.getValue()));
}
}
通过这些练习,你将深入理解Java IO的各种特性,掌握实际开发中的最佳实践。记住,编程能力的提升来自于持续的实践和思考!