Java IO 流

38 阅读6分钟

一、一句话总结

IO流 = 数据流动的管道,分字节流(8位)和字符流(16位),用于文件读写、网络通信等数据交换

二、核心分类体系

按数据类型:
├── 字节流 (byte, 8位)
│   ├── InputStream (抽象类)
│   │   ├── FileInputStream (文件)
│   │   ├── ByteArrayInputStream (内存)
│   │   └── BufferedInputStream (缓冲)
│   │
│   └── OutputStream (抽象类)
│       ├── FileOutputStream (文件)
│       ├── ByteArrayOutputStream (内存)
│       └── BufferedOutputStream (缓冲)
│
└── 字符流 (char, 16位, 处理文本)
    ├── Reader (抽象类)
    │   ├── FileReader (文件)
    │   ├── InputStreamReader (字节→字符)
    │   └── BufferedReader (缓冲)
    │
    └── Writer (抽象类)
        ├── FileWriter (文件)
        ├── OutputStreamWriter (字符→字节)
        └── BufferedWriter (缓冲)

三、字节流(处理所有类型文件)

1. FileInputStream / FileOutputStream

// 1. 读取文件(字节流)
try (FileInputStream fis = new FileInputStream("input.txt")) {
    int byteData;
    while ((byteData = fis.read()) != -1) {  // 一次读1字节
        System.out.print((char) byteData);
    }
}

// 2. 写入文件(字节流)
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
    String text = "Hello World";
    fos.write(text.getBytes());  // 字符串转字节数组写入
}

// 3. 文件复制(字节流)
try (FileInputStream fis = new FileInputStream("source.jpg");
     FileOutputStream fos = new FileOutputStream("copy.jpg")) {
    byte[] buffer = new byte[1024];  // 1KB缓冲区
    int bytesRead;
    while ((bytesRead = fis.read(buffer)) != -1) {
        fos.write(buffer, 0, bytesRead);  // 写入实际读取的字节数
    }
}

2. BufferedInputStream / BufferedOutputStream

// 缓冲流(性能更好)
try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("largefile.dat"));
     BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("copy.dat"))) {
    
    byte[] buffer = new byte[8192];  // 8KB缓冲区
    int bytesRead;
    while ((bytesRead = bis.read(buffer)) != -1) {
        bos.write(buffer, 0, bytesRead);
    }
}

四、字符流(处理文本文件)

1. FileReader / FileWriter

// 1. 读取文本文件
try (FileReader reader = new FileReader("text.txt")) {
    int charData;
    while ((charData = reader.read()) != -1) {  // 一次读1字符
        System.out.print((char) charData);
    }
}

// 2. 写入文本文件
try (FileWriter writer = new FileWriter("output.txt")) {
    writer.write("Hello Java IO\n");
    writer.write("第二行内容");
}

// 3. 追加写入
try (FileWriter writer = new FileWriter("log.txt", true)) {  // true=追加
    writer.write("新的日志记录\n");
}

2. BufferedReader / BufferedWriter

// 1. 缓冲读取(高效)
try (BufferedReader br = new BufferedReader(
        new FileReader("largefile.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {  // 按行读取
        System.out.println(line);
    }
}

// 2. 缓冲写入
try (BufferedWriter bw = new BufferedWriter(
        new FileWriter("output.txt"))) {
    bw.write("第一行");
    bw.newLine();  // 换行
    bw.write("第二行");
    bw.flush();    // 刷新缓冲区
}

五、转换流(字节↔字符)

1. InputStreamReader / OutputStreamWriter

// 1. 指定编码读取(解决中文乱码)
try (InputStreamReader isr = new InputStreamReader(
        new FileInputStream("gbk.txt"), "GBK")) {  // 指定GBK编码
    int charData;
    while ((charData = isr.read()) != -1) {
        System.out.print((char) charData);
    }
}

// 2. 指定编码写入
try (OutputStreamWriter osw = new OutputStreamWriter(
        new FileOutputStream("utf8.txt"), StandardCharsets.UTF_8)) {
    osw.write("中文内容");
}

// 3. 控制台输入(System.in是字节流)
try (BufferedReader console = new BufferedReader(
        new InputStreamReader(System.in))) {
    System.out.print("请输入: ");
    String input = console.readLine();
    System.out.println("你输入了: " + input);
}

六、数据流(处理基本数据类型)

1. DataInputStream / DataOutputStream

// 写入基本数据类型
try (DataOutputStream dos = new DataOutputStream(
        new FileOutputStream("data.bin"))) {
    dos.writeInt(100);        // 4字节
    dos.writeDouble(3.14);    // 8字节
    dos.writeBoolean(true);   // 1字节
    dos.writeUTF("字符串");   // UTF-8编码
}

// 读取基本数据类型(必须按写入顺序读)
try (DataInputStream dis = new DataInputStream(
        new FileInputStream("data.bin"))) {
    int intValue = dis.readInt();         // 100
    double doubleValue = dis.readDouble(); // 3.14
    boolean boolValue = dis.readBoolean(); // true
    String strValue = dis.readUTF();      // "字符串"
}

七、对象流(序列化)

1. ObjectInputStream / ObjectOutputStream

// 对象必须实现Serializable接口
class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    // transient字段不会被序列化
    private transient String password;
    
    // 构造器、getter、setter
}

// 序列化对象到文件
try (ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream("person.dat"))) {
    Person person = new Person("张三", 25);
    oos.writeObject(person);
}

// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("person.dat"))) {
    Person person = (Person) ois.readObject();
    System.out.println(person.getName());  // 张三
}

八、NIO(New IO,Java 1.4+)

1. Path 和 Files 工具类

import java.nio.file.*;

// 1. 文件操作(更简单的方式)
Path path = Paths.get("test.txt");

// 读取所有行
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);

// 写入文件
Files.write(path, "内容".getBytes(), StandardOpenOption.CREATE);

// 复制文件
Files.copy(Paths.get("source.txt"), Paths.get("dest.txt"));

// 判断文件是否存在
boolean exists = Files.exists(path);

// 2. 遍历目录
try (Stream<Path> paths = Files.walk(Paths.get("."), 3)) {  // 深度3层
    paths.filter(Files::isRegularFile)
         .filter(p -> p.toString().endsWith(".java"))
         .forEach(System.out::println);
}

2. Files 常用方法

// 文件属性
long size = Files.size(path);
FileTime lastModified = Files.getLastModifiedTime(path);
boolean isDirectory = Files.isDirectory(path);

// 创建目录
Files.createDirectories(Paths.get("a/b/c"));  // 创建多级目录

// 移动/重命名
Files.move(Paths.get("old.txt"), Paths.get("new.txt"));

// 删除
Files.deleteIfExists(path);  // 存在才删除,不会抛异常

九、实际应用示例

1. 配置文件读取

public class ConfigReader {
    private Properties props = new Properties();
    
    public void loadConfig(String filename) {
        try (InputStream is = new FileInputStream(filename)) {
            props.load(is);  // Properties自动处理编码
        } catch (IOException e) {
            // 使用类路径资源作为后备
            try (InputStream is = getClass().getClassLoader()
                    .getResourceAsStream("default.properties")) {
                props.load(is);
            }
        }
    }
    
    public String getProperty(String key) {
        return props.getProperty(key);
    }
}

// 使用
ConfigReader config = new ConfigReader();
config.loadConfig("app.properties");
String dbUrl = config.getProperty("database.url");

2. 日志记录器

public class FileLogger {
    private BufferedWriter writer;
    
    public FileLogger(String logFile) throws IOException {
        // 追加模式,自动刷新
        writer = new BufferedWriter(
            new OutputStreamWriter(
                new FileOutputStream(logFile, true), 
                StandardCharsets.UTF_8
            )
        );
    }
    
    public void log(String level, String message) {
        try {
            String timestamp = LocalDateTime.now().toString();
            writer.write(String.format("[%s] %s: %s", 
                timestamp, level, message));
            writer.newLine();
            writer.flush();
        } catch (IOException e) {
            System.err.println("日志写入失败: " + e.getMessage());
        }
    }
    
    public void close() {
        try {
            writer.close();
        } catch (IOException e) {
            // 忽略关闭异常
        }
    }
}

3. CSV文件处理

public class CSVProcessor {
    
    public List<Map<String, String>> readCSV(String filename) {
        List<Map<String, String>> records = new ArrayList<>();
        
        try (BufferedReader br = new BufferedReader(
                new FileReader(filename))) {
            
            String headerLine = br.readLine();
            if (headerLine == null) return records;
            
            String[] headers = headerLine.split(",");
            String line;
            
            while ((line = br.readLine()) != null) {
                String[] values = line.split(",");
                Map<String, String> record = new LinkedHashMap<>();
                
                for (int i = 0; i < headers.length && i < values.length; i++) {
                    record.put(headers[i].trim(), values[i].trim());
                }
                records.add(record);
            }
        }
        return records;
    }
    
    public void writeCSV(String filename, List<Map<String, String>> data) {
        if (data.isEmpty()) return;
        
        try (BufferedWriter bw = new BufferedWriter(
                new FileWriter(filename))) {
            
            // 写入表头
            Set<String> headers = data.get(0).keySet();
            bw.write(String.join(",", headers));
            bw.newLine();
            
            // 写入数据
            for (Map<String, String> record : data) {
                List<String> values = new ArrayList<>();
                for (String header : headers) {
                    values.add(record.getOrDefault(header, ""));
                }
                bw.write(String.join(",", values));
                bw.newLine();
            }
        }
    }
}

十、最佳实践和注意事项

1. 资源自动关闭(try-with-resources)

// ✅ 推荐:自动关闭资源
try (FileInputStream fis = new FileInputStream("file.txt");
     BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
    // 使用资源
    String line = br.readLine();
}

// ❌ 不要:手动关闭容易遗漏
FileInputStream fis = null;
try {
    fis = new FileInputStream("file.txt");
    // 使用资源
} finally {
    if (fis != null) {
        try { fis.close(); } catch (IOException e) { /* 忽略 */ }
    }
}

2. 处理大文件

// 大文件处理:使用缓冲,分块读取
try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("largefile.bin"), 8192)) {  // 8KB缓冲
    
    byte[] buffer = new byte[8192];
    int bytesRead;
    long totalBytes = 0;
    
    while ((bytesRead = bis.read(buffer)) != -1) {
        totalBytes += bytesRead;
        // 处理buffer中的前bytesRead个字节
        processChunk(buffer, bytesRead);
        
        // 显示进度(可选)
        if (totalBytes % (1024 * 1024) == 0) {  // 每MB显示一次
            System.out.printf("已处理: %.2f MB%n", totalBytes / 1024.0 / 1024.0);
        }
    }
}

3. 编码问题

// 明确指定编码,避免乱码
String content = "中文内容";

// 写入时指定编码
try (OutputStreamWriter osw = new OutputStreamWriter(
        new FileOutputStream("file.txt"), 
        StandardCharsets.UTF_8)) {
    osw.write(content);
}

// 读取时指定编码
try (InputStreamReader isr = new InputStreamReader(
        new FileInputStream("file.txt"), 
        StandardCharsets.UTF_8)) {
    char[] buffer = new char[1024];
    int charsRead;
    StringBuilder sb = new StringBuilder();
    while ((charsRead = isr.read(buffer)) != -1) {
        sb.append(buffer, 0, charsRead);
    }
    System.out.println(sb.toString());
}

4. 性能对比

// 性能比较(从快到慢):
// 1. NIO.2 (Files.readAllBytes/readAllLines)
// 2. BufferedInputStream/BufferedOutputStream
// 3. FileInputStream/FileOutputStream (无缓冲)
// 4. Scanner (方便但慢)

// 选择建议:
// - 小文件:Files.readAllBytes()
// - 大文件:BufferedInputStream/OutputStream
// - 文本文件:BufferedReader/BufferedWriter
// - 需要行处理:Files.lines() + Stream API

十一、常见问题解决

1. 文件锁问题

// 检查文件是否被其他进程占用
public boolean isFileLocked(Path path) {
    try (FileChannel channel = FileChannel.open(path, 
            StandardOpenOption.WRITE)) {
        // 尝试获取排他锁
        FileLock lock = channel.tryLock();
        if (lock != null) {
            lock.release();
            return false;  // 文件未被锁定
        }
    } catch (Exception e) {
        return true;  // 无法获取锁,可能被占用
    }
    return true;
}

2. 临时文件处理

// 创建临时文件
Path tempFile = Files.createTempFile("prefix", ".tmp");
try {
    // 使用临时文件
    Files.write(tempFile, "临时内容".getBytes());
    // 处理完成后自动删除
} finally {
    Files.deleteIfExists(tempFile);  // 确保删除
}

// 或者使用deleteOnExit
File temp = File.createTempFile("temp", ".txt");
temp.deleteOnExit();  // JVM退出时删除

总结要点

核心选择:

  1. 字节流:处理图片、视频、exe等所有文件
  2. 字符流:处理文本文件(.txt, .java, .html)
  3. 缓冲流:总是使用,提高性能
  4. 转换流:处理编码问题

最佳实践:

  1. 总是用try-with-resources
  2. 明确指定字符编码
  3. 大文件用缓冲和分块处理
  4. NIO.2的Files类更简洁

记忆口诀:

  • 字节流:InputStream/OutputStream(处理一切)
  • 字符流:Reader/Writer(处理文本)
  • 缓冲流:BufferedXXX(性能必备)
  • 转换流:指定编码(解决乱码)