IO流

发布于 2026-07-29 09:12 更新于 2026-07-29 09:12 2769 字 14 min read ... 访问量

文章系统介绍了 Java 中文件操作与 IO 流的核心内容,涵盖 File 类的路径处理、文件与目录创建、递归操作,以及字节流和字符流的使用方法。重点说明了 IO 流的分类、节点流与处理流的组合使用、常见操作如文件复制、文本读写、对象序列化,以及配置文件处理和 Apache POI 读写 Excel 文件的实践。强调了资源管理、编码规范、安全性及实际应用中的最佳实践。

IO 流

File 类

File 是文件或目录路径的抽象表示。创建 File 对象只是在内存中创建路径对象,不代表磁盘上的文件或目录一定存在。

构造器

File file1 = new File("d:/lesson/java2601/a.txt");
File file2 = new File("d:/lesson/java2601", "a.txt");
File parent = new File("d:/lesson/java2601");
File file3 = new File(parent, "a.txt");

在 Windows 路径字符串中可以使用转义后的反斜杠,也可以使用正斜杠。

创建文件和目录

创建文件

File file = new File("d:/lesson/java2601/test/a.txt");

try {
    boolean created = file.createNewFile();
    System.out.println(created ? "创建成功" : "文件已存在");
} catch (IOException e) {
    e.printStackTrace();
}

createNewFile() 不会自动创建不存在的父目录。

文件扩展名通常用于帮助操作系统和应用程序识别文件类型,但扩展名本身不会改变文件内容。

创建目录

mkdir() 只能在父目录已经存在时创建单级目录。

File directory = new File("d:/lesson/java2601/test/a");
directory.mkdir();

mkdirs() 可以同时创建缺失的多级父目录。

File directory = new File("d:/lesson/java2601/test/a/b/c");
directory.mkdirs();

创建临时文件

File directory = new File("d:/lesson/java2601/test");
try {
    File tempFile = File.createTempFile("temp", ".txt", directory);
    System.out.println(tempFile);
} catch (IOException e) {
    e.printStackTrace();
}

临时文件名前缀至少需要三个字符。

常用方法

方法作用
exists()判断路径是否存在
isFile()判断是否为普通文件
isDirectory()判断是否为目录
delete()删除文件或空目录
getName()获取文件或目录名称
getParentFile()获取父路径对应的 File 对象
getPath()获取构造对象时使用的路径形式
getAbsolutePath()获取绝对路径字符串
length()获取普通文件长度,单位为字节
listFiles()获取目录下的直接子项

listFiles() 在路径不是目录、发生 IO 错误或没有访问权限时可能返回 null,使用前应检查。

File directory = new File("d:/lesson/java2601");
File[] files = directory.listFiles();

if (files != null) {
    for (File file : files) {
        System.out.println(file);
    }
}

File.equals() 比较抽象路径名是否相等,不会自动把相对路径、符号链接或包含 .. 的路径全部规范化。需要比较实际规范路径时,可以使用 getCanonicalFile(),并处理 IOException

递归

递归是方法直接或间接调用自身。递归必须具有明确的终止条件,并确保每次调用都逐步接近终止条件。

阶乘

public static long factorial(int number) {
    if (number < 0) {
        throw new IllegalArgumentException("阶乘参数不能为负数");
    }
    if (number <= 1) {
        return 1;
    }
    return number * factorial(number - 1);
}

斐波那契数列

public static long fibonacci(int number) {
    if (number < 0) {
        throw new IllegalArgumentException("参数不能为负数");
    }
    if (number <= 1) {
        return number;
    }
    return fibonacci(number - 1) + fibonacci(number - 2);
}

这种直接递归会重复计算大量子问题,只适合演示递归。处理较大数据时应使用循环或缓存结果。

递归查找文件

public static void findPdfFiles(File path, List<File> result) {
    if (path == null || !path.exists()) {
        return;
    }

    if (path.isFile()) {
        if (path.getName().toLowerCase().endsWith(".pdf")) {
            result.add(path);
        }
        return;
    }

    File[] children = path.listFiles();
    if (children == null) {
        return;
    }

    for (File child : children) {
        findPdfFiles(child, result);
    }
}

递归删除目录

public static boolean deleteRecursively(File path) {
    if (path == null || !path.exists()) {
        return true;
    }

    if (path.isDirectory()) {
        File[] children = path.listFiles();
        if (children == null) {
            return false;
        }

        for (File child : children) {
            if (!deleteRecursively(child)) {
                return false;
            }
        }
    }

    return path.delete();
}

递归处理目录时还要注意访问权限、符号链接和过深目录可能导致的栈溢出。现代 Java 项目也可以使用 java.nio.file.Files 提供的文件树 API。

IO 流概述

IO 表示输入和输出。以程序内存为参照:

  • 输入流把外部数据读入程序。
  • 输出流把程序数据写到外部设备。

按数据单位分类

  • InputStreamOutputStream 体系按字节处理,适合图片、音频、视频和其他二进制数据。
  • ReaderWriter 体系按字符处理,适合文本数据。

字符流需要把字节按照字符编码解码为字符,或者把字符编码为字节。

按连接目标分类

  • 节点流直接连接文件、内存、网络等数据源或目标,例如 FileInputStream
  • 处理流包装其他流并增加缓冲、数据类型读写、对象序列化等功能,例如 BufferedInputStreamObjectOutputStream

处理流不一定都比节点流“更高级”或“更快”,应根据所需功能组合使用。

使用步骤

  1. 确定输入还是输出。

  2. 确定处理字节还是字符。

  3. 选择实际数据源对应的节点流。

  4. 根据需要增加缓冲、编码转换或对象处理流。

  5. 使用 try-with-resources 及时关闭资源。

FileInputStream 和 FileOutputStream

读取字节

FileInputStream 的常用 read() 方法如下:

  • read():读取一个字节,返回 0255,到达末尾返回 -1
  • read(byte[] buffer):最多读取数组长度个字节,返回实际读取数量。
  • read(byte[] buffer, int offset, int length):把数据读取到数组指定范围。

批量读取时,只能处理本次实际读取到的长度。

try (FileInputStream input = new FileInputStream(
        "d:/lesson/java2601/test/a.txt")) {

    byte[] buffer = new byte[1024];
    int length;

    while ((length = input.read(buffer)) != -1) {
        for (int i = 0; i < length; i++) {
            System.out.println(buffer[i]);
        }
    }
} catch (IOException e) {
    e.printStackTrace();
}

输出文本内容时,不应把任意字节逐个强制转换成字符,应使用字符流并指定正确编码。

写入字节

FileOutputStream 默认覆盖原文件,构造器第二个参数为 true 时表示追加。

try (FileOutputStream output = new FileOutputStream(
        "d:/lesson/java2601/test/a.txt", true)) {

    byte[] data = "ABCDEFG".getBytes(StandardCharsets.UTF_8);
    output.write(data);
} catch (IOException e) {
    e.printStackTrace();
}

复制文件

public static void copyFile(String source, String target) {
    try (FileInputStream input = new FileInputStream(source);
         FileOutputStream output = new FileOutputStream(target)) {

        byte[] buffer = new byte[8192];
        int length;

        while ((length = input.read(buffer)) != -1) {
            output.write(buffer, 0, length);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

复制二进制文件必须使用字节流。

BufferedInputStream 和 BufferedOutputStream

缓冲字节流在内存中维护缓冲区,减少底层读写调用次数。

public static void copyFileWithBuffer(String source, String target) {
    try (BufferedInputStream input = new BufferedInputStream(
            new FileInputStream(source));
         BufferedOutputStream output = new BufferedOutputStream(
            new FileOutputStream(target))) {

        byte[] buffer = new byte[8192];
        int length;

        while ((length = input.read(buffer)) != -1) {
            output.write(buffer, 0, length);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

关闭输出流时会先刷新缓冲区。需要在流保持打开的情况下立刻把数据提交到底层目标时,可以调用 flush()

FileReader 和 FileWriter

FileReaderFileWriter 用于读写文本,但传统构造器通常使用平台默认字符集。文件编码固定时,建议使用 InputStreamReaderOutputStreamWriter 显式指定字符集。

try (FileReader reader = new FileReader(
        "d:/lesson/java2601/test/a.txt")) {

    char[] buffer = new char[1024];
    int length;

    while ((length = reader.read(buffer)) != -1) {
        System.out.print(new String(buffer, 0, length));
    }
} catch (IOException e) {
    e.printStackTrace();
}

FileReader 本身不是 BufferedReader,不能把它描述为天然具有行缓冲功能。

try (FileWriter writer = new FileWriter(
        "d:/lesson/java2601/test/a.txt")) {
    writer.write("1234567890");
} catch (IOException e) {
    e.printStackTrace();
}

BufferedReader 和 BufferedWriter

BufferedReader 提供 readLine(),可以按行读取文本。返回的字符串不包含行结束符,到达文件末尾时返回 null

try (BufferedReader reader = new BufferedReader(
        new FileReader("d:/lesson/java2601/test/a.txt"))) {

    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}

BufferedWriter 提供 newLine() 写入平台对应的行分隔符。

try (BufferedWriter writer = new BufferedWriter(
        new FileWriter("d:/lesson/java2601/test/a.txt"))) {

    writer.write("第一行");
    writer.newLine();
    writer.write("第二行");
} catch (IOException e) {
    e.printStackTrace();
}

InputStreamReader 和 OutputStreamWriter

这两个转换流连接字节流与字符流,并负责字符编码转换。

try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(
                new FileInputStream("d:/lesson/java2601/test/a.txt"),
                StandardCharsets.UTF_8))) {

    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}
try (BufferedWriter writer = new BufferedWriter(
        new OutputStreamWriter(
                new FileOutputStream("d:/lesson/java2601/test/a.txt"),
                StandardCharsets.UTF_8))) {

    writer.write("使用 UTF-8 写入文本");
} catch (IOException e) {
    e.printStackTrace();
}

DataInputStream 和 DataOutputStream

数据流可以按照 Java 基本类型的二进制格式读写数据。

try (DataOutputStream output = new DataOutputStream(
        new FileOutputStream("d:/lesson/java2601/test/data.bin"))) {

    output.writeInt(1024);
    output.writeDouble(100.0);
    output.writeUTF("Java");
} catch (IOException e) {
    e.printStackTrace();
}

读取时必须使用相同的顺序和类型,否则会读到错误结果或抛出异常。

try (DataInputStream input = new DataInputStream(
        new FileInputStream("d:/lesson/java2601/test/data.bin"))) {

    int number = input.readInt();
    double price = input.readDouble();
    String text = input.readUTF();

    System.out.println(number);
    System.out.println(price);
    System.out.println(text);
} catch (IOException e) {
    e.printStackTrace();
}

对象序列化与反序列化

序列化把对象及其可序列化对象图转换为字节序列,反序列化根据字节序列恢复对象。

类需要实现 Serializable 标记接口。

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    private transient String password;
}

transient 字段不会参与默认序列化。serialVersionUID 用于判断序列化数据与当前类版本是否兼容。

序列化到字节数组

public static byte[] serialize(Object object) throws IOException {
    try (ByteArrayOutputStream bytes = new ByteArrayOutputStream();
         ObjectOutputStream output = new ObjectOutputStream(bytes)) {

        output.writeObject(object);
        return bytes.toByteArray();
    }
}

从字节数组反序列化

public static Object deserialize(byte[] data)
        throws IOException, ClassNotFoundException {

    try (ByteArrayInputStream bytes = new ByteArrayInputStream(data);
         ObjectInputStream input = new ObjectInputStream(bytes)) {

        return input.readObject();
    }
}

序列化数据不要求 JDK 小版本完全一致,但类名、字段结构、serialVersionUID 和相关类必须满足兼容条件。

反序列化不可信数据可能触发危险对象行为,真实项目中不得直接反序列化来源不可信的字节流。

PrintWriter

PrintWriter 提供 print()println()printf() 等便捷文本输出方法。

try (PrintWriter writer = new PrintWriter(
        "d:/lesson/java2601/test/a.txt",
        StandardCharsets.UTF_8.name())) {

    writer.println("你好");
    writer.println("中午吃什么");
} catch (FileNotFoundException | UnsupportedEncodingException e) {
    e.printStackTrace();
}

PrintWriter 的部分写入错误不会直接以 IOException 抛出,可以在需要时调用 checkError() 检查状态。关闭流会自动刷新缓冲区。

Properties 类

Properties 用于保存字符串键值配置,继承自 Hashtable<Object, Object>。读取配置时应优先使用 getProperty(),写入时使用 setProperty()

配置文件适合保存可由部署环境调整、但不应硬编码在程序中的数据。

常见配置格式

  • XML 具有明确结构,但编写和解析相对复杂。
  • Properties 使用简单的键值形式,层级表达能力有限。
  • YAML 具有较强的层级表达能力,常用于 Spring Boot 等框架。

读取配置

p1.name=Tom
p1.age=20
p2.name=Jerry
p2.age=22
Properties properties = new Properties();

try (Reader reader = new InputStreamReader(
        new FileInputStream("src/myinfo.properties"),
        StandardCharsets.UTF_8)) {

    properties.load(reader);

    Person person1 = new Person();
    person1.name = properties.getProperty("p1.name");
    person1.age = Integer.parseInt(properties.getProperty("p1.age"));

    Person person2 = new Person();
    person2.name = properties.getProperty("p2.name");
    person2.age = Integer.parseInt(properties.getProperty("p2.age"));
} catch (IOException e) {
    e.printStackTrace();
}

使用 Reader 可以显式选择字符集。Java 8 中直接使用 InputStream 读取 Properties 时,传统格式按 ISO-8859-1 规则处理,非 ASCII 字符通常需要 Unicode 转义。

写入配置

properties.setProperty("p3.name", "Tom");
properties.setProperty("p3.age", "22");

try (Writer writer = new OutputStreamWriter(
        new FileOutputStream("src/myinfo.properties"),
        StandardCharsets.UTF_8)) {

    properties.store(writer, "第三个人的信息");
} catch (IOException e) {
    e.printStackTrace();
}
image-001
image-001

Apache POI

Apache POI 是 Apache 软件基金会的 Java 项目,可以读写 Microsoft Office 格式,例如 Excel 和 Word。

一个典型练习是读取学生成绩工作簿中的数据,计算平均成绩,再把结果写入另一个工作表。处理 Excel 时需要根据文件格式选择对应模型:

  • .xls 通常使用 HSSF。
  • .xlsx 通常使用 XSSF。

使用 POI 读写工作簿、输入流和输出流时,也应通过 try-with-resources 正确关闭资源,并注意单元格类型、空单元格和公式单元格的处理。

喜欢的话,留下你的评论吧~

... 访问量
© 2026 跨越星轨的客 @Hoshiumi
Powered by theme astro-koharu · Inspired by Shoka