Java I/O 流初识
第一章:I/O 流核心概念与体系结构
1.1 什么是流 (Stream)
在Java中,流 是一个抽象的概念,代表了数据的有序序列。可以想象成连接程序与数据源(如文件、网络连接、内存块等)的“管道”,数据通过这个管道“流动”。
- 输入流:数据从外部(如文件)流入 程序。
- 输出流:数据从程序流出 到外部(如文件)。
1.2 流的顶级分类:字节流 vs. 字符流
这是Java I/O最根本的分类,决定了我们处理数据的“基本单位”。
| 特性 | 字节流 | 字符流 |
|---|---|---|
| 数据单位 | 字节 (byte, 8bit) | 字符 (char, 16bit) |
| 用途 | 处理所有类型的数据,如图片、视频、音频等二进制文件 | 专门处理文本数据 |
| 顶层抽象类 | InputStream, OutputStream |
Reader, Writer |
| 优势 | 通用性强,能处理任何数据 | 自动处理字符编码,方便处理文本 |
重要原则:如果是文本文件(如.txt, .java, .html),优先使用字符流,可以避免乱码问题。如果是二进制文件(如.jpg, .mp3, .class),必须使用字节流。
1.3 I/O 流的类体系结构
Java I/O 使用了“装饰者模式”,通过组合不同的流类来增强功能。其类结构主要围绕四个抽象类展开:
InputStream(字节输入流)OutputStream(字节输出流)Reader(字符输入流)Writer(字符输出流)
所有具体的流类都是这四个类的子类。
第二章:字节流 (Byte Streams)
字节流用于处理二进制数据,核心类是 InputStream 和 OutputStream。
2.1 文件字节流 (FileInputStream & FileOutputStream)
这是最基础的流,用于直接与文件进行字节交互。
示例:文件复制(一次读取一个字节)
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopyByte {
public static void main(String[] args) {
// 声明在try()括号中,JDK7+会自动关闭资源 (自动调用close方法)
try (FileInputStream fis = new FileInputStream("source.jpg");
FileOutputStream fos = new FileOutputStream("target.jpg")) {
int byteData;
// read() 一次读取一个字节,返回int值,读到末尾返回-1
while ((byteData = fis.read()) != -1) {
// write() 一次写入一个字节
fos.write(byteData);
}
System.out.println("文件复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
// 无需手动调用 fis.close() 和 fos.close(), try-with-resources 会自动处理
}
}
示例:使用缓冲区提高效率(一次读取多个字节)
try (FileInputStream fis = new FileInputStream("source.jpg");
FileOutputStream fos = new FileOutputStream("target.jpg")) {
byte[] buffer = new byte[1024]; // 创建一个1KB的缓冲区
int length;
// read(buffer) 将数据读入字节数组,返回读取的字节数
while ((length = fis.read(buffer)) != -1) {
// write(buffer, 0, length) 将缓冲区中从0到length的数据写入
fos.write(buffer, 0, length);
}
System.out.println("文件复制完成(使用缓冲区)!");
} catch (IOException e) {
e.printStackTrace();
}
使用缓冲区的意义:单字节读写涉及大量底层的系统调用,效率极低。使用缓冲区后,减少了I/O操作次数,性能提升巨大。
2.2 缓冲字节流 (BufferedInputStream & BufferedOutputStream)
这是处理流/包装流的一种,它们本身不直接连接数据源,而是“包装”在另一个节点流(如FileInputStream)之上,提供缓冲功能,是更高效的用法。
示例:使用缓冲流进行文件复制
import java.io.*;
public class FileCopyBuffered {
public static void main(String[] args) {
// 在文件流的外层再包裹一层缓冲流
try (FileInputStream fis = new FileInputStream("source.jpg");
BufferedInputStream bis = new BufferedInputStream(fis); // 包装
FileOutputStream fos = new FileOutputStream("target.jpg");
BufferedOutputStream bos = new BufferedOutputStream(fos)) { // 包装
int byteData;
// 此时 bis.read() 会先从内部缓冲区读取,缓冲区空了才去底层文件流取一大块数据
while ((byteData = bis.read()) != -1) {
bos.write(byteData); // bos.write() 会先写入内部缓冲区,满了再一次性写入底层文件流
}
// bos.flush(); // 在close()时会自动调用flush(),强制清空缓冲区并写入磁盘
System.out.println("文件复制完成(使用缓冲流)!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
第三章:字符流 (Character Streams)
字符流用于处理文本数据,核心类是 Reader 和 Writer。它们内部会自动进行字节与字符的编码解码转换。
3.1 文件字符流 (FileReader & FileWriter)
用于直接读写文本文件。
示例:文本文件复制
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class TextFileCopy {
public static void main(String[] args) {
// 注意:FileReader/FileWriter 使用平台默认编码(如GBK, UTF-8),可能产生乱码
try (FileReader reader = new FileReader("source.txt");
FileWriter writer = new FileWriter("target.txt")) {
int charData;
// read() 一次读取一个字符(两个字节)
while ((charData = reader.read()) != -1) {
writer.write(charData);
}
System.out.println("文本文件复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.2 缓冲字符流 (BufferedReader & BufferedWriter)
这是处理字符流时最常用、最高效的类。特别是 BufferedReader 提供了 readLine() 方法,可以方便地按行读取文本。
示例:按行读写文本文件
import java.io.*;
public class ReadWriteByLine {
public static void main(String[] args) {
try (FileReader fr = new FileReader("source.txt");
BufferedReader br = new BufferedReader(fr); // 包装
FileWriter fw = new FileWriter("target.txt");
BufferedWriter bw = new BufferedWriter(fw)) { // 包装
String line;
// readLine() 读取一行文本(不包含换行符),返回null表示结束
while ((line = br.readLine()) != null) {
bw.write(line); // 写入一行
bw.newLine(); // 写入一个与系统相关的换行符
}
System.out.println("按行复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.3 字节流与字符流的桥梁:InputStreamReader & OutputStreamWriter
当需要指定字符编码时,就必须使用这两个转换流。它们是连接字节世界和字符世界的桥梁。
InputStreamReader: 将一个字节输入流InputStream转换为字符输入流Reader。OutputStreamWriter: 将一个字符输出流Writer转换为字节输出流OutputStream。
示例:指定UTF-8编码读取文本文件
import java.io.*;
public class SpecifyEncoding {
public static void main(String[] args) {
// 用字节流打开文件,再用InputStreamReader指定编码转换为字符流
try (FileInputStream fis = new FileInputStream("source_utf8.txt");
// 关键:将字节流 fis 转换为字符流 isr,并指定编码为 "UTF-8"
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
BufferedReader br = new BufferedReader(isr)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
第四章:高级话题与标准I/O
4.1 打印流 (PrintStream & PrintWriter)
这是最方便的向目的地“写”数据的流,提供了丰富的 print(), println(), printf() 方法。System.out 和 System.err 就是 PrintStream 对象。
示例:使用PrintWriter写入文件
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class PrintWriterDemo {
public static void main(String[] args) {
try (FileWriter fw = new FileWriter("log.txt");
PrintWriter pw = new PrintWriter(fw)) { // 包装
pw.println("=== 程序日志 ===");
pw.printf("当前时间:%tF %<tT%n", System.currentTimeMillis());
pw.print("状态:正常结束");
// println 会自动换行,printf 和 print 不会
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.2 数据流 (DataInputStream & DataOutputStream)
用于读写Java基本数据类型(如int, double, boolean)和String,保持其数据的原始格式,而不是字符串形式。
示例:读写基本数据类型
import java.io.*;
public class DataStreamDemo {
public static void main(String[] args) {
// 写入数据
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.bin"))) {
dos.writeInt(100);
dos.writeDouble(3.14159);
dos.writeBoolean(true);
dos.writeUTF("Hello, 数据流!"); // 采用修改版UTF-8编码写入字符串
} catch (IOException e) {
e.printStackTrace();
}
// 读取数据 (必须按写入的顺序和类型读取)
try (DataInputStream dis = new DataInputStream(new FileInputStream("data.bin"))) {
int i = dis.readInt();
double d = dis.readDouble();
boolean b = dis.readBoolean();
String s = dis.readUTF();
System.out.printf("读取的数据:%d, %f, %b, %s%n", i, d, b, s);
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.3 对象流与序列化 (ObjectInputStream & ObjectOutputStream)
用于直接将对象写入流或从流中读取对象,这个过程称为序列化(写)和反序列化(读)。被序列化的类必须实现 java.io.Serializable 标记接口。
示例:对象的序列化与反序列化
import java.io.*;
// 1. 实现 Serializable 接口
class Person implements Serializable {
private static final long serialVersionUID = 1L; // 建议显式声明序列化ID
private String name;
private int age;
// 构造器、getter/setter、toString...
public Person(String name, int age) { this.name = name; this.age = age; }
@Override
public String toString() { return "Person{name='" + name + "', age=" + age + '}'; }
}
public class ObjectStreamDemo {
public static void main(String[] args) {
// 序列化对象到文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
Person person = new Person("张三", 25);
oos.writeObject(person); // 将对象写入流
System.out.println("对象序列化完成");
} catch (IOException e) {
e.printStackTrace();
}
// 从文件反序列化对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.bin"))) {
Person restoredPerson = (Person) ois.readObject(); // 从流中读取对象
System.out.println("反序列化得到的对象:" + restoredPerson);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
4.4 标准输入/输出流
Java提供了三个预定义的流对象:
System.in:InputStream类型,代表标准输入(通常是键盘)。System.out:PrintStream类型,代表标准输出。System.err:PrintStream类型,代表标准错误输出。
示例:从标准输入读取
import java.util.Scanner; // 更常用的方式,包装了System.in
public class StandardIO {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入您的姓名: ");
String name = scanner.nextLine();
System.out.println("你好, " + name + "!");
scanner.close();
}
}
总结
| 场景 | 推荐使用的流 |
|---|---|
| 二进制文件(图片、视频等)复制 | BufferedInputStream + BufferedOutputStream |
| 文本文件读写(使用默认编码) | BufferedReader + BufferedWriter |
| 文本文件读写(需指定编码) | InputStreamReader + OutputStreamWriter |
| 按行读取文本 | BufferedReader.readLine() |
| 方便地输出格式化的文本 | PrintWriter / PrintStream |
| 读写基本数据类型 | DataInputStream + DataOutputStream |
| 读写Java对象 | ObjectInputStream + ObjectOutputStream (需实现Serializable) |
最佳实践:
- 明确数据类型:先判断是二进制还是文本。
- 使用缓冲:几乎在所有情况下,都应该使用缓冲流来包装底层节点流。
- 使用try-with-resources:确保流被正确关闭,避免资源泄漏。
- 注意字符编码:在跨平台或国际化应用中,务必指定正确的字符编码(如UTF-8)。
更多推荐


所有评论(0)