4
0

Java 入门指南:第四章 - Java IO 与文件操作

2026-09-07
2026-09-07
文章摘要
|

第三章我们深入学习了集合框架,本章将进入 Java 的输入输出(IO)系统。IO 是程序与外部世界交互的桥梁,无论是读取文件、网络通信还是控制台交互,都离不开 IO 操作。我们将从传统的 BIO(阻塞 IO)到 NIO(新 IO)进行全面讲解。


第四章:Java IO 与文件操作

4.1 IO 流概述

4.1.1 什么是 IO 流?

IO 流(Input/Output Stream)是一系列数据的序列,可以看作是一根管道,数据在管道中流动。Java 的 IO 流分为两大体系:

字节流(处理二进制数据)
├── InputStream(输入字节流)
│   ├── FileInputStream
│   ├── ByteArrayInputStream
│   ├── BufferedInputStream
│   └── ObjectInputStream
└── OutputStream(输出字节流)
    ├── FileOutputStream
    ├── ByteArrayOutputStream
    ├── BufferedOutputStream
    └── ObjectOutputStream

字符流(处理文本数据)
├── Reader(输入字符流)
│   ├── FileReader
│   ├── BufferedReader
│   ├── InputStreamReader
│   └── StringReader
└── Writer(输出字符流)
    ├── FileWriter
    ├── BufferedWriter
    ├── OutputStreamWriter
    └── StringWriter

4.1.2 字节流 vs 字符流

特性 字节流 字符流
处理单位 字节(8位) 字符(16位 Unicode)
适用场景 二进制文件(图片、音频、视频) 文本文件
基类 InputStream / OutputStream Reader / Writer
编码处理 需要手动处理 自动处理编码
public class StreamOverview {
    public static void main(String[] args) {
        // 1. 字节流:适合所有类型文件
        try (FileInputStream fis = new FileInputStream("data.bin");
             FileOutputStream fos = new FileOutputStream("output.bin")) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 2. 字符流:专门处理文本
        try (FileReader fr = new FileReader("input.txt");
             FileWriter fw = new FileWriter("output.txt")) {
            char[] buffer = new char[1024];
            int len;
            while ((len = fr.read(buffer)) != -1) {
                fw.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.2 File 类:文件和目录操作

File 类用于表示文件和目录的路径名,但它并不真正操作文件内容,而是操作文件本身的元数据。

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;

public class FileDemo {
    public static void main(String[] args) throws IOException {
        // 1. 创建 File 对象
        File file = new File("example.txt");
        File dir = new File("mydir");
        File absoluteFile = new File("/Users/username/Documents/file.txt");
        
        // 2. 创建文件
        boolean created = file.createNewFile();
        System.out.println("文件创建" + (created ? "成功" : "失败(可能已存在)"));
        
        // 3. 创建目录
        boolean dirCreated = dir.mkdir();
        System.out.println("目录创建" + (dirCreated ? "成功" : "失败"));
        
        // 创建多级目录
        File multiDir = new File("parent/child/grandchild");
        boolean multiCreated = multiDir.mkdirs();
        System.out.println("多级目录创建" + (multiCreated ? "成功" : "失败"));
        
        // 4. 文件信息
        System.out.println("=== 文件信息 ===");
        System.out.println("名称:" + file.getName());
        System.out.println("绝对路径:" + file.getAbsolutePath());
        System.out.println("路径:" + file.getPath());
        System.out.println("父目录:" + file.getParent());
        System.out.println("文件大小:" + file.length() + " bytes");
        
        // 5. 判断方法
        System.out.println("\n=== 判断 ===");
        System.out.println("是否存在:" + file.exists());
        System.out.println("是否文件:" + file.isFile());
        System.out.println("是否目录:" + file.isDirectory());
        System.out.println("是否隐藏:" + file.isHidden());
        System.out.println("是否可读:" + file.canRead());
        System.out.println("是否可写:" + file.canWrite());
        
        // 6. 修改时间
        long lastModified = file.lastModified();
        if (lastModified > 0) {
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
                    .format(new Date(lastModified));
            System.out.println("最后修改时间:" + time);
        }
        
        // 7. 重命名
        File newFile = new File("renamed.txt");
        boolean renamed = file.renameTo(newFile);
        System.out.println("重命名" + (renamed ? "成功" : "失败"));
        
        // 8. 列出目录内容
        File currentDir = new File(".");
        String[] list = currentDir.list();
        System.out.println("\n当前目录内容:");
        if (list != null) {
            for (String name : list) {
                File item = new File(name);
                String type = item.isDirectory() ? "[目录]" : "[文件]";
                System.out.println(type + " " + name);
            }
        }
        
        // 9. 列出所有文件(使用 FileFilter)
        File[] txtFiles = currentDir.listFiles((dir2, name) -> name.endsWith(".txt"));
        System.out.println("\n所有 .txt 文件:");
        if (txtFiles != null) {
            for (File f : txtFiles) {
                System.out.println(f.getName());
            }
        }
        
        // 10. 删除文件
        // boolean deleted = newFile.delete();
        // System.out.println("删除" + (deleted ? "成功" : "失败"));
        
        // 11. 删除目录(必须为空)
        // boolean dirDeleted = dir.delete();
        // System.out.println("删除目录" + (dirDeleted ? "成功" : "失败"));
        
        // 12. 临时文件
        File tempFile = File.createTempFile("temp", ".tmp");
        tempFile.deleteOnExit(); // JVM 退出时删除
        System.out.println("临时文件:" + tempFile.getAbsolutePath());
        
        // 13. 分隔符(跨平台)
        System.out.println("路径分隔符:" + File.pathSeparator);
        System.out.println("文件分隔符:" + File.separator);
    }
}

4.3 字节流:InputStream 和 OutputStream

4.3.1 FileInputStream 和 FileOutputStream

public class ByteStreamDemo {
    public static void main(String[] args) {
        // 1. 写入文件
        writeFile();
        
        // 2. 读取文件
        readFile();
        
        // 3. 文件复制
        copyFile("source.txt", "destination.txt");
        
        // 4. 追加内容
        appendFile("log.txt", "新的日志内容\n");
    }
    
    // 写入文件
    public static void writeFile() {
        // try-with-resources 自动关闭流
        try (FileOutputStream fos = new FileOutputStream("output.txt")) {
            String content = "Hello, Java IO!";
            byte[] bytes = content.getBytes();
            fos.write(bytes);
            System.out.println("文件写入成功");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // 读取文件
    public static void readFile() {
        try (FileInputStream fis = new FileInputStream("output.txt")) {
            // 方式1:逐个字节读取(慢)
            // int b;
            // while ((b = fis.read()) != -1) {
            //     System.out.print((char) b);
            // }
            
            // 方式2:批量读取(推荐)
            byte[] buffer = new byte[1024];
            int len;
            StringBuilder content = new StringBuilder();
            while ((len = fis.read(buffer)) != -1) {
                content.append(new String(buffer, 0, len));
            }
            System.out.println("文件内容:" + content.toString());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // 文件复制
    public static void copyFile(String source, String dest) {
        long startTime = System.currentTimeMillis();
        
        try (FileInputStream fis = new FileInputStream(source);
             FileOutputStream fos = new FileOutputStream(dest)) {
            
            byte[] buffer = new byte[8192]; // 8KB 缓冲区
            int len;
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
            
            long endTime = System.currentTimeMillis();
            System.out.println("文件复制完成,耗时:" + (endTime - startTime) + "ms");
            
        } catch (FileNotFoundException e) {
            System.err.println("文件不存在:" + e.getMessage());
        } catch (IOException e) {
            System.err.println("复制失败:" + e.getMessage());
        }
    }
    
    // 追加内容
    public static void appendFile(String filename, String content) {
        // 第二个参数 true 表示追加
        try (FileOutputStream fos = new FileOutputStream(filename, true)) {
            fos.write(content.getBytes());
            System.out.println("追加成功");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.3.2 BufferedInputStream 和 BufferedOutputStream

public class BufferedStreamDemo {
    public static void main(String[] args) {
        // 使用缓冲流提高性能(内部缓冲区默认 8192 bytes)
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("large_file.dat"));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copied_file.dat"))) {
            
            byte[] buffer = new byte[8192];
            int len;
            while ((len = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            
            // 强制刷新缓冲区(close 时会自动调用)
            bos.flush();
            System.out.println("缓冲复制完成");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.3.3 DataInputStream 和 DataOutputStream

用于读写 Java 基本数据类型,保持数据格式一致。

public class DataStreamDemo {
    public static void main(String[] args) {
        // 写入基本数据类型
        try (DataOutputStream dos = new DataOutputStream(
                new FileOutputStream("data.dat"))) {
            
            dos.writeInt(100);
            dos.writeDouble(99.99);
            dos.writeBoolean(true);
            dos.writeUTF("Hello, 世界!"); // UTF-8 编码字符串
            
            System.out.println("数据写入完成");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 读取基本数据类型(顺序必须与写入一致)
        try (DataInputStream dis = new DataInputStream(
                new FileInputStream("data.dat"))) {
            
            int i = dis.readInt();
            double d = dis.readDouble();
            boolean b = dis.readBoolean();
            String s = dis.readUTF();
            
            System.out.println("读取的数据:");
            System.out.println("int: " + i);
            System.out.println("double: " + d);
            System.out.println("boolean: " + b);
            System.out.println("String: " + s);
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.4 字符流:Reader 和 Writer

4.4.1 FileReader 和 FileWriter

public class CharStreamDemo {
    public static void main(String[] args) {
        // 1. 写入文本文件
        writeText();
        
        // 2. 读取文本文件
        readText();
        
        // 3. 带编码的读写
        readWithEncoding();
    }
    
    public static void writeText() {
        try (FileWriter fw = new FileWriter("chars.txt")) {
            fw.write("第一行文字\n");
            fw.write("第二行文字\n");
            fw.write("第三行文字\n");
            
            // 写入字符数组
            char[] chars = {'J', 'a', 'v', 'a'};
            fw.write(chars);
            
            System.out.println("字符写入完成");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void readText() {
        try (FileReader fr = new FileReader("chars.txt")) {
            // 方式1:逐个字符读取
            // int ch;
            // while ((ch = fr.read()) != -1) {
            //     System.out.print((char) ch);
            // }
            
            // 方式2:批量读取
            char[] buffer = new char[1024];
            int len;
            StringBuilder content = new StringBuilder();
            while ((len = fr.read(buffer)) != -1) {
                content.append(buffer, 0, len);
            }
            System.out.println("文件内容:\n" + content);
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void readWithEncoding() {
        // 指定编码读取(GBK)
        try (InputStreamReader isr = new InputStreamReader(
                new FileInputStream("gbk_file.txt"), "GBK");
             BufferedReader br = new BufferedReader(isr)) {
            
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.4.2 BufferedReader 和 BufferedWriter

public class BufferedCharDemo {
    public static void main(String[] args) {
        // 使用缓冲字符流提高文本读写效率
        // 并支持按行读写
        
        // 1. 按行写入
        try (BufferedWriter bw = new BufferedWriter(
                new FileWriter("lines.txt"))) {
            
            bw.write("第一行");
            bw.newLine(); // 跨平台换行
            bw.write("第二行");
            bw.newLine();
            bw.write("第三行");
            
            bw.flush();
            System.out.println("行写入完成");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 2. 按行读取(推荐方式)
        try (BufferedReader br = new BufferedReader(
                new FileReader("lines.txt"))) {
            
            String line;
            int lineNum = 0;
            while ((line = br.readLine()) != null) {
                lineNum++;
                System.out.println("第 " + lineNum + " 行: " + line);
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.4.3 PrintWriter 和 PrintStream

public class PrintDemo {
    public static void main(String[] args) {
        // 1. PrintWriter:便捷的文本输出
        try (PrintWriter pw = new PrintWriter(new FileWriter("print.txt"))) {
            pw.println("格式化输出");
            pw.printf("姓名: %s, 年龄: %d, 薪资: %.2f%n", "张三", 25, 8000.50);
            pw.print("自动 flush: ");
            pw.flush();
            
            System.out.println("PrintWriter 写入完成");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 2. System.out 就是 PrintStream
        System.out.println("这是 System.out");
        
        // 3. 重定向输出到文件
        try (PrintStream ps = new PrintStream(new FileOutputStream("redirect.log"))) {
            // 保存原始输出流
            PrintStream originalOut = System.out;
            
            // 重定向
            System.setOut(ps);
            System.out.println("这一行会写入到文件");
            System.out.println("而不是控制台");
            
            // 恢复
            System.setOut(originalOut);
            System.out.println("恢复控制台输出");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.5 对象序列化

对象序列化(Serialization)是将 Java 对象转换为字节流,以便存储到文件或通过网络传输。

4.5.1 序列化基础

import java.io.*;

// 必须实现 Serializable 接口
class Person implements Serializable {
    private static final long serialVersionUID = 1L; // 版本号
    
    private String name;
    private int age;
    private transient String password; // transient 字段不会被序列化
    private static String company = "ABC Corp"; // 静态变量不会被序列化
    
    public Person(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }
    
    @Override
    public String toString() {
        return String.format("Person{name='%s', age=%d, password='%s', company='%s'}", 
                name, age, password, company);
    }
}

public class SerializationDemo {
    public static void main(String[] args) {
        // 1. 序列化
        serializeObject();
        
        // 2. 反序列化
        deserializeObject();
        
        // 3. 序列化集合
        serializeCollection();
    }
    
    public static void serializeObject() {
        Person person = new Person("李四", 30, "secret123");
        
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("person.ser"))) {
            
            oos.writeObject(person);
            System.out.println("对象序列化成功:" + person);
            System.out.println("password 字段被 transient 修饰,不会被序列化");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void deserializeObject() {
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("person.ser"))) {
            
            Person person = (Person) ois.readObject();
            System.out.println("对象反序列化成功:" + person);
            System.out.println("password 为 null(因为 transient)");
            System.out.println("company 为静态变量初始值(不会被序列化)");
            
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    public static void serializeCollection() {
        // 序列化集合(集合中的元素也必须可序列化)
        Person p1 = new Person("张三", 25, "pwd1");
        Person p2 = new Person("王五", 28, "pwd2");
        
        java.util.ArrayList<Person> list = new java.util.ArrayList<>();
        list.add(p1);
        list.add(p2);
        
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("list.ser"))) {
            
            oos.writeObject(list);
            System.out.println("集合序列化成功");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 反序列化集合
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("list.ser"))) {
            
            @SuppressWarnings("unchecked")
            java.util.ArrayList<Person> deserializedList = 
                    (java.util.ArrayList<Person>) ois.readObject();
            
            System.out.println("集合反序列化成功:");
            deserializedList.forEach(System.out::println);
            
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

4.5.2 自定义序列化

class CustomSerializable implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String data;
    private int number;
    
    public CustomSerializable(String data, int number) {
        this.data = data;
        this.number = number;
    }
    
    // 自定义序列化
    private void writeObject(ObjectOutputStream out) throws IOException {
        // 先写入默认序列化内容
        out.defaultWriteObject();
        
        // 额外写入自定义数据
        out.writeInt(number * 2); // 加密或转换
        System.out.println("自定义序列化完成");
    }
    
    // 自定义反序列化
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 先读取默认序列化内容
        in.defaultReadObject();
        
        // 读取自定义数据
        int customValue = in.readInt();
        this.number = customValue / 2; // 还原
        System.out.println("自定义反序列化完成");
    }
    
    @Override
    public String toString() {
        return String.format("Custom{data='%s', number=%d}", data, number);
    }
}

public class CustomSerializationDemo {
    public static void main(String[] args) throws Exception {
        CustomSerializable obj = new CustomSerializable("测试数据", 100);
        
        // 序列化
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("custom.ser"))) {
            oos.writeObject(obj);
        }
        
        // 反序列化
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("custom.ser"))) {
            CustomSerializable result = (CustomSerializable) ois.readObject();
            System.out.println("反序列化结果:" + result);
        }
    }
}

4.6 NIO(New IO)简介

Java NIO(New Input/Output)是 JDK 1.4 引入的新 IO API,提供非阻塞 IO 和更高效的文件操作。

4.6.1 核心概念

  • Channel(通道):双向数据传输的通道
  • Buffer(缓冲区):存储数据的容器
  • Selector(选择器):多路复用器,用于非阻塞 IO

4.6.2 文件操作(NIO.2,JDK 7+)

import java.nio.file.*;
import java.nio.*;
import java.io.*;

public class NIODemo {
    public static void main(String[] args) throws IOException {
        // 1. 创建文件
        Path path = Paths.get("nio_example.txt");
        if (Files.notExists(path)) {
            Files.createFile(path);
            System.out.println("文件创建成功:" + path);
        }
        
        // 2. 写入文件
        String content = "NIO 文件写入测试\n第二行\n第三行";
        Files.write(path, content.getBytes(), StandardOpenOption.WRITE);
        System.out.println("写入成功");
        
        // 3. 读取文件
        byte[] bytes = Files.readAllBytes(path);
        String readContent = new String(bytes);
        System.out.println("读取内容:\n" + readContent);
        
        // 4. 按行读取
        System.out.println("\n按行读取:");
        Files.lines(path).forEach(System.out::println);
        
        // 5. 复制文件
        Path dest = Paths.get("nio_copy.txt");
        Files.copy(path, dest, StandardCopyOption.REPLACE_EXISTING);
        System.out.println("复制成功");
        
        // 6. 移动/重命名
        Path moved = Paths.get("nio_moved.txt");
        Files.move(dest, moved, StandardCopyOption.REPLACE_EXISTING);
        System.out.println("移动成功");
        
        // 7. 文件属性
        System.out.println("\n文件属性:");
        System.out.println("大小:" + Files.size(path));
        System.out.println("是否文件:" + Files.isRegularFile(path));
        System.out.println("是否目录:" + Files.isDirectory(path));
        System.out.println("最后修改时间:" + Files.getLastModifiedTime(path));
        
        // 8. 遍历目录
        System.out.println("\n当前目录内容:");
        try (var stream = Files.list(Paths.get("."))) {
            stream.forEach(p -> System.out.println("  " + p.getFileName()));
        }
        
        // 9. 递归遍历
        System.out.println("\n递归遍历当前目录:");
        try (var walk = Files.walk(Paths.get("."), 2)) { // 深度限制
            walk.forEach(p -> {
                String indent = "  ".repeat(p.getNameCount() - 1);
                System.out.println(indent + p.getFileName());
            });
        }
        
        // 10. 删除文件
        // Files.deleteIfExists(moved);
        // System.out.println("文件已删除");
        
        // 11. 创建临时文件
        Path tempFile = Files.createTempFile("prefix", ".tmp");
        System.out.println("临时文件:" + tempFile);
        
        // 12. 文件监视(WatchService)
        watchDirectory();
    }
    
    public static void watchDirectory() throws IOException {
        // 监控目录变化
        Path dir = Paths.get(".");
        WatchService watchService = FileSystems.getDefault().newWatchService();
        dir.register(watchService,
                StandardWatchEventKinds.ENTRY_CREATE,
                StandardWatchEventKinds.ENTRY_DELETE,
                StandardWatchEventKinds.ENTRY_MODIFY);
        
        System.out.println("\n开始监控目录变化...");
        // 在实际使用中,可以在单独线程中监控
        // 这里仅演示注册
        watchService.close();
    }
}

4.6.3 Buffer 和 Channel 操作

import java.nio.*;
import java.nio.channels.*;
import java.io.*;

public class BufferChannelDemo {
    public static void main(String[] args) throws IOException {
        // 1. 使用 Channel 和 Buffer 读写文件
        Path path = Paths.get("channel_demo.txt");
        
        // 写入
        try (FileChannel channel = FileChannel.open(path, 
                StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
            
            String data = "Hello, NIO Channel!";
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.put(data.getBytes());
            buffer.flip(); // 切换为读模式
            
            while (buffer.hasRemaining()) {
                channel.write(buffer);
            }
            System.out.println("Channel 写入完成");
        }
        
        // 读取
        try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int bytesRead = channel.read(buffer);
            
            buffer.flip();
            byte[] bytes = new byte[bytesRead];
            buffer.get(bytes);
            System.out.println("Channel 读取内容:" + new String(bytes));
        }
        
        // 2. 使用 MappedByteBuffer(内存映射文件,高效)
        try (FileChannel channel = FileChannel.open(path,
                StandardOpenOption.READ, StandardOpenOption.WRITE)) {
            
            long size = channel.size();
            MappedByteBuffer mappedBuffer = channel.map(
                    FileChannel.MapMode.READ_WRITE, 0, size);
            
            // 直接操作缓冲区,修改会反映到文件
            mappedBuffer.put(0, (byte) 'X');
            System.out.println("内存映射文件修改完成");
        }
    }
}

4.7 实用案例

4.7.1 配置文件读取器

import java.util.*;

public class ConfigReader {
    private Properties properties = new Properties();
    
    public void load(String filePath) throws IOException {
        try (InputStream input = new FileInputStream(filePath)) {
            properties.load(input);
            System.out.println("配置文件加载成功:" + filePath);
        }
    }
    
    public String get(String key) {
        return properties.getProperty(key);
    }
    
    public String get(String key, String defaultValue) {
        return properties.getProperty(key, defaultValue);
    }
    
    public int getInt(String key, int defaultValue) {
        String value = get(key);
        try {
            return value != null ? Integer.parseInt(value) : defaultValue;
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }
    
    public static void main(String[] args) {
        // 创建配置文件 config.properties
        // database.url=jdbc:mysql://localhost:3306/test
        // database.username=root
        // database.password=123456
        // app.name=MyApp
        // app.version=1.0
        
        ConfigReader config = new ConfigReader();
        try {
            config.load("config.properties");
            System.out.println("数据库 URL:" + config.get("database.url"));
            System.out.println("用户名:" + config.get("database.username", "admin"));
            System.out.println("应用名称:" + config.get("app.name"));
            System.out.println("版本:" + config.getInt("app.version", 1));
        } catch (IOException e) {
            System.err.println("加载配置文件失败:" + e.getMessage());
        }
    }
}

4.7.2 日志记录器

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

class Logger {
    private static final String LOG_FILE = "application.log";
    private static final DateTimeFormatter FORMATTER = 
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
    // 使用 volatile 保证可见性
    private static volatile Logger instance;
    
    private Logger() {}
    
    public static Logger getInstance() {
        if (instance == null) {
            synchronized (Logger.class) {
                if (instance == null) {
                    instance = new Logger();
                }
            }
        }
        return instance;
    }
    
    private void writeLog(String level, String message) {
        String logEntry = String.format("[%s] [%s] %s%n", 
                LocalDateTime.now().format(FORMATTER), level, message);
        
        // 写入日志文件
        try (FileWriter fw = new FileWriter(LOG_FILE, true);
             BufferedWriter bw = new BufferedWriter(fw)) {
            bw.write(logEntry);
        } catch (IOException e) {
            System.err.println("日志写入失败:" + e.getMessage());
        }
        
        // 同时输出到控制台
        System.out.print(logEntry);
    }
    
    public void info(String message) {
        writeLog("INFO", message);
    }
    
    public void warn(String message) {
        writeLog("WARN", message);
    }
    
    public void error(String message) {
        writeLog("ERROR", message);
    }
    
    public void error(String message, Throwable e) {
        writeLog("ERROR", message + " - " + e.getMessage());
        // 可写入堆栈跟踪
    }
}

public class LoggerDemo {
    public static void main(String[] args) {
        Logger logger = Logger.getInstance();
        
        logger.info("应用程序启动");
        logger.warn("内存使用率较高");
        
        try {
            int result = 10 / 0;
        } catch (Exception e) {
            logger.error("计算错误", e);
        }
        
        logger.info("应用程序关闭");
    }
}

4.7.3 CSV 文件处理

import java.io.*;
import java.util.*;

public class CSVProcessor {
    // 读取 CSV
    public static List<Map<String, String>> readCSV(String filePath, String delimiter) 
            throws IOException {
        List<Map<String, String>> rows = new ArrayList<>();
        List<String> headers = null;
        
        try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
            String line;
            int lineNum = 0;
            
            while ((line = br.readLine()) != null) {
                lineNum++;
                String[] fields = line.split(delimiter);
                
                if (lineNum == 1) {
                    // 第一行是表头
                    headers = Arrays.asList(fields);
                } else {
                    // 数据行
                    Map<String, String> row = new LinkedHashMap<>();
                    for (int i = 0; i < fields.length && i < headers.size(); i++) {
                        row.put(headers.get(i), fields[i]);
                    }
                    rows.add(row);
                }
            }
        }
        
        return rows;
    }
    
    // 写入 CSV
    public static void writeCSV(String filePath, String delimiter, 
            List<String> headers, List<Map<String, String>> rows) throws IOException {
        
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(filePath))) {
            // 写入表头
            bw.write(String.join(delimiter, headers));
            bw.newLine();
            
            // 写入数据
            for (Map<String, String> row : rows) {
                List<String> values = new ArrayList<>();
                for (String header : headers) {
                    values.add(row.getOrDefault(header, ""));
                }
                bw.write(String.join(delimiter, values));
                bw.newLine();
            }
        }
    }
    
    public static void main(String[] args) throws IOException {
        String csvFile = "employees.csv";
        String delimiter = ",";
        
        // 创建示例数据
        List<String> headers = Arrays.asList("id", "name", "department", "salary");
        List<Map<String, String>> data = new ArrayList<>();
        
        Map<String, String> row1 = new LinkedHashMap<>();
        row1.put("id", "1");
        row1.put("name", "张三");
        row1.put("department", "研发部");
        row1.put("salary", "8000");
        data.add(row1);
        
        Map<String, String> row2 = new LinkedHashMap<>();
        row2.put("id", "2");
        row2.put("name", "李四");
        row2.put("department", "市场部");
        row2.put("salary", "6000");
        data.add(row2);
        
        // 写入 CSV
        writeCSV(csvFile, delimiter, headers, data);
        System.out.println("CSV 写入成功:" + csvFile);
        
        // 读取 CSV
        List<Map<String, String>> readData = readCSV(csvFile, delimiter);
        System.out.println("\nCSV 读取结果:");
        for (Map<String, String> row : readData) {
            System.out.println(row);
        }
    }
}

4.8 异常处理与最佳实践

public class IOExceptionBestPractices {
    public static void main(String[] args) {
        // 1. 使用 try-with-resources(自动关闭资源)
        try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
            String line = br.readLine();
            // 处理内容
        } catch (IOException e) {
            // 处理异常
            System.err.println("读取文件失败:" + e.getMessage());
        }
        // 不需要 finally 块手动关闭
        
        // 2. 分级异常处理
        try {
            readFile("important.txt");
        } catch (FileNotFoundException e) {
            System.err.println("文件未找到,请检查文件路径");
        } catch (IOException e) {
            System.err.println("读取文件发生IO错误");
        } catch (Exception e) {
            System.err.println("发生未知错误");
        }
        
        // 3. 使用具体异常类型
        // 不要捕获 Exception,尽量捕获具体异常
        
        // 4. 记录日志
        try {
            riskyOperation();
        } catch (IOException e) {
            // 记录日志(使用 Logger 或 System.err)
            System.err.println("操作失败:" + e);
            // 可以重新抛出或返回错误状态
        }
        
        // 5. 处理资源释放
        // 使用 try-with-resources 是最好的方式
        
        // 6. 避免空的 catch 块
        try {
            int result = 10 / 0;
        } catch (ArithmeticException e) {
            // 不要空捕获,至少要记录日志
            System.err.println("计算错误:" + e.getMessage());
        }
        
        // 7. 文件路径使用 Paths.get() 而非字符串拼接
        Path path = Paths.get(System.getProperty("user.home"), "Documents", "file.txt");
        System.out.println("安全路径:" + path);
    }
    
    private static void readFile(String path) throws IOException {
        try (FileInputStream fis = new FileInputStream(path)) {
            // 读取文件
        }
    }
    
    private static void riskyOperation() throws IOException {
        throw new IOException("模拟IO错误");
    }
}

第五章:IO 总结与选择指南

5.1 IO 选择决策树

需要处理什么类型的数据?
│
├── 二进制数据(图片、音频、视频、序列化对象)
│   └── 使用字节流(InputStream / OutputStream)
│       ├── 简单读写 → FileInputStream / FileOutputStream
│       ├── 性能优化 → BufferedInputStream / BufferedOutputStream
│       ├── 基本数据类型 → DataInputStream / DataOutputStream
│       └── 对象序列化 → ObjectInputStream / ObjectOutputStream
│
└── 文本数据
    └── 使用字符流(Reader / Writer)
        ├── 简单读写 → FileReader / FileWriter
        ├── 性能优化 + 按行读写 → BufferedReader / BufferedWriter
        ├── 格式化输出 → PrintWriter
        ├── 指定编码 → InputStreamReader / OutputStreamWriter
        └── 内存操作 → StringReader / StringWriter

5.2 性能优化建议

场景 建议
大文件读取 使用缓冲流(BufferedXXX),缓冲区大小 8192 或更大
小文件读写 使用 NIO Files.readAllBytes() / write()
频繁读写 使用 BufferedStream 减少系统调用
文本处理 使用 BufferedReader 按行读取
网络传输 使用 NIO 非阻塞 IO
内存映射文件 使用 MappedByteBuffer(大文件高效)

5.3 常见陷阱与注意

  1. 忘记关闭流 → 使用 try-with-resources
  2. 编码问题 → 始终指定字符编码
  3. 文件路径 → 使用 File.separatorPaths.get()
  4. 性能问题 → 使用缓冲流
  5. 跨平台 → 使用 System.lineSeparator() 而不是 \n
  6. 序列化版本 → 定义 serialVersionUID

结语

本章详细讲解了 Java IO 体系的方方面面,从基础的 File 类到字节流、字符流,再到高级的对象序列化和 NIO。IO 操作是每一个 Java 开发者必须掌握的技能,在实际项目中,无论是日志记录、配置文件读取、数据持久化还是网络通信,都离不开 IO。

本章要点回顾:

  • ✅ File 类的文件和目录操作
  • ✅ 字节流(InputStream / OutputStream)
  • ✅ 字符流(Reader / Writer)
  • ✅ 缓冲流提高性能
  • ✅ 对象序列化和反序列化
  • ✅ NIO 和 NIO.2 新特性
  • ✅ 实用案例和最佳实践

建议练习:

  1. 实现一个文件搜索工具(递归查找指定类型文件)
  2. 实现一个简单的文本编辑器(打开、编辑、保存)
  3. 实现一个文件批量重命名工具
  4. 使用序列化实现对象持久化存储

下一章预告:我们将进入 Java 多线程与并发编程,学习如何让程序同时执行多个任务,以及如何安全地处理共享资源。敬请期待!🚀

评论