Java基础IO
modified-time: 2026-01-06 21:07:23
字节流和字符流
- 字符流文件: .txt、.json、.csv、.xml 等
- 二进制文件(字节流):图片、视频、.pdf、.docx、.jar 等
本质上都是字节,只是字符流通过字符集编码,二进制文件不能直接当成文本去解码
读写字符流
-
处理流
BufferedReader和BufferedWriter处理读取到的文件。
-
转换流
- 使用
InputStreamReader和OutputStreamWriter将读取到的字节流转换为正确编码的 字符流 需要指定明确的编码 可以直接字符串也可以使用StandardCharsets.xxx
new InputStreamReader(new FileInputStream(inputFile), StandardCharsets.UTF_8 ); new InputStreamReader(new FileInputStream(inputFile), "UTF-8"); - 使用
为什么不使用 FileReader 和 FileWriter? new FileReader(filePath) // 内部实际是 new InputStreamReader(new FileInputStream(…), 平台默认编码) 他们内部使用的是当前平台默认编码,无法显式指定读取写入的编码方式。 结论:永远不要用 FileReader / FileWriter,改用 FileInputStream + InputStreamReader(指定编码)
- 节点流
FileInputStream和FileOutputSteam直接读取
// 示例
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(inputFile), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(outputFile),StandardCharsets.UTF_8))) {
// do sth...
}catch (IOException e) {
e.printStackTrace();
}
读写字节流(二进制)
- 处理流
BufferedInputStream和BufferedOutputStream- 数据类型读写:
DataInputStream、DataOutputStream(读写 int、double 等基本类型) - 对象序列化:
ObjectInputStream、ObjectOutputStream
- 转换流 (因为是二进制文件所以可以不使用转换流)
- 字节流
FileInputStream和FileOutputStream
// 示例
try (BufferedInputStream reader = new BufferedInputStream(new FileInputStream(inputPath));
BufferedOutputStream writer = new BufferedOutputStream(new FileOutputStream(outputPath))) {
// do sth...
}catch (IOException e) {
e.printStackTrace();
}
缓存使用
使用缓存可以手动控制单次读入的数据量可以避免:
- 如果单行长文本使用
readLine()可能 GC OOM - 如果是多行但换行频率高会不断循环增加网络 RTT 硬盘 IO 等
- 如果是字节流使用
readAllBytes()读取大文件也会导致内存被大量占用降低效率甚至 OOM. 使用步骤:
-
字节流:
byte[] buffer = new byte[81920]; while ((len = reader.read(buffer)) != -1) { writer.write(buffer, 0, len); } -
字符流
char[] buffer = new char[8192]; while ((len = reader.read(buffer)) != -1) { //如果是 readLine 的话用 != null writer.write(buffer, 0, len); }
- 缓冲区长度陷阱(核心):
- 注意你的 writer.write(buffer, 0, len)。
- 使用
BufferedInputStream/BufferedReader时,它们 内部本来就有一层缓冲,默认大小 8KB; - 思考: buffer 的长度是 81920。如果文件的最后一次读取只剩 100 个字节,那么 len 就会等于 100。
- 如果写成 writer.write(buffer),程序会将整个 81920 字节全部写入文件,导致 文件末尾多出大量的空白或旧数据残余。这就是为什么要用 len 的原因。
| 流家族 | 抽象基类 | 常见节点流示例 | 常见处理流示例 |
|---|---|---|---|
| 字节流 | InputStream / OutputStream | FileInputStream / FileOutputStream | BufferedInputStream / BufferedOutputStream, DataInputStream / DataOutputStream, ObjectInputStream / ObjectOutputStream |
| 字符流 | Reader / Writer | FileReader / FileWriter | BufferedReader / BufferedWriter, InputStreamReader / OutputStreamWriter |
RandomAccessFile
用于解决无法随机访问读写的问题而诞生,但因为其比较底层,现在被 FileChannel + MappedByteBuffer 代替
- RAF 的本质: 它是一个带“指针(File Pointer)”的状态机。
- 并发冲突点: 指针是共享资源。多线程必须通过
new多个 RAF 实例来实现“逻辑上的无状态”,或者在单实例下使用synchronized(但会退化为单线程)。
如何使用
// 创建
RandomAccessFile raf = new RandomAccessFile(inputPath, mode);
//基础读写
raf.read(buffer);
raf.write(buffer, start, end);
//获取文件长度与申请空间
raf.length();
raf.setLength();
// 预分配是防止文件碎片化和频繁扩容的关键
//刷盘
raf.getFD.sync();
❓ 提问:
-
为什么这里不需要向上面一样包装
BufferedInput/OutputStream?BufferedOutputStream的存在是为了给那些“一次只写一个字节/少量字节”,进行缓冲减少频繁直接调用 native 方法写入。它在内存里开辟一个 8KB 的数组,凑够了再调一次系统内核(System Call)。 -
为什么要避免多次直接调用系统内核去写入?
调用系统内核需要从用户态转变为内核态,该过程耗费大量时间会消耗 CPU 周期,要减少不必要的损耗。 这也就是为什么在非强一致性操作中常使用
raf.getFD.sync()而不使用RandomAccessFile(inputPath, "rwd/rws"); -
为什么要刷盘?
write() 成功不代表数据落盘,只代表数据进入了内核缓存。
-
如果我在 FileOutputStream 套了 BufferedputStream 我每次读取少量数据后就 write 是否会让其失效?
只要 buffer 没满、没手动 flush、没 close,数据就停留在用户态的 byte [] 数组里,不触发系统调用。
| 触发时机 | 是否切换内核态 |
|---|---|
| write(b) 但 buffer 未满 | ❌ 否,纯内存操作 |
| write(b) 导致 buffer 满了 | ✅ 是,自动调用底层 write |
| 手动调用 flush() | ✅ 是 |
| 调用 close() | ✅ 是(内部先 flush) |
实战示例(多线程复制文件)
要求:
- 使用 RAF 读取原数据获取长度,预分配空间,为每个子线程分配任务
- 使用线程池和
CompletableFuture完成子线程调用与任务执行 - 使子线程只工作在自己被分配到的区间内
try (RandomAccessFile raf = new RandomAccessFile(input, "rw")) {
//获取长度,为预分配空间和切片做准备
long length = raf.length();
//根据线程数量切片
long splitSize = length / 16;
//分配空间后写入刷盘
raf.setLength(length);
raf.getFD().sync();
ThreadPoolExecutor executor = threadpool();
try {
List<CompletableFuture<String>> cf = new ArrayList<>();
//步长为每个子线程获得的工作量
for (long i = 0; i < length; i += splitSize) {
if (i > length) {
break;
}
long start = i;
//如果剩余数据不足分片大小,则只设置剩余大小
long end = i + splitSize > length ? length : i + splitSize;
cf.add(CompletableFuture.supplyAsync(() -> singleCopy(new Task(start, end, input, output)),
executor));
}
CompletableFuture.allOf(cf.toArray(CompletableFuture[]::new)).join();
} finally {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}
class Task {
public long startOffset;
public long endOffset;
public String inputPath;
public String outputPath;
public Task(long startOffset, long endOffset, String inputPath, String outputPath) {
this.startOffset = startOffset;
this.endOffset = endOffset;
this.inputPath = inputPath;
this.outputPath = outputPath;
}
}
public static String singleCopy(Task task) {
try (RandomAccessFile read = new RandomAccessFile(task.inputPath, "r");
RandomAccessFile target = new RandomAccessFile(task.outputPath, "rw")) {
//设置读取和写入的偏移量,也就是开始坐标
read.seek(task.startOffset);
target.seek(task.startOffset);
int len;
byte[] buffer = new byte[81920];
//使用局部变量记录当前读取到的位置
long currentPos = task.startOffset;
while (currentPos < task.endOffset) {
//如果剩余量小于 buffer 里读取到的长度,则只读取 剩余工作量
//避免污染其他线程的工作区间
long remaining = task.endOffset - currentPos;
int maxRead = (int) Math.min(buffer.length, remaining);
len = read.read(buffer, 0, maxRead);
if (len == -1) {
break;
}
target.write(buffer, 0, len);
currentPos += len;
}
target.getFD().sync();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return "success!";
}
⚠️ 注意 为什么这里不使用
getFilePointer()去直接获取当前偏移量位置,而是使用局部变量? 答案依旧是getFilePointer()是用的 native 方法,会从用户态切换到内核态