IO流核心概念
Java IO包含面向字节与面向字符两类抽象。字节流处理原始二进制数据,字符流在字节与字符之间进行编码转换。阻塞IO在读写时占用线程,NIO通过缓冲区与通道支持更高效的文件与网络传输。
IO(Input/Output)是指数据的输入与输出操作。当应用程序需要从外部存储(如文件、数据库、网络)读取数据到内存时,这个过程称为输入;反之,将内存中的数据写出到外部存储的过程称为输出。由于数据传输的特性类似于水流,因此我们将这种数据传输机制称为IO流。
Java的IO流体系以4个抽象基类为核心,派生出40多个具体实现类:
- InputStream / Reader: 输入流的抽象基类,前者处理字节数据,后者处理字符数据
- OutputStream / Writer: 输出流的抽象基类,前者处理字节数据,后者处理字符数据
字节流操作
InputStream核心方法
InputStream是所有字节输入流的抽象父类,用于从数据源读取字节数据到内存。
核心API方法:
read(): 读取单个字节,返回0-255之间的int值,返回-1表示流结束 read(byte[] b): 将数据读入字节数组,返回实际读取的字节数 read(byte[] b, int off, int len): 指定偏移量和读取长度的读取操作 skip(long n): 跳过n个字节 available(): 返回可读取的字节数估计值 close(): 关闭流并释放系统资源 JDK 9新增的便捷方法:
readAllBytes(): 一次性读取所有字节 readNBytes(byte[] b, int off, int len): 阻塞直到读取len个字节 transferTo(OutputStream out): 将所有字节传输到输出流 FileInputStream文件读取示例 FileInputStream是最常用的字节输入流实现,可直接读取文件内容:
public class LogFileReader {
public void readAccessLog(String logPath) {
try (InputStream fis = new FileInputStream(logPath)) {
System.out.println("日志文件可读字节数: " + fis.available());
// 跳过前10个字节(通常是时间戳)
long skipped = fis.skip(10);
System.out.println("已跳过字节数: " + skipped);
System.out.print("访问记录内容: ");
int byteData;
while ((byteData = fis.read()) != -1) {
System.out.print((char) byteData);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
假设access.log文件内容为:
2024-11-26 UserLoginSuccess
输出结果:
日志文件可读字节数: 33 已跳过字节数: 10 访问记录内容: UserLoginSuccess
BufferedInputStream缓冲读取 实际项目中,我们通常会搭配BufferedInputStream来提升读取性能:
public class ConfigLoader {
public String loadConfig(String configPath) throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(configPath))) {
// 使用JDK 9+的便捷方法一次性读取全部内容
return new String(bis.readAllBytes());
}
}
}
DataInputStream类型化读取 DataInputStream可以读取Java基本数据类型,常用于读取二进制配置文件:
public class BinaryConfigReader {
public void readDatabaseConfig(String path) throws IOException {
try (DataInputStream dis = new DataInputStream(
new FileInputStream(path))) {
boolean isEnabled = dis.readBoolean(); // 读取布尔值
int maxConnections = dis.readInt(); // 读取整数
String dbUrl = dis.readUTF(); // 读取字符串
System.out.println("数据库启用状态: " + isEnabled);
System.out.println("最大连接数: " + maxConnections);
System.out.println("数据库URL: " + dbUrl);
}
}
}
ObjectInputStream对象反序列化 ObjectInputStream用于反序列化Java对象,对象必须实现Serializable接口:
public class UserSessionManager {
public UserSession restoreSession(String sessionFile) throws Exception {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(sessionFile))) {
return (UserSession) ois.readObject();
}
}
}
class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;
private String sessionToken;
private transient String password; // transient字段不会被序列化
// getters and setters...
}
OutputStream核心方法 OutputStream是所有字节输出流的抽象父类,用于将字节数据写入目标位置。
核心API方法:
write(int b): 写入单个字节 write(byte[] b): 写入字节数组 write(byte[] b, int off, int len): 写入字节数组的指定部分 flush(): 刷新缓冲区,强制写出缓存数据 close(): 关闭流并释放资源 FileOutputStream文件写入示例
public class AuditLogger {
public void logUserAction(String action) {
try (FileOutputStream fos = new FileOutputStream("audit.log", true)) { // true表示追加模式
byte[] actionBytes = action.getBytes();
fos.write(actionBytes);
fos.write('\n'); // 换行
} catch (IOException e) {
e.printStackTrace();
}
}
}
实际项目中通常搭配BufferedOutputStream使用:
public class ReportExporter {
public void exportReport(String reportPath, List<String> data) throws IOException {
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(reportPath))) {
for (String line : data) {
bos.write(line.getBytes());
bos.write('\n');
}
// BufferedOutputStream会在close时自动flush,也可手动flush
bos.flush();
}
}
}
DataOutputStream类型化写入
public class BinaryConfigWriter {
public void saveDatabaseConfig(String path) throws IOException {
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream(path))) {
dos.writeBoolean(true); // 写入布尔值
dos.writeInt(100); // 写入整数
dos.writeUTF("jdbc:mysql://localhost:3306/app"); // 写入字符串
}
}
}
ObjectOutputStream对象序列化
public class SessionPersistence {
public void saveSession(UserSession session, String path) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(path))) {
oos.writeObject(session);
}
}
}
字符流操作
字节流以字节为单位处理数据,如果直接用字节流处理文本会遇到编码问题。字符流的出现主要解决两个问题:
编码转换开销: 字符流内部已处理好编码转换,避免重复转换 乱码问题: 字符流会按照指定编码正确解析字符 让我们看看如果用字节流读取中文会发生什么:
// 错误示例:使用字节流读取中文
try (FileInputStream fis = new FileInputStream("message.txt")) {
int b;
while ((b = fis.read()) != -1) {
System.out.print((char) b); // 输出乱码: è¿æ¯ä¸æ
}
}
字符流默认使用Unicode编码,我们也可以通过构造方法指定编码方式。需要注意的是,Unicode只是字符集(给每个字符分配唯一编号),UTF-8、UTF-16、UTF-32才是具体的编码方案(定义如何存储这些编号)。例如UTF-8编码中,英文占1字节,中文占3字节。
Reader字符输入流 Reader是所有字符输入流的抽象基类,用于读取字符数据。
核心API方法:
read(): 读取单个字符 read(char[] cbuf): 读取字符到数组 read(char[] cbuf, int off, int len): 读取指定长度的字符 skip(long n): 跳过n个字符 close(): 关闭流释放资源 InputStreamReader桥接转换 InputStreamReader是字节流到字符流的桥梁,其子类FileReader进一步简化了文件读取:
// 继承关系
public class InputStreamReader extends Reader {
// 内部使用StreamDecoder进行解码
}
public class FileReader extends InputStreamReader {
// 简化文件读取操作
}
示例代码:
public class MultiLanguageReader {
public void readUserProfile(String profilePath) {
try (FileReader reader = new FileReader(profilePath)) {
int ch;
long skipped = reader.skip(5); // 跳过文件头标识
System.out.println("跳过的字符数: " + skipped);
System.out.print("用户信息: ");
while ((ch = reader.read()) != -1) {
System.out.print((char) ch);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
假设profile.txt内容为:
用户:张三,年龄:28
输出:
跳过的字符数: 5 用户信息: 三,年龄:28
Writer字符输出流 Writer是所有字符输出流的抽象基类,用于写入字符数据。
核心API方法:
write(int c): 写入单个字符 write(char[] cbuf): 写入字符数组 write(char[] cbuf, int off, int len): 写入字符数组的一部分 write(String str): 写入字符串 write(String str, int off, int len): 写入字符串的一部分 append(CharSequence csq): 追加字符序列 append(char c): 追加单个字符 flush(): 刷新缓冲区 close(): 关闭流 OutputStreamWriter桥接转换 OutputStreamWriter是字符流到字节流的桥梁:
public class OutputStreamWriter extends Writer {
// 内部使用StreamEncoder进行编码
}
public class FileWriter extends OutputStreamWriter {
// 简化文件写入操作
}
示例代码:
public class NotificationWriter {
public void saveNotification(String message) {
try (Writer writer = new FileWriter("notifications.txt", true)) {
writer.write("【系统通知】");
writer.write(message);
writer.write("\n");
} catch (IOException e) {
e.printStackTrace();
}
}
}
实战应用:大文件断点续传
典型应用场景:
前端上传 10GB 视频 → 切成 100 个 100MB 的分片 后端逐个接收分片 → 最后调用 mergeChunks() 合并成完整视频 targetFile:最终要生成的大文件路径,比如 /data/videos/movie.mp4 chunkFiles:分片文件列表,比如 [“/tmp/chunk_0”, “/tmp/chunk_1”, …] chunkSize:每个分片的预期大小(单位:字节),比如 100 * 1024 * 1024(100MB)
public void mergeChunksFast(String targetFile, List<String> chunkFiles) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(targetFile, "rw")) {
FileChannel out = raf.getChannel();
long position = 0;
for (String chunk : chunkFiles) {
try (RandomAccessFile chunkRaf = new RandomAccessFile(chunk, "r")) {
FileChannel in = chunkRaf.getChannel();
// 零拷贝:直接从分片文件通道传输到目标通道
in.transferTo(0, in.size(), out.position(position));
position += in.size();
}
}
out.truncate(position); // 截断多余空间
}
}