Java I/O系统:从“流”到“新世界”,一篇让你不再“流”泪的指南
大家好,我是会编程的吕洞宾。今天咱们不炼丹,改聊聊Java里那些让人又爱又恨的I/O(输入/输出)操作。别怕,我保证用最“接地气”的方式,带你捋清楚从老派的I/O流到炫酷的新I/O系统。咱们的目标是:看完这篇,你写I/O代码时能像吕洞宾御剑飞行一样潇洒!
一、 I/O流:Java世界的“水管工”
想象一下,数据就像水。I/O流就是连接水源(数据源)和水池(你的程序)的一根根水管。Java提供了琳琅满目的“水管”,咱们分门别类瞧瞧。
1. 各种InputStream和OutputStream类型
这是最基础的“水管”,负责处理字节(原始数据)。
- 特征:它们是抽象类,所有字节输入/输出流的爸爸。
- 常见儿子们:
FileInputStream/FileOutputStream: 从文件读字节,往文件写字节。最常用,就像你家入户的总水管。ByteArrayInputStream/ByteArrayOutputStream: 把内存中的字节数组当成流来处理。适合在内存里折腾数据,不用真写文件。BufferedInputStream/BufferedOutputStream: 给其他流套个“缓冲层”。没有它,读一个字节就从硬盘拿一次,慢得像滴水;有了它,一次读一大块放内存里,用的时候从内存取,快如自来水!强烈建议给文件流套上它。DataInputStream/DataOutputStream: 能直接读写Java基本数据类型(int, double等)和String。它们给字节流穿上了“理解Java语言”的外衣。ObjectInputStream/ObjectOutputStream: 更狠,能直接读写整个Java对象(需要对象实现Serializable接口)。相当于能把一个“包裹”(对象)整个塞进水管或取出来。
- 优缺点:
- 优点:概念简单,适合处理所有二进制数据(如图片、视频)。
- 缺点:直接操作字节,处理文本时很麻烦(需要考虑编码,一个中文字符可能占多个字节)。
举例与反例:
// 正例:用缓冲流高效拷贝文件
try (FileInputStream fis = new FileInputStream("source.jpg");
BufferedInputStream bis = new BufferedInputStream(fis);
FileOutputStream fos = new FileOutputStream("copy.jpg");
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
}
// 反例:不用缓冲流,一个字节一个字节读(慢到怀疑人生)
try (FileInputStream fis = new FileInputStream("bigFile.txt");
FileOutputStream fos = new FileOutputStream("output.txt")) {
int b;
while ((b = fis.read()) != -1) { // 每次只读一个字节!
fos.write(b);
}
}
// **原因**:每次`read()`和`write()`都是系统调用,涉及用户态和内核态切换,极其耗时。对于大文件,时间可能是缓冲流的几十上百倍。
2. 添加属性和有用的接口
有时候光有水不够,还得知道水的温度、压力。这就是“装饰器模式”在I/O流里的应用。
- 核心接口/类:
FilterInputStream/FilterOutputStream: 是装饰器模式的“插座”。上面提到的BufferedInputStream、DataInputStream都是它的子类。PushbackInputStream: 可以把读出来的一个字节“推回去”,下次再读。就像你看书,看了一眼觉得没懂,可以把视线挪回去重新看。
- 特征:通过层层包装,给基础流添加新功能。组合灵活,功能强大。
- 优缺点:
- 优点:符合开闭原则,可以动态地给流添加功能。
- 缺点:创建流对象时可能需要多层嵌套,代码看起来有点绕。
举例:
// 组合使用:一个能缓冲、能回推、还能读基本类型的输入流
try (DataInputStream dis = new DataInputStream(
new PushbackInputStream(
new BufferedInputStream(
new FileInputStream("data.bin"), 1024), 8))) {
// 现在dis可以高效地(缓冲)、灵活地(回推)、方便地(读基本类型)读取文件了
int anInt = dis.readInt();
}
3. 各种Reader和Writer
字节流处理文本太费劲?Reader和Writer来救场!它们是处理字符(文本)的“水管”,核心是解决了字符编码问题。
- 特征:它们是抽象类,内部会自动处理字符编码(如UTF-8, GBK)。
- 常见类型:
InputStreamReader/OutputStreamWriter: 桥梁!能把字节流转换成字符流。构造时需要指定字符集(Charset),比如StandardCharsets.UTF_8。FileReader/FileWriter: 读写文本文件的便捷类。注意:它们使用平台默认编码,容易导致跨平台乱码。更推荐用InputStreamReader和OutputStreamWriter指定明确编码。BufferedReader/BufferedWriter: 和字节缓冲流一样,给字符流加缓冲,并且BufferedReader有个超好用的readLine()方法,能一次读一行。PrintWriter: 我们最爱的System.out就是它!提供了print(),println(),printf()等格式输出方法,比原始的Writer方便太多。
- 优缺点:
- 优点:专为文本设计,无需手动处理编码转换,提供了方便的文本操作方法。
- 缺点:只能处理文本,不能处理二进制数据。
举例与反例:
// 正例:用指定编码安全地读取文本文件
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("text.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// 反例:使用FileReader,依赖默认编码(可能导致乱码)
try (FileReader fr = new FileReader("text.txt")) {
// 如果文件是UTF-8编码,但系统默认是GBK,这里读出来就是乱码
}
// **原因**:`FileReader`无法指定编码,其行为取决于`file.encoding`系统属性,不可控。
4. 自成一家的RandomAccessFile
这位是I/O界的“异类”。它不像流那样只能顺序读/写,它可以像数组一样,随机访问文件的任何位置。
- 特征:
- 同时实现了
DataInput和DataOutput接口,既能读也能写。 - 通过
seek(long pos)方法,可以把文件指针跳到任意位置。 - 构造时需要指定模式(
"r"只读,"rw"读写等)。
- 同时实现了
- 优缺点:
- 优点:随机访问,非常适合修改文件中间某部分内容,或者实现数据库索引等结构。
- 缺点:API相对底层和古老,不如NIO的
FileChannel+MappedByteBuffer组合强大和高效。
举例:
// 在文件末尾追加内容
try (RandomAccessFile raf = new RandomAccessFile("log.txt", "rw")) {
raf.seek(raf.length()); // 跳到文件末尾
raf.writeBytes("\nNew log entry at " + new Date());
}
5. I/O流的典型用法
总结一下,经典I/O流的使用有个黄金模式:
- 嵌套装饰:从最底层的节点流(如
FileInputStream)开始,根据需求用装饰流(如BufferedInputStream,DataInputStream)一层层包装。 - 使用Try-With-Resources:所有流都实现了
AutoCloseable接口,用try-with-resources语法确保流一定会被关闭,防止资源泄漏。 - 缓冲区是王道:处理文件或网络数据时,务必使用缓冲流。
// 典型模板:读取一个UTF-8编码的文本文件,并打印行号
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("input.txt"), StandardCharsets.UTF_8))) {
String line;
int lineNum = 1;
while ((line = br.readLine()) != null) {
System.out.println(lineNum++ + ": " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
二、 标准I/O:程序与世界的“标准对话窗口”
每个Java程序一出生,就拥有了三个“标准流”,它们连接着程序和外部的世界(通常是控制台)。
1. 从标准输入中读取
System.in是一个静态的InputStream对象。默认关联键盘输入。
// 从控制台读取一行输入(古老方式,麻烦)
BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
System.out.print("Enter your name: ");
String name = stdin.readLine();
2. 将System.out转换为PrintWriter
System.out是一个PrintStream。但有时候我们需要它作为Writer来用(比如某些API要求Writer参数)。
// 将System.out包装成PrintWriter
PrintWriter out = new PrintWriter(System.out, true); // 第二个参数autoFlush设为true
out.println("Hello, this is from a PrintWriter!");
// 现在out拥有了和System.out一样的print/println方法,但本质是Writer
3. 标准I/O重定向
这是个很酷的功能!你可以让程序的输入不来自键盘,输出不显示在屏幕。
// 将标准输出重定向到一个文件
PrintStream originalOut = System.out;
try (PrintStream fileOut = new PrintStream(new FileOutputStream("output.log"))) {
System.setOut(fileOut); // 从此以后,System.out.println的内容都进文件了
System.out.println("This line goes to the file, not console!");
} finally {
System.setOut(originalOut); // 恢复原状
}
System.out.println("Now back to console.");
应用场景:日志记录、捕获程序输出进行自动化测试等。
4. 进程控制
通过Runtime.exec()或ProcessBuilder启动外部进程(另一个程序),然后可以通过获取该进程的输入、输出、错误流来和它交互。
// 执行系统命令并读取结果
Process process = new ProcessBuilder("ls", "-la").start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
reader.lines().forEach(System.out::println);
}
int exitCode = process.waitFor(); // 等待进程结束
System.out.println("Exited with code: " + exitCode);
这让你能在Java程序里调用命令行工具,非常强大。
三、 新I/O系统(NIO):为速度而生的“高速公路”
老I/O流是“流”,像水管,数据一个方向顺序流动。NIO(New I/O)的核心是通道(Channel)和缓冲区(Buffer),更像铁路和货车:数据(货物)被装进Buffer(货车),通过Channel(铁轨)运输,可以双向,可以非阻塞(异步),效率高得多。
1. 字节缓冲区 ByteBuffer
ByteBuffer是NIO的基石,一个可以装原生数据的容器。
- 特征:
- 状态变量:
position(当前位置),limit(限制位置),capacity(容量)。理解这三个变量是玩转Buffer的关键! - 操作:
put()写数据,get()读数据,flip()切换为读模式,clear()/compact()清空或压缩后切换为写模式。
- 状态变量:
- 典型流程:
- 向Buffer写数据 (
put) ->position移动。 - 写完,调用
flip()->limit移到当前position,position归零,切换为读模式。 - 从Buffer读数据 (
get) ->position移动。 - 读完,调用
clear()(丢弃所有数据)或compact()(保留未读数据)-> 切换回写模式,准备接收新数据。
- 向Buffer写数据 (
ByteBuffer buf = ByteBuffer.allocate(48); // 分配一个48字节的Heap Buffer
// 写模式
buf.put((byte) 1);
buf.putInt(42);
buf.putChar('A');
// 切换为读模式
buf.flip();
// 读模式(必须按写入顺序和类型读!)
byte aByte = buf.get();
int anInt = buf.getInt();
char aChar = buf.getChar();
2. 转换数据
Charset类提供了字符和字节缓冲区转换的桥梁。
// 字符串 -> ByteBuffer
ByteBuffer buffer = StandardCharsets.UTF_8.encode("Hello NIO");
// ByteBuffer -> 字符串
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer);
String str = charBuffer.toString();
3. 获取基本类型 & 4. 视图缓冲区
ByteBuffer提供了一系列asXxxBuffer()方法来创建视图缓冲区。它们和原始ByteBuffer共享底层数据,但以不同的数据类型视角看待它。
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 写入一些数据
byteBuffer.putInt(100);
byteBuffer.putDouble(3.14);
byteBuffer.flip();
// 创建IntBuffer视图,从当前位置开始
IntBuffer intBuffer = byteBuffer.asIntBuffer();
int firstInt = intBuffer.get(); // 读取100
// 注意:get()之后,原ByteBuffer的position也前进了4个字节(一个int的大小)
// 现在原ByteBuffer的position指向double数据的开始
DoubleBuffer doubleBuffer = byteBuffer.asDoubleBuffer();
double aDouble = doubleBuffer.get(); // 读取3.14
优点:操作方便,无需手动计算字节偏移量。注意:视图缓冲区会修改原缓冲区的position。
5. 用缓冲区操纵数据
除了基本的put/get,Buffer还支持批量操作、相对/绝对位置操作、标记/重置等。
ByteBuffer buf = ByteBuffer.allocate(10);
// 相对操作 (position会自动移动)
buf.put(new byte[]{1,2,3});
// 绝对操作 (指定索引,不改变position)
buf.put(5, (byte) 99);
buf.flip();
byte[] dst = new byte[3];
buf.get(dst, 0, 3); // 批量读
buf.mark(); // 标记当前位置
buf.get(); // position移动
buf.reset(); // 重置到标记位置
6. 内存映射文件
这是NIO的大杀器!它允许你将一个文件或文件的一部分直接映射到内存中。这样操作文件就像操作内存数组一样快,因为操作系统会负责数据的缓存和同步。
try (RandomAccessFile file = new RandomAccessFile("huge.data", "rw");
FileChannel channel = file.getChannel()) {
// 将文件的前100字节映射到内存
MappedByteBuffer mappedBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 100);
// 直接像操作数组一样操作文件!
for (int i = 0; i < mappedBuffer.limit(); i++) {
byte b = mappedBuffer.get(i);
mappedBuffer.put(i, (byte)(b + 1)); // 每个字节加1
}
// 无需显式关闭,修改会自动写回文件(由OS决定时机,也可force()强制写回)
}
适用场景:处理超大文件(如视频编辑、数据库文件),效率远超普通流。
7. 文件加锁
NIO提供了更细粒度的文件锁机制。
try (FileChannel channel = FileChannel.open(Paths.get("test.txt"), StandardOpenOption.WRITE)) {
// 获取排他锁(写锁),如果文件已被其他进程锁定,这里会阻塞
FileLock lock = channel.lock(); // 锁整个文件
// FileLock lock = channel.lock(0, 10, false); // 锁文件的前10个字节(共享锁)
try {
// 安全地进行写操作...
channel.write(ByteBuffer.wrap("Locked data".getBytes()));
} finally {
lock.release(); // 释放锁
}
}
注意:Java的文件锁是**劝告式(advisory)**的,不是强制式(mandatory)。意思是它只阻止其他遵守此锁规则的Java程序(或明确检查此锁的程序)访问,对于不检查锁的程序(比如用普通文本编辑器打开)无效。
好了,各位道友,Java I/O的漫漫长路,咱们今天就逛到这。从朴实无华的字节流,到专精文本的字符流,再到高效灵活的NIO缓冲区与通道,每一样工具都有其用武之地。记住吕洞宾的忠告:
- 处理文本,优先考虑Reader/Writer并明确指定编码。
- 读写文件,务必给流穿上“缓冲”的外衣。
- 追求性能,尤其是大文件或随机访问,勇敢拥抱NIO和内存映射。
理解了这些,你在Java世界里处理数据输入输出,便能如吕洞宾的飞剑一般,指哪打哪,游刃有余。我们下回再见!
更多推荐


所有评论(0)