Skip to content

第15章:I/O 与文件

Vol 1 的最后一章。学完它,你就能用 Java 读写文件——程序真正能和"外面"打交道了。

📌 代码说明:标注"可复制运行"的代码块包含完整的 main 方法。没标注的是概念示意片段。

一个故事开头

你的奶茶店运行了几个月,积累了很多订单数据。但你发现每次关掉程序,数据就全丢了——因为所有数据都存在内存里(变量、集合)。

内存是临时存储。文件是持久存储。

java
// 内存——关了就没了
List<String> orders = new ArrayList<>();
orders.add("小明:波波奶茶×2");

// 文件——下次启动还在
// menu.txt 里写了:
// 波波奶茶 15.0
// 柠檬茶 12.0

I/O(Input/Output,输入/输出)就是程序和外部世界(文件、网络、键盘)交换数据的方式。这一章只讲文件 I/O——网络 I/O 是 Vol 4 的内容。


1. 文件系统操作——File

java.io.File 类代表文件和目录路径。

java
// 可复制运行,保存为 FileDemo.java
import java.io.File;

public class FileDemo {
    public static void main(String[] args) throws Exception {
        // 创建一个 File 对象(只是"指向"这个路径,不保证文件存在)
        File file = new File("menu.txt");
        
        System.out.println("文件名: " + file.getName());
        System.out.println("路径: " + file.getPath());
        System.out.println("绝对路径: " + file.getAbsolutePath());
        System.out.println("是否存在: " + file.exists());
        System.out.println("是文件吗: " + file.isFile());
        System.out.println("是目录吗: " + file.isDirectory());
        System.out.println("大小: " + file.length() + " 字节");
        
        // 创建新文件
        if (!file.exists()) {
            boolean created = file.createNewFile();  // 返回 true 如果创建成功
            System.out.println("创建文件: " + created);
        }
        
        // 目录操作
        File dir = new File("data/orders/2026");
        System.out.println("目录是否存在: " + dir.exists());
        dir.mkdirs();  // 创建多级目录——mkdirs() 带 s,不是 mkdir()
        System.out.println("创建目录后: " + dir.exists());
    }
}

常用 File 方法

java
File f = new File("test.txt");
f.exists()        // 是否存在
f.isFile()        // 是文件吗
f.isDirectory()   // 是目录吗
f.length()        // 文件大小(字节)
f.getName()       // 文件名
f.getPath()       // 路径
f.getAbsolutePath() // 绝对路径
f.createNewFile() // 创建新文件
f.mkdir()         // 创建目录(单级)
f.mkdirs()        // 创建目录(多级)
f.delete()        // 删除文件或空目录
f.listFiles()     // 列出目录下的文件

2. 读文件

方法1:FileReader + BufferedReader(逐行读)

java
// 先准备一个文件
// 在项目目录下创建 menu.txt,内容:
// 波波奶茶,15.0
// 柠檬茶,12.0
// 杨枝甘露,18.0

// 可复制运行,保存为 ReadFileDemo.java
import java.io.*;

public class ReadFileDemo {
    public static void main(String[] args) {
        // try-with-resources——自动关闭文件(第11章讲过)
        try (BufferedReader reader = new BufferedReader(new FileReader("menu.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                // readLine() 读到文件末尾返回 null
                System.out.println(line);
            }
        } catch (FileNotFoundException e) {
            System.out.println("文件不存在:" + e.getMessage());
        } catch (IOException e) {
            System.out.println("读取出错:" + e.getMessage());
        }
    }
}

方法2:Files.readAllLines(Java 8+,最简单)

java
// 可复制运行,保存为 ReadAllDemo.java
import java.nio.file.*;
import java.io.*;
import java.util.*;

public class ReadAllDemo {
    public static void main(String[] args) throws IOException {
        // 一行搞定——把整个文件读成 List<String>
        List<String> lines = Files.readAllLines(Paths.get("menu.txt"));
        
        for (String line : lines) {
            System.out.println(line);
        }
    }
}

解析一行数据

java
// 概念示意
String line = "波波奶茶,15.0";
String[] parts = line.split(",");  // ["波波奶茶", "15.0"]
String name = parts[0];
double price = Double.parseDouble(parts[1]);

3. 写文件

方法1:FileWriter + BufferedWriter

java
// 可复制运行,保存为 WriteFileDemo.java
import java.io.*;

record OrderItem(String drink, int quantity, double total) {}

public class WriteFileDemo {
    public static void main(String[] args) {
        // 准备订单数据
        OrderItem[] orders = {
            new OrderItem("波波奶茶", 2, 30.0),
            new OrderItem("柠檬茶", 1, 12.0),
            new OrderItem("杨枝甘露", 3, 54.0)
        };
        
        // 写文件
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("orders.txt"))) {
            writer.write("=== 团子奶茶店 ===");
            writer.newLine();
            
            for (OrderItem order : orders) {
                String line = order.drink() + "," + order.quantity() + "," + order.total();
                writer.write(line);
                writer.newLine();
            }
            
            System.out.println("订单已写入 orders.txt");
        } catch (IOException e) {
            System.out.println("写入出错:" + e.getMessage());
        }
    }
}

FileWriter 的第二个参数——追加模式

java
// FileWriter("orders.txt", true)   → 追加到文件末尾(不覆盖)
// FileWriter("orders.txt", false)  → 覆盖文件(默认)
try (BufferedWriter writer = new BufferedWriter(new FileWriter("orders.txt", true))) {
    writer.write("新订单...");
    writer.newLine();
}

方法2:Files.write(Java 8+)

java
import java.nio.file.*;
import java.util.*;

List<String> lines = Arrays.asList("波波奶茶,15.0", "柠檬茶,12.0");
Files.write(Paths.get("new_menu.txt"), lines);

4. 字节流 vs 字符流

到目前为止你看到的是字符流——专门用来读写文本文件的。还有字节流——用来读写任何文件(图片、音频、视频)。

java
// 字节流——读图片
// 概念示意
try (FileInputStream fis = new FileInputStream("logo.png");
     FileOutputStream fos = new FileOutputStream("copy.png")) {
    
    byte[] buffer = new byte[1024];  // 一次读 1KB
    int bytesRead;
    while ((bytesRead = fis.read(buffer)) != -1) {
        fos.write(buffer, 0, bytesRead);
    }
}
种类字符流字节流
处理文本(.txt, .csv, .json)任意文件(图片、音频、视频)
输入FileReaderBufferedReaderFileInputStream
输出FileWriterBufferedWriterFileOutputStream
日常使用读写配置、日志、数据文件复制文件、处理图片等

5. 完整例子:订单管理系统

java
// 可复制运行,保存为 OrderManager.java
import java.io.*;
import java.nio.file.*;
import java.time.*;
import java.util.*;

record Drink(String name, double price) {
    public static Drink fromLine(String line) {
        String[] parts = line.split(",");
        return new Drink(parts[0], Double.parseDouble(parts[1]));
    }
    
    public String toLine() {
        return name + "," + price;
    }
}

public class OrderManager {
    private static final String MENU_FILE = "menu.txt";
    private static final String ORDER_FILE = "orders.log";
    
    // 加载菜单
    public static List<Drink> loadMenu() throws IOException {
        List<Drink> menu = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(MENU_FILE))) {
            String line;
            while ((line = reader.readLine()) != null) {
                if (!line.trim().isEmpty()) {
                    menu.add(Drink.fromLine(line));
                }
            }
        }
        return menu;
    }
    
    // 保存菜单
    public static void saveMenu(List<Drink> menu) throws IOException {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(MENU_FILE))) {
            for (Drink drink : menu) {
                writer.write(drink.toLine());
                writer.newLine();
            }
        }
    }
    
    // 记录订单
    public static void logOrder(String customer, String drink, int quantity) throws IOException {
        String timestamp = LocalDateTime.now().toString();
        String line = timestamp + "," + customer + "," + drink + "," + quantity;
        Files.write(Paths.get(ORDER_FILE), 
            Collections.singletonList(line),
            StandardOpenOption.CREATE, StandardOpenOption.APPEND);
    }
    
    // 查看所有订单
    public static List<String> loadOrders() throws IOException {
        if (!Files.exists(Paths.get(ORDER_FILE))) {
            return new ArrayList<>();
        }
        return Files.readAllLines(Paths.get(ORDER_FILE));
    }
    
    public static void main(String[] args) {
        try {
            // 创建菜单
            List<Drink> menu = new ArrayList<>();
            menu.add(new Drink("波波奶茶", 15.0));
            menu.add(new Drink("柠檬茶", 12.0));
            menu.add(new Drink("杨枝甘露", 18.0));
            saveMenu(menu);
            System.out.println("菜单已保存");
            
            // 读取菜单
            List<Drink> loaded = loadMenu();
            System.out.println("\n今日菜单:");
            for (Drink d : loaded) {
                System.out.println("  " + d.name() + " — " + d.price() + "元");
            }
            
            // 记录几笔订单
            logOrder("小明", "波波奶茶", 2);
            logOrder("小红", "柠檬茶", 1);
            logOrder("小张", "杨枝甘露", 3);
            System.out.println("\n订单已记录");
            
            // 查看所有订单
            System.out.println("\n历史订单:");
            for (String order : loadOrders()) {
                System.out.println("  " + order);
            }
            
        } catch (IOException e) {
            System.err.println("IO错误:" + e.getMessage());
        }
    }
}

本章小结

  1. File——操作文件系统(存不存在、路径、创建、删除)
  2. 字符流——读写文本文件:FileReader / FileWriter + BufferedReader / BufferedWriter
  3. 字节流——读写任意文件:FileInputStream / FileOutputStream
  4. try-with-resources——自动关闭文件资源(第11章的复习)
  5. Java NIO——Files.readAllLines()Files.write(),比传统 IO 更简洁
  6. 追加模式——new FileWriter("file", true) 不覆盖原文件

✅ 验收标准

完成本章后,你应该能:

  • [ ] 用 File 类操作文件和目录(创建、删除、检查)
  • [ ] 用字符流读写文本文件
  • [ ] 用字节流读写二进制文件
  • [ ] 用 try-with-resources 自动关闭资源
  • [ ] 用 Java NIO 的 Files 工具类简化操作

📌 常见卡点

  • 文件流用完后忘记关闭——趁早用 try-with-resources
  • 字符编码问题——默认编码和平台相关,指定 UTF-8
  • 绝对路径 vs 相对路径——了解工作目录概念
  • 字节流的 read() 返回 int 而不是 byte

🔜 现在不需要理解

  • RandomAccessFile——随机访问文件的高级场景
  • FileChannel 和内存映射文件
  • NIO.2 的 WatchService 文件监听
  • 对象序列化 ObjectOutputStream


🧪 练习

1. 写文件:创建一个 students.txt,写入三个学生的姓名和分数(格式:小明,85),每行一个。

2. 读文件:读取 students.txt,计算平均分并输出。

3. 追加数据:在 students.txt 末尾追加一个学生的数据,然后重新读取验证。


Vol 1 完结 🎉

恭喜你学完了 Vol 1 的全部 15 章。从"Hello World"到"文件读写",你已经掌握了 Java 编程的完整基础。

接下来:

用 ❤️ 构建 | Software Systems Atlas