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); // 截断多余空间
    }
}