Java IO 流与配置文件

本文系统讲解 Java IO 流与 Properties 配置文件,覆盖字节流、字符流、文件读写、文件复制、异常处理、资源关闭和配置文件读取保存等内容。文章通过案例演示程序如何与外部文件交换数据,帮助读者理解 Java 文件操作的核心流程。

第一章 IO 流的介绍和分类

1.1 什么是 IO 流

IO 是 Input 和 Output 的缩写:

  • Input:输入,把外部数据读入程序。
  • Output:输出,把程序数据写到外部。

流可以理解成数据管道:

Text
文件 ----输入流----> Java 程序
Java 程序 ----输出流----> 文件

1.2 按数据单位分类

字节流以字节为单位,能处理所有文件:

  • InputStream
  • OutputStream
  • FileInputStream
  • FileOutputStream

字符流以字符为单位,专门处理纯文本:

  • Reader
  • Writer
  • FileReader
  • FileWriter

图片、视频、压缩包用字节流;普通文本优先用字符流。

1.3 按流向分类

判断流向时,以 Java 程序为参照:

分类说明
输入流外部数据进入程序
输出流程序数据写到外部

1.4 按功能分类

分类说明示例
节点流直接连接数据源FileInputStreamFileReader
处理流包装其他流增强功能BufferedInputStreamBufferedReader

第二章 FileOutputStream 写出数据

2.1 基本使用

FileOutputStream 用来把字节写入文件。

Java
import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamDemo {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("a.txt");
        fos.write(97);
        fos.close();
    }
}

97 对应字符 a

2.2 写字节数组

Java
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class WriteBytesDemo {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("a.txt");

        byte[] data = "你好,Java".getBytes(StandardCharsets.UTF_8);
        fos.write(data);

        fos.close();
    }
}

2.3 写数组的一部分

Java
import java.io.FileOutputStream;
import java.io.IOException;

public class WritePartBytesDemo {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("a.txt");

        byte[] data = {97, 98, 99, 100};
        fos.write(data, 1, 2); // 写 b 和 c

        fos.close();
    }
}

2.4 覆盖写和追加写

默认覆盖原文件:

Java
FileOutputStream fos = new FileOutputStream("a.txt");

追加写:

Java
FileOutputStream fos = new FileOutputStream("a.txt", true);
Java
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class AppendDemo {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("log.txt", true);
        fos.write(("登录成功" + System.lineSeparator()).getBytes(StandardCharsets.UTF_8));
        fos.close();
    }
}

第三章 IO 流标准异常处理代码

3.1 finally 写法

真实开发中,流必须可靠关闭。JDK7 前常用 finally

Java
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class FinallyCloseDemo {
    public static void main(String[] args) {
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("a.txt");
            fos.write("hello".getBytes(StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3.2 try-with-resources

JDK7 开始推荐这种写法。资源实现了 AutoCloseable,就能自动关闭。

Java
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class TryWithResourcesDemo {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("a.txt")) {
            fos.write("hello".getBytes(StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

多个资源会按创建顺序的反方向关闭:

Java
try (FileInputStream fis = new FileInputStream("source.txt");
     FileOutputStream fos = new FileOutputStream("target.txt")) {
    // 先使用 fis 和 fos,结束时先关 fos,再关 fis。
}

3.3 异常处理建议

  • 学习阶段可以使用 e.printStackTrace()
  • 项目中通常使用日志框架记录异常。
  • 不要 catch 后什么都不做。
  • 不要无脑写 catch (Exception e)
  • IO 资源优先使用 try-with-resources

第四章 FileInputStream 读取数据

4.1 一次读取一个字节

Java
import java.io.FileInputStream;
import java.io.IOException;

public class ReadOneByteDemo {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("a.txt");

        int b;
        while ((b = fis.read()) != -1) {
            System.out.print((char) b);
        }

        fis.close();
    }
}

read() 返回 int,读不到数据时返回 -1

4.2 一次读取一个字节数组

Java
import java.io.FileInputStream;
import java.io.IOException;

public class ReadBufferDemo {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("a.txt");

        byte[] buffer = new byte[1024];
        int len;
        while ((len = fis.read(buffer)) != -1) {
            String text = new String(buffer, 0, len);
            System.out.print(text);
        }

        fis.close();
    }
}

最后一次读取通常不会填满数组,所以必须用 new String(buffer, 0, len)

4.3 中文乱码原因

中文通常由多个字节组成。字节流读取文本时,如果把一个中文字符拆开处理,就可能乱码。处理纯文本时,优先使用字符流,并明确编码。


第五章 字节流案例:文件拷贝

5.1 文件拷贝的本质

文件拷贝就是一边读,一边写:

Text
源文件 ----FileInputStream----> 程序内存 ----FileOutputStream----> 目标文件

5.2 基础版文件拷贝

Java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyFileDemo {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("source.jpg");
             FileOutputStream fos = new FileOutputStream("target.jpg")) {

            byte[] buffer = new byte[1024 * 8];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这个版本可以复制图片、视频、文本、压缩包等任意文件。

5.3 封装文件复制方法

Java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileCopyUtils {
    /**
     * 使用字节流复制文件。
     *
     * @param sourcePath 源文件路径
     * @param targetPath 目标文件路径
     * @throws IOException 文件不存在、无权限或磁盘异常时抛出
     */
    public static void copyFile(String sourcePath, String targetPath) throws IOException {
        try (FileInputStream fis = new FileInputStream(sourcePath);
             FileOutputStream fos = new FileOutputStream(targetPath)) {

            byte[] buffer = new byte[1024 * 8];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                // 关键业务逻辑:最后一次读取可能不足整个缓冲区,只能写入实际读取到的 len 个字节。
                fos.write(buffer, 0, len);
            }
        }
    }
}

缓冲区常用 8192 字节,也就是 8KB。它能减少磁盘读写次数,比一个字节一个字节复制效率高很多。


第六章 FileReader 读取数据

6.1 FileReader 基本使用

FileReader 是字符输入流,适合读取文本。

Java
import java.io.FileReader;
import java.io.IOException;

public class FileReaderDemo {
    public static void main(String[] args) throws IOException {
        FileReader reader = new FileReader("a.txt");

        int ch;
        while ((ch = reader.read()) != -1) {
            System.out.print((char) ch);
        }

        reader.close();
    }
}

6.2 字符数组读取

Java
import java.io.FileReader;
import java.io.IOException;

public class FileReaderBufferDemo {
    public static void main(String[] args) throws IOException {
        FileReader reader = new FileReader("a.txt");

        char[] buffer = new char[1024];
        int len;
        while ((len = reader.read(buffer)) != -1) {
            System.out.print(new String(buffer, 0, len));
        }

        reader.close();
    }
}

6.3 指定编码读取

传统 FileReader 使用系统默认编码。如果文件编码和系统默认编码不一致,可能乱码。JDK11 开始可以指定编码:

Java
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class FileReaderCharsetDemo {
    public static void main(String[] args) throws IOException {
        try (FileReader reader = new FileReader("a.txt", StandardCharsets.UTF_8)) {
            char[] buffer = new char[1024];
            int len;
            while ((len = reader.read(buffer)) != -1) {
                System.out.print(new String(buffer, 0, len));
            }
        }
    }
}

低版本 JDK 可以使用 InputStreamReader 指定编码。


第七章 FileWriter 写出数据

7.1 FileWriter 基本使用

FileWriter 是字符输出流,适合写文本。

Java
import java.io.FileWriter;
import java.io.IOException;

public class FileWriterDemo {
    public static void main(String[] args) throws IOException {
        FileWriter writer = new FileWriter("a.txt");

        writer.write("你好,Java");
        writer.write(System.lineSeparator());
        writer.write("字符流适合写文本");

        writer.close();
    }
}

7.2 写字符、字符串、字符数组

Java
import java.io.FileWriter;
import java.io.IOException;

public class FileWriterMethodsDemo {
    public static void main(String[] args) throws IOException {
        FileWriter writer = new FileWriter("a.txt");

        writer.write('A');
        writer.write("BCDE");

        char[] chars = {'你', '好', 'J', 'a', 'v', 'a'};
        writer.write(chars);
        writer.write(chars, 0, 2);

        writer.close();
    }
}

7.3 追加写、flush 和 close

Java
import java.io.FileWriter;
import java.io.IOException;

public class FileWriterAppendDemo {
    public static void main(String[] args) throws IOException {
        FileWriter writer = new FileWriter("log.txt", true);

        writer.write("用户登录成功");
        writer.write(System.lineSeparator());
        writer.flush(); // 刷新后还能继续写

        writer.write("继续记录日志");
        writer.close(); // 关闭前会自动刷新,关闭后不能继续写
    }
}

推荐写法:

Java
import java.io.FileWriter;
import java.io.IOException;

public class FileWriterTryDemo {
    public static void main(String[] args) {
        try (FileWriter writer = new FileWriter("a.txt")) {
            writer.write("自动关闭资源");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

第八章 Properties 集合

8.1 Properties 是什么

Properties 常用于读取和保存 .properties 配置文件。配置文件通常是字符串键值对。

Properties
username=root
password=123456
url=jdbc:mysql://localhost:3306/test
driver=com.mysql.cj.jdbc.Driver

8.2 基本操作

Java
import java.util.Properties;

public class PropertiesBasicDemo {
    public static void main(String[] args) {
        Properties properties = new Properties();

        properties.setProperty("username", "root");
        properties.setProperty("password", "123456");

        System.out.println(properties.getProperty("username"));
        System.out.println(properties.getProperty("missing", "默认值"));
    }
}

8.3 读取配置文件

Java
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Properties;

public class PropertiesLoadDemo {
    public static void main(String[] args) {
        Properties properties = new Properties();

        try (FileReader reader = new FileReader("db.properties", StandardCharsets.UTF_8)) {
            properties.load(reader);
            System.out.println(properties.getProperty("url"));
            System.out.println(properties.getProperty("username"));
            System.out.println(properties.getProperty("password"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

8.4 保存配置文件

Java
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Properties;

public class PropertiesStoreDemo {
    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.setProperty("theme", "dark");
        properties.setProperty("language", "zh-CN");

        try (FileWriter writer = new FileWriter("app.properties", StandardCharsets.UTF_8)) {
            properties.store(writer, "application settings");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

8.5 配置读取工具

Java
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Properties;

public class ConfigUtils {
    /**
     * 从指定路径加载 properties 配置文件。
     *
     * @param path 配置文件路径
     * @return 加载完成的 Properties 对象
     * @throws IOException 文件不存在、编码错误或读取失败时抛出
     */
    public static Properties loadProperties(String path) throws IOException {
        Properties properties = new Properties();
        try (FileReader reader = new FileReader(path, StandardCharsets.UTF_8)) {
            properties.load(reader);
        }
        return properties;
    }
}

第九章 对象的序列化与反序列化(含现代企业级方案)

9.1 什么是序列化与反序列化

  • 序列化 (Serialization):把 Java 对象转换为特定格式(如字节序列、JSON 字符串等)的过程,常用于将对象持久化保存到磁盘,或在网络中传输对象。
  • 反序列化 (Deserialization):把特定格式的数据恢复为 Java 对象的过程。

9.2 现代项目主流方案:JSON 序列化(重点)

在现代企业级 Java 开发(尤其是 Spring Boot、微服务架构)中,JSON 序列化是最常用、最核心的方案。它的优点是跨语言、可读性好、体积小。 主流的 JSON 序列化框架包括 Jackson(Spring Boot 默认集成)、Fastjson2(阿里开源,性能极高)和 Gson(Google 出品)。

9.2.1 使用 Jackson 进行序列化与反序列化演示

引入 Jackson 依赖(Web 项目通常已自带):

XML
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version>
</dependency>

代码演示:

Java
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonSerializeDemo {
    public static void main(String[] args) throws Exception {
        User user = new User("admin", "123456");
        
        // 核心类:ObjectMapper
        ObjectMapper mapper = new ObjectMapper();

        // 1. 序列化:Java 对象 -> JSON 字符串
        String jsonString = mapper.writeValueAsString(user);
        System.out.println("JSON序列化结果:" + jsonString);
        // 输出: {"username":"admin","password":"..."}

        // 2. 反序列化:JSON 字符串 -> Java 对象
        User parsedUser = mapper.readValue(jsonString, User.class);
        System.out.println("JSON反序列化对象:" + parsedUser.getUsername());
    }
}

9.3 Java 原生序列化(简单了解)

Java 自身提供了一套基于 java.io.SerializableObjectOutputStream 的字节序列化机制。 ⚠️ 注意:原生序列化由于存在安全漏洞、无法跨语言调用、且序列化后体积庞大,在现代企业级开发中已经极少使用,仅做基础知识了解即可。

9.3.1 Serializable 接口与关键字

要想类的对象可以被原生序列化,该类必须实现 java.io.Serializable 标记接口。

Java
import java.io.Serializable;

public class User implements Serializable {
    // 推荐显式声明 serialVersionUID,用于版本控制,避免修改类后反序列化失败
    private static final long serialVersionUID = 1L;

    private String username;
    
    // transient 修饰的成员变量不会被原生序列化
    private transient String password;

    public User() {}
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
    public String getUsername() { return username; }
}

9.3.2 原生流序列化代码演示

Java
import java.io.*;

public class NativeSerializeDemo {
    public static void main(String[] args) {
        // 1. 序列化 (ObjectOutputStream)
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
            oos.writeObject(new User("admin", "123456"));
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 2. 反序列化 (ObjectInputStream)
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
            User user = (User) ois.readObject();
            // 由于 password 被 transient 修饰,反序列化后值为 null
            System.out.println("反序列化完成");
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

总结

IO 流解决的是程序和文件之间的数据交换问题。学习时不要只记类名,更要理解每种流适合处理什么数据:

  • 字节流适合处理所有类型的文件,尤其是图片、视频、压缩包等二进制文件。
  • 字符流适合处理纯文本文件。
  • try-with-resources 能自动关闭资源,是更推荐的异常处理方式。
  • 文件复制的本质是“边读边写”。
  • Properties 让配置从代码中分离出来,适合保存简单键值对配置。
  • 序列化方面,现代开发中几乎都采用 JSON 序列化(Jackson/Fastjson 等),Java 原生 Serializable 仅做基础了解即可。

掌握这些内容后,你就能独立完成文件读写、文件复制、文本处理和配置文件读取等常见任务。