Java中的IO流详细笔记

目录

1. 引言

2. 基础概念

2.1 什么是IO流?

3. 主要特性

3.1 方向性

3.2 装饰者模式

3.3 缓冲机制

3.4 自动关闭资源

3.5 字符编码处理

4. 详细用法

4.1 文件字节流 - 处理二进制数据的基础流

常用API

4.1.1 FileInputStream

4.1.1 FileInputStream

新手常见问题

4.1.2 FileOutputStream

字节流使用细节

4.2 文件字符流 - 专门处理文本数据的基础流

常用API

4.2.1 FileReader

4.2.1 FileReader

字符流编码注意事项

4.2.2 FileWriter

4.3 缓冲流 - 大幅提升IO性能的包装流

常用API

4.3.1 BufferedInputStream/BufferedOutputStream

4.3.1 BufferedInputStream/BufferedOutputStream

4.3.2 BufferedReader/BufferedWriter

4.4 数据流 - 读写基本数据类型的便捷流

常用API

使用特点

4.5 对象流 - 实现Java对象序列化和反序列化

4.5.1 序列化和反序列化基础

4.5.2 序列化的使用条件

4.5.3 序列化版本号(serialVersionUID)

4.5.4 常用API

4.5.5 对象序列化示例

4.5.6 序列化的核心细节

4.5.7 序列化常见问题与解决方案

4.6 转换流 - 连接字节流和字符流的桥梁

4.6.1 转换流的作用

常用API

4.6.2 InputStreamReader使用示例

4.6.3 OutputStreamWriter使用示例

4.6.4 新手常见问题与细节

实际案例:跨平台文件编码转换器

4.7 打印流 - 方便输出各种数据类型的工具流

4.7.1 打印流的特点

4.7.2 常用API - PrintStream

4.7.3 常用API - PrintWriter

4.7.4 PrintStream使用示例

4.7.5 PrintWriter使用示例

4.7.6 System.out与System.err

4.7.7 打印流与其他流的区别

4.7.8 自动刷新机制

打印流的特点

常用的格式化打印

5. 实际应用场景

5.1 文件复制

5.2 配置文件读写 - 处理应用程序配置

5.2.1 Properties文件处理

5.2.2 Properties文件常见问题

5.3 数据序列化应用 - 对象持久化与传输

5.3.1 对象序列化保存示例

5.3.2 对象反序列化读取示例

5.3.3 序列化版本控制实例

5.3.4 序列化在网络传输中的应用

5.3.5 使用序列化实现对象深拷贝

5.4 实际应用案例 - 日志记录器

6. 注意事项和最佳实践

6.1 资源管理

6.2 性能优化

6.3 异常处理

6.4 字符编码

6.5 安全最佳实践

6.6 资源关闭顺序

6.7 flush与close的区别

7. 与相关技术的对比

7.1 Java IO流与NIO对比

7.2 Java IO与Apache Commons IO对比

7.3 其他常用工具库对比

Hutool工具包示例

Guava工具包示例

8. 面试常见问题

Q1: Java中的IO流分为哪几种?主要特点是什么?

Q2: 为什么要使用缓冲流?它是如何提高性能的?

Q3: 序列化和反序列化是什么?如何实现?有哪些注意事项?

Q4: Java中如何实现文件复制?请给出不同性能级别的实现。


1. 引言

当你尝试从键盘读取用户输入、将数据保存到文件、或是通过网络传输信息时,你实际上都在处理数据的"流动"。想象一下,数据就像水一样,需要从一个地方"流"到另一个地方 - 从文件流向程序,或从程序流向网络。这就是为什么我们需要学习IO流技术。

Java的IO流技术为我们提供了一套完整的解决方案,使我们能够轻松地处理数据的输入与输出,无论是处理本地文件、网络通信,还是内存中的数据交换。掌握IO流,就像拥有了数据处理的"水管工具箱",让你能够自如地控制数据的流向和处理方式。

2. 基础概念

2.1 什么是IO流?

IO流(Input/Output Stream)是Java中用于处理输入和输出数据传输的抽象概念。它将数据的传输过程形象地比喻为"流水",数据如同水流一样从一个位置流向另一个位置。

                   IO流体系
                      |
           +-----------------------+
           |                       |
        字节流                   字符流
           |                       |
    +------+------+         +------+------+
    |             |         |             |
InputStream  OutputStream  Reader       Writer

3. 主要特性

3.1 方向性

IO流具有明确的方向性,每个流要么是输入流,要么是输出流,不能同时兼具两种功能。这种单向设计使得API更加简洁,使用更加清晰。

// 输入流示例:从文件读取数据
InputStream input = new FileInputStream("data.txt");

// 输出流示例:向文件写入数据
OutputStream output = new FileOutputStream("result.txt");

3.2 装饰者模式

Java IO流采用了装饰者设计模式,允许在基本流的基础上叠加额外的功能。这种模式使得IO流组件可以灵活组合,按需添加所需功能。

// 基本文件流 + 缓冲功能 + 对象序列化功能
ObjectOutputStream oos = new ObjectOutputStream(
                          new BufferedOutputStream(
                            new FileOutputStream("object.dat")));

3.3 缓冲机制

IO流中的缓冲机制大幅提高了数据处理效率。缓冲流通过减少底层系统调用次数,显著提升IO操作性能。

// 不使用缓冲的文件读取
FileReader fr = new FileReader("data.txt");

// 使用缓冲的文件读取,性能更好
BufferedReader br = new BufferedReader(new FileReader("data.txt"));

3.4 自动关闭资源

Java 7引入的try-with-resources语法可以自动关闭IO资源,避免资源泄漏,简化了异常处理代码。

// 自动关闭资源,不需要手动调用close()
try (FileInputStream fis = new FileInputStream("input.txt")) {
    // 使用输入流
}

3.5 字符编码处理

字符流可以自动处理字符编码转换,使得处理不同语言和编码的文本文件变得简单。

// 指定UTF-8编码读取文本文件
Reader reader = new InputStreamReader(
                  new FileInputStream("text.txt"), "UTF-8");

4. 详细用法

              Java IO流体系
                   |
     +-------------+-------------+
     |                           |
   字节流                      字符流
     |                           |
+----+----+                 +----+----+
|         |                 |         |
输入流    输出流            输入流    输出流
|         |                 |         |
InputStream OutputStream    Reader   Writer
     |         |               |        |
     +----+----+               +---+----+
          |                        |
     +----+----+----------------+  |
     |         |       |        |  |
 文件流     缓冲流    数据流  对象流 打印流
     |         |       |        |  |
FileXXXStream BufferedXXX DataXXX ObjectXXX PrintXXX

4.1 文件字节流 - 处理二进制数据的基础流

常用API

FileInputStream 常用方法

方法描述
public FileInputStream(File file)通过File对象创建文件输入流
public FileInputStream(String name)通过文件路径名创建文件输入流
public int read()读取单个字节,返回0-255的整数,如果到达文件末尾则返回-1
public int read(byte[] b)读取多个字节到字节数组,返回实际读取的字节数
public int read(byte[] b, int off, int len)读取len个字节到数组b的off位置,返回实际读取的字节数
public long skip(long n)跳过n个字节不读取
public int available()返回可以无阻塞地读取的字节数
public void close()关闭流并释放相关资源

FileOutputStream 常用方法

方法描述
public FileOutputStream(File file)通过File对象创建文件输出流(默认覆盖原文件)
public FileOutputStream(String name)通过文件路径名创建文件输出流(默认覆盖原文件)
public FileOutputStream(File file, boolean append)创建文件输出流,append为true表示追加模式
public void write(int b)写入单个字节
public void write(byte[] b)写入字节数组
public void write(byte[] b, int off, int len)写入字节数组的一部分(从off开始,长度为len)
public void flush()刷新缓冲区,确保所有数据被写出
public void close()关闭流并释放相关资源
          文件字节流体系
               |
      +-------------------+
      |                   |
FileInputStream    FileOutputStream
      |                   |
  读取二进制文件        写入二进制文件
4.1.1 FileInputStream

文件字节流是最基础的IO流,用于读写文件中的原始字节数据。

4.1.1 FileInputStream
try (FileInputStream fis = new FileInputStream("data.bin")) {
    // 读取单个字节
    int data = fis.read();  // 返回-1表示已到达文件末尾
    
    // 读取多个字节到缓冲区
    byte[] buffer = new byte[1024];
    int bytesRead = fis.read(buffer);  // 返回实际读取的字节数
    
    // 获取可用字节数
    int available = fis.available();
    
    // 跳过字节
    long skipped = fis.skip(100);  // 跳过100个字节
} catch (IOException e) {
    e.printStackTrace();
}
新手常见问题
  1. 忘记关闭流:使用完毕后必须关闭流释放资源,推荐使用try-with-resources自动关闭
  2. 误用返回值read()返回的是0-255的字节值,到达文件末尾返回-1,不要误解为字节本身
  3. 读取效率低:单字节读取效率极低,应使用字节数组批量读取
  4. 编码问题:处理文本时,字节流不处理字符编码,可能导致乱码,应使用字符流
  5. 缓冲区大小:缓冲区太小会导致频繁读取,太大会浪费内存,一般1KB-8KB为宜
4.1.2 FileOutputStream
try (FileOutputStream fos = new FileOutputStream("output.bin")) {
    // 写入单个字节
    fos.write(65);  // 写入字符'A'的ASCII码
    
    // 写入字节数组
    byte[] data = {66, 67, 68};  // 'B', 'C', 'D'
    fos.write(data);
    
    // 写入字节数组的部分内容
    fos.write(data, 1, 2);  // 只写入'C', 'D'
    
    // 刷新缓冲区,确保数据写入
    fos.flush();
} catch (IOException e) {
    e.printStackTrace();
}
字节流使用细节
  1. 文件覆盖问题FileOutputStream默认会覆盖现有文件,如需追加内容,使用追加模式构造函数

    // 追加模式
    FileOutputStream fos = new FileOutputStream("log.txt", true);
    
  2. 文件锁定:如果文件正被其他进程独占写入,可能导致FileNotFoundException

  3. 大文件处理:处理大文件时,建议使用缓冲流提高性能

  4. 二进制一致性:字节流可确保二进制数据的精确复制,适合非文本文件

  5. 目录自动创建:输出流不会自动创建不存在的目录,需要手动处理

    File file = new File("data/logs/app.log");
    if (!file.getParentFile().exists()) {
        file.getParentFile().mkdirs(); // 创建父目录
    }
    FileOutputStream fos = new FileOutputStream(file);
    

4.2 文件字符流 - 专门处理文本数据的基础流

常用API

FileReader 常用方法

方法描述
public FileReader(File file)通过File对象创建字符输入流
public FileReader(String fileName)通过文件路径名创建字符输入流
public int read()读取单个字符,返回0-65535的整数,到达末尾返回-1
public int read(char[] cbuf)读取多个字符到字符数组,返回实际读取的字符数
public int read(char[] cbuf, int off, int len)读取字符到数组指定位置,返回实际读取的字符数
public void close()关闭流并释放相关资源

FileWriter 常用方法

方法描述
public FileWriter(File file)通过File对象创建字符输出流(默认覆盖)
public FileWriter(String fileName)通过文件路径名创建字符输出流(默认覆盖)
public FileWriter(File file, boolean append)创建字符输出流,append为true表示追加模式
public void write(int c)写入单个字符
public void write(char[] cbuf)写入字符数组
public void write(String str)写入字符串
public void write(String str, int off, int len)写入字符串的一部分
public void flush()刷新缓冲区
public void close()关闭流并释放资源
          文件字符流结构
               |
     +---------+---------+
     |                   |
 FileReader           FileWriter
     |                   |
读取文本文件           写入文本文件
     |                   |
自动处理字符编码     自动处理字符编码
     |                   |
基于默认字符集       默认使用系统编码
4.2.1 FileReader

文件字符流专门用于处理文本数据,会自动进行字符编码/解码。

4.2.1 FileReader
try (FileReader fr = new FileReader("text.txt")) {
    // 读取单个字符
    int character = fr.read();  // 返回-1表示已到达文件末尾
    
    // 读取多个字符到缓冲区
    char[] buffer = new char[1024];
    int charsRead = fr.read(buffer);  // 返回实际读取的字符数
    
    // 将读取的字符转换为字符串
    String content = new String(buffer, 0, charsRead);
    System.out.println(content);
} catch (IOException e) {
    e.printStackTrace();
}
字符流编码注意事项
  1. 默认字符集问题:FileReader/FileWriter使用系统默认字符集,不同系统可能不同

    • Windows中文系统通常默认GBK/GB2312
    • Linux/Mac通常默认UTF-8
    • 跨平台应用应显式指定字符集
  2. 编码不匹配导致乱码:读写文件时使用的编码必须与文件的实际编码一致

    // 使用指定编码需要使用InputStreamReader/OutputStreamWriter
    try (Reader reader = new InputStreamReader(
             new FileInputStream("text.txt"), "UTF-8")) {
        // 读取内容
    }
    
  3. BOM标记问题:某些UTF-8文件可能包含BOM标记,需要特殊处理

  4. 缓冲区刷新:FileWriter有内部缓冲区,必须flush()或close()才能确保数据写入文件

  5. 非文本文件:字符流不适合处理二进制文件(图片、音频等),会导致数据损坏

4.2.2 FileWriter
try (FileWriter fw = new FileWriter("output.txt")) {
    // 写入单个字符
    fw.write('A');
    
    // 写入字符串
    fw.write("Hello, Java IO!");
    
    // 写入字符数组
    char[] chars = {'J', 'a', 'v', 'a'};
    fw.write(chars);
    
    // 写入字符数组的部分内容
    fw.write(chars, 1, 2);  // 只写入'a', 'v'
    
    // 刷新缓冲区
    fw.flush();
} catch (IOException e) {
    e.printStackTrace();
}

4.3 缓冲流 - 大幅提升IO性能的包装流

常用API

BufferedInputStream 常用方法

方法描述
public BufferedInputStream(InputStream in)创建缓冲输入流,使用默认缓冲区大小
public BufferedInputStream(InputStream in, int size)创建缓冲输入流,指定缓冲区大小
public int read()读取单个字节
public int read(byte[] b, int off, int len)读取多个字节到数组指定位置
public void mark(int readlimit)标记当前位置,readlimit指定在标记失效前可读取的最大字节数
public void reset()重置到最后一次mark的位置
public boolean markSupported()判断是否支持mark/reset操作
public void close()关闭流并释放资源

BufferedOutputStream 常用方法

方法描述
public BufferedOutputStream(OutputStream out)创建缓冲输出流,使用默认缓冲区大小
public BufferedOutputStream(OutputStream out, int size)创建缓冲输出流,指定缓冲区大小
public void write(int b)写入单个字节
public void write(byte[] b, int off, int len)写入字节数组的一部分
public void flush()刷新缓冲区
public void close()关闭流并释放资源

BufferedReader 常用方法

方法描述
public BufferedReader(Reader in)创建缓冲字符输入流,使用默认缓冲区大小
public BufferedReader(Reader in, int sz)创建缓冲字符输入流,指定缓冲区大小
public String readLine()读取一行文本,不包含换行符,到达末尾返回null
public int read()读取单个字符
public void close()关闭流并释放资源

BufferedWriter 常用方法

方法描述
public BufferedWriter(Writer out)创建缓冲字符输出流,使用默认缓冲区大小
public BufferedWriter(Writer out, int sz)创建缓冲字符输出流,指定缓冲区大小
public void write(int c)写入单个字符
public void write(String s, int off, int len)写入字符串的一部分
public void newLine()写入一个行分隔符(平台相关)
public void flush()刷新缓冲区
public void close()关闭流并释放资源
          缓冲流体系
               |
     +--------------------+
     |                    |
 字节缓冲流             字符缓冲流
     |                    |
+----+----+           +---+----+
|         |           |        |
BufferedIn  BufferedOut  BufferedReader  BufferedWriter
Stream      Stream       |                |
                     可按行读取        提供newLine()方法
4.3.1 BufferedInputStream/BufferedOutputStream

缓冲流通过使用内存缓冲区,大幅提高IO操作效率。

4.3.1 BufferedInputStream/BufferedOutputStream
// 缓冲字节输入流
try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("data.bin"), 8192)) {  // 指定缓冲区大小为8KB
    
    byte[] buffer = new byte[1024];
    int bytesRead;
    
    // 读取数据(性能比直接使用FileInputStream更好)
    while ((bytesRead = bis.read(buffer)) != -1) {
        // 处理读取的数据
    }
} catch (IOException e) {
    e.printStackTrace();
}

// 缓冲字节输出流
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("output.bin"))) {
    
    byte[] data = new byte[1024];
    // 假设data中已有数据
    
    // 写入数据(性能比直接使用FileOutputStream更好)
    bos.write(data);
    
    // 强制刷新缓冲区
    bos.flush();
} catch (IOException e) {
    e.printStackTrace();
}
4.3.2 BufferedReader/BufferedWriter
// 缓冲字符输入流
try (BufferedReader br = new BufferedReader(new FileReader("text.txt"))) {
    
    // 按行读取文本(这是BufferedReader的独特功能)
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}

// 缓冲字符输出流
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
    
    // 写入文本
    bw.write("Hello, Java IO!");
    
    // 写入行分隔符(平台无关)
    bw.newLine();
    
    // 写入更多文本
    bw.write("这是第二行");
    
    // 刷新缓冲区
    bw.flush();
} catch (IOException e) {
    e.printStackTrace();
}

4.4 数据流 - 读写基本数据类型的便捷流

常用API

DataInputStream 常用方法

方法描述
public DataInputStream(InputStream in)创建数据输入流
public final boolean readBoolean()读取一个布尔值
public final byte readByte()读取一个字节
public final short readShort()读取一个短整型值
public final int readInt()读取一个整型值
public final long readLong()读取一个长整型值
public final float readFloat()读取一个浮点型值
public final double readDouble()读取一个双精度浮点型值
public final String readUTF()读取UTF-8编码的字符串
public void close()关闭流并释放资源

DataOutputStream 常用方法

方法描述
public DataOutputStream(OutputStream out)创建数据输出流
public final void writeBoolean(boolean v)写入布尔值
public final void writeByte(int v)写入字节
public final void writeShort(int v)写入短整型值
public final void writeInt(int v)写入整型值
public final void writeLong(long v)写入长整型值
public final void writeFloat(float v)写入浮点型值
public final void writeDouble(double v)写入双精度浮点型值
public final void writeUTF(String str)写入UTF-8编码的字符串
public void flush()刷新缓冲区
public void close()关闭流并释放资源
           数据流体系
               |
      +-------------------+
      |                   |
DataInputStream    DataOutputStream
      |                   |
读取基本数据类型      写入基本数据类型
      |                   |
处理二进制格式        处理二进制格式
使用特点
  1. 必须按照写入顺序读取数据,否则会得到错误的结果
  2. 可以方便地处理各种Java基本数据类型,而不用手动进行类型转换
  3. 读写的数据是二进制格式,而非文本格式
  4. 通常用于网络传输或数据持久化需要保存Java数据类型的场景

数据流可以方便地读写Java基本数据类型和字符串。

// 写入数据
try (DataOutputStream dos = new DataOutputStream(
        new FileOutputStream("data.dat"))) {
    
    // 写入各种数据类型
    dos.writeInt(100);
    dos.writeDouble(3.14);
    dos.writeBoolean(true);
    dos.writeUTF("Hello, DataStream!");  // 写入UTF-8编码的字符串
} 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(i + ", " + d + ", " + b + ", " + s);
} catch (IOException e) {
    e.printStackTrace();
}

4.5 对象流 - 实现Java对象序列化和反序列化

           对象流/序列化体系
                  |
      +------------+------------+
      |                         |
ObjectInputStream       ObjectOutputStream
      |                         |
 (反序列化)                 (序列化)
      |                         |
   读取对象                  写入对象
      |                         |
  readObject()             writeObject()
4.5.1 序列化和反序列化基础

序列化:将Java对象转换成字节序列的过程,便于存储或网络传输 反序列化:将字节序列恢复为Java对象的过程

4.5.2 序列化的使用条件
  1. 必须实现Serializable接口
    • Serializable是一个标记接口,无需实现任何方法
    • 不实现此接口会抛出NotSerializableException异常
// 可序列化的类定义示例
public class Person implements Serializable {
    // 建议显式定义序列化版本号
    private static final long serialVersionUID = 1L;
    
    private String name;
    private int age;
    // transient关键字标记的字段不会被序列化
    private transient String password;
    
    // 构造方法、getter和setter方法略
}
4.5.3 序列化版本号(serialVersionUID)
  • 作用:确保序列化与反序列化的类版本一致
  • 问题:如果不指定,JVM会根据类结构自动生成,修改类后会变化
  • 后果:类结构变化但未指定版本号,反序列化时会抛出InvalidClassException
// 正确指定序列化版本号
private static final long serialVersionUID = 1L;
4.5.4 常用API

ObjectOutputStream 常用方法

方法描述
public ObjectOutputStream(OutputStream out)创建对象输出流
public final void writeObject(Object obj)将对象序列化并写入流
public void writeBoolean(boolean val)写入布尔值
public void writeInt(int val)写入整型值
public void writeUTF(String str)写入UTF-8编码的字符串
public void flush()刷新缓冲区
public void close()关闭流并释放资源

ObjectInputStream 常用方法

方法描述
public ObjectInputStream(InputStream in)创建对象输入流
public final Object readObject()读取并反序列化对象(需要强制类型转换)
public boolean readBoolean()读取布尔值
public int readInt()读取整型值
public String readUTF()读取UTF-8编码的字符串
public void close()关闭流并释放资源
4.5.5 对象序列化示例
// 序列化对象到文件
public static void serializeObject() {
    try (ObjectOutputStream oos = new ObjectOutputStream(
            new FileOutputStream("person.dat"))) {
        
        // 创建对象
        Person person = new Person("张三", 25);
        
        // 序列化对象
        oos.writeObject(person);
        
        System.out.println("对象已序列化到文件");
        
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// 从文件反序列化对象
public static void deserializeObject() {
    try (ObjectInputStream ois = new ObjectInputStream(
            new FileInputStream("person.dat"))) {
        
        // 反序列化对象 (需要强制类型转换)
        Person person = (Person) ois.readObject();
        
        // 使用反序列化后的对象
        System.out.println("姓名: " + person.getName());
        System.out.println("年龄: " + person.getAge());
        
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}
4.5.6 序列化的核心细节
  1. transient关键字阻止字段序列化
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String username;
    // 敏感信息不序列化
    private transient String password;
    
    // 反序列化后password字段值为null
}
  1. 静态字段不参与序列化
public class Configuration implements Serializable {
    // 静态字段不会被序列化,因为它们属于类而非对象
    private static String appName = "MyApp";
    
    // 实例字段会被序列化
    private String userPreference;
}
  1. 父类字段序列化条件
// 父类必须实现Serializable,否则其字段不会被序列化
public class Person implements Serializable {
    protected String name;
}

// 子类会自动序列化从父类继承的字段
public class Student extends Person {
    private int studentId;
}
  1. 自定义序列化行为
public class Account implements Serializable {
    private String accountNumber;
    private transient double balance; // 不想直接序列化余额
    
    // 自定义序列化方法
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // 先调用默认的序列化
        
        // 对敏感数据进行加密后序列化
        out.writeDouble(balance * 1.5); // 简单示例:存储加密后的余额
    }
    
    // 自定义反序列化方法
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // 先调用默认的反序列化
        
        // 读取并解密数据
        balance = in.readDouble() / 1.5; // 简单示例:解密余额
    }
}
4.5.7 序列化常见问题与解决方案
  1. NotSerializableException

    • 问题: 类未实现Serializable接口
    • 解决: 让类实现Serializable接口
  2. InvalidClassException

    • 问题: 类结构变化导致serialVersionUID不匹配
    • 解决: 显式定义serialVersionUID保持一致
  3. 序列化安全问题

    • 问题: 敏感数据被序列化存储
    • 解决: 使用transient修饰敏感字段或自定义序列化方法
  4. 父类不可序列化

    • 问题: 父类没有实现Serializable
    • 解决: 让父类实现Serializable或在子类自定义序列化方法
  5. 序列化对象中引用了不可序列化对象

    • 问题: 类包含不可序列化的引用对象
    • 解决: 使引用对象实现Serializable或将该引用标记为transient

对象流用于序列化和反序列化Java对象。

// 首先,确保需要序列化的类实现Serializable接口
class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    
    // 构造函数、getter和setter略
}

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

// 反序列化对象
try (ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("person.obj"))) {
    
    Person person = (Person) ois.readObject();  // 从文件读取并反序列化对象
    System.out.println(person.getName() + ", " + person.getAge());
} catch (IOException | ClassNotFoundException e) {
    e.printStackTrace();
}

4.6 转换流 - 连接字节流和字符流的桥梁

             转换流原理结构
                  |
       +----------+----------+
       |                     |
InputStreamReader     OutputStreamWriter
       |                     |
  字节流 → 字符流         字符流 → 字节流
       |                     |
     数据源                目的地
   (FileInputStream)    (FileOutputStream)
4.6.1 转换流的作用
  1. 作用1:指定字符集读写

    • 可以指定任意字符集进行读写,解决中文乱码问题
    • 常用于处理非UTF-8编码的文本文件
  2. 作用2:字节流和字符流之间的桥梁

    • 让字节流能够使用字符流中的方法
    • 解决跨平台字符编码不一致的问题
常用API

InputStreamReader 常用方法

方法描述
public InputStreamReader(InputStream in)使用默认字符编码创建 InputStreamReader 对象,将字节流转换为字符流
public InputStreamReader(InputStream in, String charsetName)使用指定的字符编码 charsetName 创建 InputStreamReader 对象,把字节流转换为字符流
public int read()从输入流读取单个字符,返回字符的 Unicode 码值,若到达流末尾返回 -1
public int read(char[] cbuf, int offset, int length)从输入流中将最多 length 个字符读入到字符数组 cbuf 中,从偏移量 offset 处开始存储,返回读取的字符数,到达流末尾时返回 -1
public void close()关闭流并释放与之关联的资源

OutputStreamWriter 常用方法

方法描述
public OutputStreamWriter(OutputStream out)使用默认字符编码创建 OutputStreamWriter 对象,将字符流连接到字节流
public OutputStreamWriter(OutputStream out, String charsetName)使用指定的字符编码 charsetName 创建 OutputStreamWriter 对象,把字符流连接到字节流
public void write(int c)写入单个字符
public void write(char[] cbuf, int off, int len)将字符数组 cbuf 中从偏移量 off 开始的 len 个字符写入流中
public void write(String str, int off, int len)将字符串 str 中从偏移量 off 开始的 len 个字符写入流中
public void flush()刷新该流的缓冲,确保缓冲中的字符都被写入到底层输出流
public void close()关闭流并释放与之关联的资源,在关闭前会先调用 flush () 方法
4.6.2 InputStreamReader使用示例
// 使用指定字符集读取文件(例如GBK编码的文件)
try (InputStreamReader isr = new InputStreamReader(
        new FileInputStream("gbkfile.txt"), "GBK")) {
    
    // 读取单个字符
    int c;
    while ((c = isr.read()) != -1) {
        System.out.print((char)c);
    }
    
    // 也可以使用字符数组批量读取
    /*
    char[] buffer = new char[1024];
    int len;
    while ((len = isr.read(buffer)) != -1) {
        System.out.print(new String(buffer, 0, len));
    }
    */
} catch (IOException e) {
    e.printStackTrace();
}
4.6.3 OutputStreamWriter使用示例
// 使用指定字符集写入文件
try (OutputStreamWriter osw = new OutputStreamWriter(
        new FileOutputStream("output.txt"), "UTF-8")) {
    
    // 写入中文内容
    osw.write("这是使用OutputStreamWriter写入的中文");
    
    // 写入多行内容
    osw.write("\r\n第二行内容");
    osw.write("\r\n第三行内容");
    
    // 刷新缓冲区确保写入
    osw.flush();
} catch (IOException e) {
    e.printStackTrace();
}
4.6.4 新手常见问题与细节
  1. 编码与解码必须一致

    • 读文件时的编码必须与文件实际编码一致
    • 否则会出现乱码,尤其是中文、日文等非ASCII字符
  2. 平台默认编码差异

    • Windows中文系统默认GBK/GB2312
    • Linux/Mac系统默认UTF-8
    • 不同JDK版本默认编码可能不同
  3. 字符集命名规范

    • JDK支持的字符集命名:UTF-8, GBK, ISO-8859-1等
    • Java 7+可使用StandardCharsets常量:StandardCharsets.UTF_8
  4. BOM标记问题

    • 某些UTF-8文件包含BOM标记(EF BB BF),可能导致文件开头出现乱码
    • 解决方法:跳过BOM或使用专门处理BOM的库
  5. JDK11+推荐写法

    // JDK11+更简洁的文件读写(自动处理字符集)
    String content = Files.readString(Path.of("test.txt"), StandardCharsets.UTF_8);
    Files.writeString(Path.of("output.txt"), content, StandardCharsets.UTF_8);
    
实际案例:跨平台文件编码转换器
/**
 * 文件编码转换器
 * 将一个编码的文本文件转换为另一个编码
 */
public static void convertFileEncoding(String sourceFile, String targetFile, 
                                     String fromEncoding, String toEncoding) throws IOException {
    try (BufferedReader reader = new BufferedReader(
             new InputStreamReader(new FileInputStream(sourceFile), fromEncoding));
         BufferedWriter writer = new BufferedWriter(
             new OutputStreamWriter(new FileOutputStream(targetFile), toEncoding))) {
        
        // 逐行读取并写入
        String line;
        while ((line = reader.readLine()) != null) {
            writer.write(line);
            writer.newLine(); // 写入行分隔符
        }
        // 确保所有数据写入
        writer.flush();
        
        System.out.println("文件编码已从 " + fromEncoding + " 转换为 " + toEncoding);
    }
}

转换流用于字节流和字符流之间的转换,可以指定字符编码。

```java
// 将字节输入流转换为字符输入流,并指定字符编码
try (InputStreamReader isr = new InputStreamReader(
        new FileInputStream("text.txt"), "UTF-8")) {
    
    char[] buffer = new char[1024];
    int charsRead;
    
    while ((charsRead = isr.read(buffer)) != -1) {
        System.out.print(new String(buffer, 0, charsRead));
    }
} catch (IOException e) {
    e.printStackTrace();
}

// 将字符输出流转换为字节输出流,并指定字符编码
try (OutputStreamWriter osw = new OutputStreamWriter(
        new FileOutputStream("output.txt"), "UTF-8")) {
    
    osw.write("这是使用UTF-8编码写入的中文文本");
    osw.flush();
} catch (IOException e) {
    e.printStackTrace();
}

4.7 打印流 - 方便输出各种数据类型的工具流

              打印流体系
                  |
      +------------+------------+
      |                         |
  PrintStream               PrintWriter
      |                         |
基于字节输出流               基于字符输出流
      |                         |
System.out/err            文本处理更便捷
      |                         |
不处理异常,更安全          不处理异常,更安全
4.7.1 打印流的特点
  1. 有字节打印流和字符打印流两种

    • PrintStream(字节打印流):操作目的地,不操作数据源
    • PrintWriter(字符打印流):操作目的地,不操作数据源
  2. 打印流的共同特点

    • 不会抛出IOException,内部会捕获异常
    • 都有自动刷新功能,调用println方法会自动刷新
    • 可以方便地打印各种数据类型
    • 是输出流,不能读取数据
4.7.2 常用API - PrintStream
方法描述
public PrintStream(OutputStream out)创建打印输出流
public PrintStream(String fileName)创建输出到指定文件的打印流
public PrintStream(File file)创建输出到指定File对象的打印流
public PrintStream(OutputStream out, boolean autoFlush)创建打印流,可设置自动刷新
public void print(boolean b)打印布尔值
public void print(int i)打印整数
public void print(Object obj)打印对象(调用toString())
public void println()打印换行符
public void println(String x)打印字符串后跟换行符
public PrintStream printf(String format, Object... args)格式化打印
public boolean checkError()检查是否有错误发生
public void close()关闭流并释放资源
4.7.3 常用API - PrintWriter
方法描述
public PrintWriter(Writer out)创建字符打印输出流
public PrintWriter(OutputStream out)基于字节流创建字符打印流
public PrintWriter(String fileName)创建输出到指定文件的打印流
public PrintWriter(File file)创建输出到指定File对象的打印流
public PrintWriter(Writer out, boolean autoFlush)创建打印流,可设置自动刷新
public void print(String s)打印字符串
public void print(Object obj)打印对象(调用toString())
public void println(String x)打印字符串后跟换行符
public PrintWriter printf(String format, Object... args)格式化打印
public PrintWriter format(String format, Object... args)格式化输出,与printf相似
public boolean checkError()检查是否有错误发生
public void close()关闭流并释放资源
4.7.4 PrintStream使用示例
// 1. 创建打印流
try (PrintStream ps = new PrintStream("log.txt")) {
    // 2. 打印不同类型的数据
    ps.println("这是一行文本"); // 打印字符串并换行
    ps.println(100); // 打印整数并换行
    ps.println(3.14); // 打印浮点数并换行
    ps.println(true); // 打印布尔值并换行
    
    // 3. 格式化打印
    ps.printf("姓名: %s, 年龄: %d, 成绩: %.1f%n", "张三", 18, 92.5);
    
    // 4. 打印对象
    Date now = new Date();
    ps.println(now); // 调用对象的toString()方法
    
    // 5. 不换行打印
    ps.print("ABC");
    ps.print("DEF"); // 输出结果为ABCDEF
    
    System.out.println("数据已写入log.txt文件");
} catch (FileNotFoundException e) {
    e.printStackTrace();
}
4.7.5 PrintWriter使用示例
// 1. 创建字符打印流
try (PrintWriter pw = new PrintWriter(new FileWriter("report.txt"), true)) { // true表示自动刷新
    // 2. 打印内容
    pw.println("==== 系统报告 ====");
    pw.println("生成时间: " + new Date());
    
    // 3. 格式化打印表格
    pw.println("\n---- 用户信息 ----");
    pw.printf("%-10s %-8s %-8s%n", "用户名", "年龄", "级别");
    pw.printf("%-10s %-8d %-8s%n", "张三", 18, "普通用户");
    pw.printf("%-10s %-8d %-8s%n", "李四", 24, "管理员");
    
    // 4. 使用format方法(与printf功能相同)
    pw.format("当前系统内存使用率: %.2f%%%n", 78.55);
    
    System.out.println("报告已生成到report.txt文件");
} catch (IOException e) {
    e.printStackTrace();
}
4.7.6 System.out与System.err

System类中预定义了两个打印流对象:

// 标准输出流(控制台输出)
public static final PrintStream out;

// 标准错误流(控制台错误输出)
public static final PrintStream err;

可以重定向这些流:

// 重定向标准输出
PrintStream originalOut = System.out;
try {
    // 将标准输出重定向到文件
    PrintStream fileOut = new PrintStream(new FileOutputStream("output.log"));
    System.setOut(fileOut);
    
    // 现在System.out的输出会写入文件
    System.out.println("这条消息将写入文件而不是控制台");
    
    // 完成后恢复原始输出
    System.setOut(originalOut);
    System.out.println("回到控制台输出");
} catch (FileNotFoundException e) {
    e.printStackTrace();
}
4.7.7 打印流与其他流的区别
  1. 异常处理

    • 普通流的写入方法会抛出IOException
    • 打印流的写入方法不抛出异常,使用checkError()检查错误
  2. 格式化能力

    • 普通流只能写入字节/字符
    • 打印流可以格式化输出各种数据类型
  3. 自动刷新

    • 普通流需要手动调用flush()
    • 打印流可以设置自动刷新模式
  4. 易用性

    • 普通流API相对底层
    • 打印流提供了更易用的高级API
4.7.8 自动刷新机制

PrintWriter的自动刷新必须同时满足两个条件:

  1. 构造方法中启用了自动刷新(传入true)
  2. 调用了println(), printf()或format()方法(注意:print()方法不会自动刷新)
// 启用自动刷新的PrintWriter
PrintWriter pw = new PrintWriter(new FileWriter("auto-flush.txt"), true);

pw.print("不会自动刷新"); // 不会触发刷新
pw.println("会自动刷新");  // 会触发刷新
pw.printf("也会%s", "自动刷新"); // 会触发刷新
```   |
自动刷新支持           自动刷新支持
打印流的特点
  1. 不会抛出IOException:内部捕获异常,可通过checkError()方法查看是否有错误
  2. 自动刷新:构造时可设置自动刷新模式,每次调用println、printf或format方法后自动刷新
  3. 格式化输出:支持类似C语言的printf格式化功能
  4. 永不阻塞:写入方法从不抛出IOException
常用的格式化打印
PrintWriter pw = new PrintWriter(new FileWriter("log.txt"));

// 格式化日期
pw.printf("当前日期:%tF%n", new Date()); // 2023-06-01

// 格式化数字
pw.printf("浮点数:%.2f%n", 3.14159); // 浮点数:3.14

// 表格式输出
pw.printf("%-10s%-8s%-8s%n", "姓名", "年龄", "成绩"); // 左对齐
pw.printf("%-10s%-8d%-8.1f%n", "张三", 18, 92.5);
pw.printf("%-10s%-8d%-8.1f%n", "李四", 17, 87.5);

打印流提供了方便的打印功能,可以轻松输出各种数据类型。

// 文件打印流
try (PrintStream ps = new PrintStream("log.txt")) {
    // 打印各种数据
    ps.print("String");
    ps.println(100);  // 打印并换行
    ps.printf("浮点数: %.2f%n", 3.14159);  // 格式化打印
    
    // 重定向标准输出
    System.setOut(ps);
    System.out.println("这条信息将输出到log.txt文件");
} catch (IOException e) {
    e.printStackTrace();
}

// 字符打印流
try (PrintWriter pw = new PrintWriter("report.txt", "UTF-8")) {
    pw.println("PrintWriter支持指定字符编码");
    pw.printf("当前日期: %tF%n", new Date());  // 格式化日期
} catch (IOException e) {
    e.printStackTrace();
}

5. 实际应用场景

      Java IO应用场景
            |
  +-------------------+
  |         |         |
文件操作  配置文件  数据处理
  |         |         |
复制/移动  读/写    序列化
删除/重命名        格式转换

5.1 文件复制

文件复制是IO流的基础应用,可以使用字节流高效复制任何类型的文件。

public static void copyFile(String sourcePath, String targetPath) throws IOException {
    try (InputStream in = new BufferedInputStream(new FileInputStream(sourcePath));
         OutputStream out = new BufferedOutputStream(new FileOutputStream(targetPath))) {
        
        byte[] buffer = new byte[8192];  // 8KB缓冲区
        int bytesRead;
        
        // 循环读取和写入数据
        while ((bytesRead = in.read(buffer)) != -1) {
            out.write(buffer, 0, bytesRead);
        }
        
        // 确保所有数据写入目标文件
        out.flush();
    }
}

5.2 配置文件读写 - 处理应用程序配置

               配置文件处理
                     |
        +------------+------------+
        |            |            |
  Properties文件   XML配置       JSON配置
        |            |            |
   键值对格式     结构化数据    轻量级数据
5.2.1 Properties文件处理

Properties类是Java中专门用于处理.properties配置文件的工具类。

Properties常用API

方法描述
public void load(InputStream inStream)从输入流加载属性
public void load(Reader reader)从字符输入流加载属性
public void store(OutputStream out, String comments)将属性保存到输出流
public void store(Writer writer, String comments)将属性保存到字符输出流
public String getProperty(String key)获取指定键的属性值
public String getProperty(String key, String defaultValue)获取属性值,若不存在返回默认值
public void setProperty(String key, String value)设置属性值
public Set<String> stringPropertyNames()获取所有属性名

典型用法

// 读取配置文件
public static Properties loadConfig(String filePath) throws IOException {
    Properties props = new Properties();
    try (FileInputStream fis = new FileInputStream(filePath)) {
        props.load(fis);
    }
    return props;
}

// 使用示例
try {
    Properties config = loadConfig("app.properties");
    
    // 读取配置
    String dbUrl = config.getProperty("database.url");
    String username = config.getProperty("database.username");
    // 提供默认值
    int timeout = Integer.parseInt(config.getProperty("database.timeout", "30"));
    
    System.out.println("数据库连接: " + dbUrl);
    
    // 修改配置
    config.setProperty("database.timeout", "60");
    
    // 保存配置
    try (FileOutputStream fos = new FileOutputStream("app.properties")) {
        config.store(fos, "Updated database configuration");
    }
} catch (IOException e) {
    System.err.println("配置文件处理错误: " + e.getMessage());
}
5.2.2 Properties文件常见问题
  1. 文件编码问题.properties文件默认使用ISO-8859-1编码,中文需要转义

    // 使用UTF-8读取包含中文的配置
    try (InputStreamReader reader = new InputStreamReader(
            new FileInputStream("config.properties"), "UTF-8")) {
        props.load(reader);
    }
    
  2. 配置路径问题:路径可能因运行环境而异

    // 从类路径加载配置
    try (InputStream in = Main.class.getClassLoader().getResourceAsStream("config.properties")) {
        if (in != null) {
            props.load(in);
        }
    }
    
  3. 配置热更新:如何实现不重启应用更新配置

    // 简单的定时重载配置示例
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
    executor.scheduleAtFixedRate(() -> {
        try {
            reloadConfig();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }, 1, 1, TimeUnit.MINUTES);
    
  4. 多级配置合并:开发、测试、生产环境配置管理

    // 合并多个配置文件
    Properties baseProps = loadConfig("base.properties");
    Properties envProps = loadConfig(env + ".properties");
    
    // 环境特定配置覆盖基础配置
    for (String key : envProps.stringPropertyNames()) {
        baseProps.setProperty(key, envProps.getProperty(key));
    }
    

5.3 数据序列化应用 - 对象持久化与传输

            序列化应用场景
                 |
     +-----------+-----------+
     |           |           |
对象持久化     网络传输    深拷贝
     |           |           |
保存到文件    RMI/网络协议  克隆对象
5.3.1 对象序列化保存示例
/**
 * 保存用户对象到文件
 */
public static void saveUsers(List<User> users, String filePath) {
    try (ObjectOutputStream oos = new ObjectOutputStream(
            new BufferedOutputStream(new FileOutputStream(filePath)))) {
        
        // 首先写入用户数量
        oos.writeInt(users.size());
        
        // 逐个序列化用户对象
        for (User user : users) {
            oos.writeObject(user);
        }
        
        System.out.println("成功保存 " + users.size() + " 个用户到 " + filePath);
    } catch (IOException e) {
        System.err.println("保存用户数据失败: " + e.getMessage());
        e.printStackTrace();
    }
}
5.3.2 对象反序列化读取示例
/**
 * 从文件读取用户对象列表
 */
public static List<User> loadUsers(String filePath) {
    List<User> users = new ArrayList<>();
    
    try (ObjectInputStream ois = new ObjectInputStream(
            new BufferedInputStream(new FileInputStream(filePath)))) {
        
        // 读取用户数量
        int count = ois.readInt();
        
        // 逐个反序列化用户对象
        for (int i = 0; i < count; i++) {
            User user = (User) ois.readObject();
            users.add(user);
        }
        
        System.out.println("成功加载 " + users.size() + " 个用户从 " + filePath);
    } catch (IOException | ClassNotFoundException e) {
        System.err.println("加载用户数据失败: " + e.getMessage());
        e.printStackTrace();
    }
    
    return users;
}
5.3.3 序列化版本控制实例
/**
 * 带版本控制的用户类
 */
public class User implements Serializable {
    // 显式定义序列化版本号
    private static final long serialVersionUID = 1L;
    
    private String username;
    private String email;
    // 新增字段 (v2)
    private Date registrationDate;
    // 敏感字段不序列化
    private transient String password;
    
    // 自定义序列化方法
    private void writeObject(ObjectOutputStream out) throws IOException {
        // 调用默认序列化
        out.defaultWriteObject();
        
        // 写入额外数据(如版本号)
        out.writeInt(2); // 当前类版本为2
    }
    
    // 自定义反序列化方法
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 调用默认反序列化
        in.defaultReadObject();
        
        // 读取版本号
        int version = in.readInt();
        
        // 根据版本处理兼容性
        if (version == 1) {
            // v1版本没有registrationDate字段,设置一个默认值
            this.registrationDate = new Date();
        }
        
        // 重新计算/初始化transient字段
        this.password = ""; // 安全起见,不恢复密码
    }
}
5.3.4 序列化在网络传输中的应用
// 客户端发送序列化对象
public void sendObject(Socket socket, Serializable obj) throws IOException {
    try (ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream())) {
        oos.writeObject(obj);
        oos.flush();
    }
}

// 服务端接收序列化对象
public Object receiveObject(Socket socket) throws IOException, ClassNotFoundException {
    try (ObjectInputStream ois = new ObjectInputStream(socket.getInputStream())) {
        return ois.readObject();
    }
}
5.3.5 使用序列化实现对象深拷贝
/**
 * 通过序列化实现深拷贝
 */
public static <T extends Serializable> T deepCopy(T obj) {
    try {
        // 将对象写入字节数组
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        oos.close();
        
        // 从字节数组读取对象副本
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        @SuppressWarnings("unchecked")
        T copy = (T) ois.readObject();
        ois.close();
        
        return copy;
    } catch (IOException | ClassNotFoundException e) {
        throw new RuntimeException("深拷贝失败", e);
    }
}

5.4 实际应用案例 - 日志记录器

下面展示一个使用多种IO流实现的简单日志记录器:

/**
 * 一个简单的日志记录器实现
 */
public class SimpleLogger {
    private final PrintWriter logWriter;
    private final boolean autoFlush;
    private final String dateFormat = "yyyy-MM-dd HH:mm:ss.SSS";
    
    /**
     * 创建日志记录器
     * @param logFile 日志文件路径
     * @param append 是否追加模式
     * @param autoFlush 是否自动刷新
     */
    public SimpleLogger(String logFile, boolean append, boolean autoFlush) throws IOException {
        // 创建日志目录
        File file = new File(logFile);
        File parent = file.getParentFile();
        if (parent != null && !parent.exists()) {
            parent.mkdirs();
        }
        
        // 创建带缓冲的PrintWriter
        this.logWriter = new PrintWriter(
                new BufferedWriter(
                        new FileWriter(logFile, append)), 
                autoFlush);
        this.autoFlush = autoFlush;
    }
    
    /**
     * 记录一条日志
     */
    public void log(String level, String message) {
        // 获取当前时间
        SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
        String timestamp = sdf.format(new Date());
        
        // 格式化日志消息
        logWriter.printf("%s [%s] %s%n", timestamp, level, message);
        
        // 如果不是自动刷新,则手动刷新
        if (!autoFlush) {
            logWriter.flush();
        }
    }
    
    /**
     * 记录普通信息
     */
    public void info(String message) {
        log("INFO", message);
    }
    
    /**
     * 记录警告信息
     */
    public void warn(String message) {
        log("WARN", message);
    }
    
    /**
     * 记录错误信息
     */
    public void error(String message) {
        log("ERROR", message);
    }
    
    /**
     * 记录异常信息
     */
    public void error(String message, Throwable t) {
        log("ERROR", message);
        
        // 使用StringWriter捕获堆栈信息
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        t.printStackTrace(pw);
        log("TRACE", sw.toString());
    }
    
    /**
     * 关闭日志记录器
     */
    public void close() {
        if (logWriter != null) {
            logWriter.close();
        }
    }
    
    // 使用示例
    public static void main(String[] args) {
        try {
            SimpleLogger logger = new SimpleLogger("logs/app.log", true, true);
            
            logger.info("应用启动");
            logger.warn("配置文件不存在,使用默认配置");
            
            try {
                // 模拟异常
                int result = 10 / 0;
            } catch (Exception e) {
                logger.error("计算过程中发生异常", e);
            }
            
            logger.info("应用退出");
            logger.close();
        } catch (IOException e) {
            System.err.println("无法创建日志记录器: " + e.getMessage());
        }
    }
}

6. 注意事项和最佳实践

        IO操作最佳实践
              |
  +-------------------------+
  |           |             |
资源管理     性能优化     安全防护
  |           |             |
自动关闭    使用缓冲     异常处理
避免泄漏    批量操作     权限检查
          合适的缓冲区    编码指定

6.1 资源管理

  • 始终关闭IO流:未关闭的流会导致资源泄漏。优先使用try-with-resources语法自动关闭资源。
  • 关闭最外层的流:当使用装饰者模式嵌套多个流时,只需关闭最外层的流,它会级联关闭内部所有流。
// 推荐:使用try-with-resources自动关闭资源
try (InputStream in = new FileInputStream("file.txt")) {
    // 使用流
}

// 不推荐:手动关闭容易在异常情况下泄露资源
InputStream in = null;
try {
    in = new FileInputStream("file.txt");
    // 使用流
} finally {
    if (in != null) {
        try {
            in.close();
        } catch (IOException e) {
            // 处理关闭异常
        }
    }
}

6.2 性能优化

  • 使用缓冲流:BufferedInputStream/BufferedOutputStream可以显著提高IO性能。
  • 选择合适的缓冲区大小:对于大文件,使用8KB~16KB的缓冲区通常能获得最佳性能。
  • 减少IO操作次数:批量读写比单字节读写效率高得多。
  • 使用NIO的内存映射文件:处理超大文件时,使用内存映射更高效。
// 高效的文件复制实现
public static void fastCopy(File source, File target) throws IOException {
    try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(source), 16384);
         BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(target), 16384)) {
        
        byte[] buffer = new byte[16384];  // 16KB缓冲区
        int bytesRead;
        
        while ((bytesRead = in.read(buffer)) != -1) {
            out.write(buffer, 0, bytesRead);
        }
    }
}

6.3 异常处理

  • 具体捕获IO异常:尽量捕获具体的异常类型,如FileNotFoundException,而不是笼统的IOException。
  • 不忽略关闭异常:关闭资源时的异常可能表明数据未完全写入。
  • 保留原始异常信息:使用异常链(exception chaining)保留原始异常信息。
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
    // 使用流
} catch (FileNotFoundException e) {
    System.err.println("找不到指定文件: " + e.getMessage());
    // 处理文件未找到的情况
} catch (IOException e) {
    System.err.println("IO操作失败: " + e.getMessage());
    // 处理其他IO异常
}

6.4 字符编码

  • 显式指定字符编码:避免依赖平台默认编码,总是明确指定编码(如UTF-8)。
  • 统一应用中的编码:整个应用使用一致的字符编码,避免乱码问题。
  • 处理BOM标记:某些UTF-8文件可能包含BOM标记,可能导致问题。
// 推荐:显式指定字符编码
Reader reader = new InputStreamReader(new FileInputStream("text.txt"), StandardCharsets.UTF_8);
Writer writer = new OutputStreamWriter(new FileOutputStream("output.txt"), StandardCharsets.UTF_8);

// 不推荐:依赖平台默认编码,可能导致跨平台问题
Reader badReader = new InputStreamReader(new FileInputStream("text.txt"));

6.5 安全最佳实践

  • 验证用户输入的路径:防止目录遍历攻击。
  • 限制文件操作权限:使用Java的安全管理器限制IO操作。
  • 避免在临时目录中存储敏感信息:临时文件可能被其他用户访问。
  • 使用安全的序列化机制:普通序列化存在安全风险,考虑JSON或其他安全替代方案。
// 验证文件路径是否安全
public static boolean isPathSafe(String path) {
    File file = new File(path).getCanonicalFile();
    File safeRoot = new File("/safe/directory").getCanonicalFile();
    
    // 确保文件在安全目录下
    return file.toPath().startsWith(safeRoot.toPath());
}

6.6 资源关闭顺序

  • 先打开后关闭:后打开的流应该先关闭
  • 从外到内关闭:外层流关闭时会自动关闭内层流
  • 先写入后关闭:关闭前确保所有数据都已写入
// 资源关闭顺序示例
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
    // 先打开FileInputStream
    fis = new FileInputStream("file.txt");
    // 后打开BufferedInputStream
    bis = new BufferedInputStream(fis);
    
    // 使用流...
} finally {
    // 先关闭后打开的流
    if (bis != null) {
        try {
            bis.close(); // 这里会自动关闭fis
        } catch (IOException e) {
            // 处理异常
        }
    }
}

6.7 flush与close的区别

  • flush:刷新缓冲区,将数据写入底层设备,但流仍然可用
  • close:关闭流,释放资源,之后不能再使用该流
  • close自动flush:大多数流的close()方法会自动调用flush()
// flush用法
FileWriter writer = new FileWriter("data.txt");
writer.write("一些数据");
writer.flush(); // 确保数据写入文件
writer.write("更多数据"); // 流仍然可用

// close用法
writer.close(); // 自动flush并关闭流
// writer.write("错误!"); // 错误!流已关闭

7. 与相关技术的对比

              IO技术对比
                  |
 +----------------+----------------+
 |                |                |
Java IO          Java NIO       工具库
 |                |                |
流式API        通道和缓冲      Commons IO
阻塞IO         非阻塞IO          Guava
简单直观       高级功能          Hutool

7.1 Java IO流与NIO对比

特性传统IO (java.io)NIO (java.nio)
设计模式流式,阻塞通道和缓冲区,可非阻塞
处理方式一次一个字节/字符基于缓冲区块处理
适用场景简单顺序访问,内容较小高并发,大文件,需要随机访问
API复杂度简单直观较复杂
缓冲特性需要显式使用BufferedInputStream等内置缓冲功能
异步能力不支持支持异步IO
文件锁定不支持支持
内存映射文件不支持支持
// 传统IO读取文件
try (FileInputStream fis = new FileInputStream("data.txt")) {
    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = fis.read(buffer)) != -1) {
        // 处理数据
    }
}

// NIO读取文件
try (FileChannel channel = FileChannel.open(Paths.get("data.txt"), StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    while (channel.read(buffer) != -1) {
        buffer.flip();  // 切换到读模式
        // 从buffer读取数据
        buffer.clear();  // 准备下一次写入
    }
}

7.2 Java IO与Apache Commons IO对比

特性Java标准IOApache Commons IO
API设计较底层高级封装,更加便捷
代码量较多显著减少
功能特性基础功能增强功能(如文件监控、文件过滤等)
依赖性无外部依赖需要引入第三方库
维护成本只依赖JDK需跟踪第三方库更新
// 标准Java IO复制文件
try (InputStream in = new FileInputStream(src);
     OutputStream out = new FileOutputStream(dest)) {
    byte[] buffer = new byte[8192];
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
        out.write(buffer, 0, bytesRead);
    }
}

// Apache Commons IO复制文件
File srcFile = new File("source.txt");
File destFile = new File("destination.txt");
FileUtils.copyFile(srcFile, destFile);

7.3 其他常用工具库对比

工具库主要特点适用场景
Apache Commons IO简化常见IO操作,提供实用工具类简单高效的日常IO操作
Guava IO与Google Guava集合库无缝集成需要与Guava其他功能集成的项目
Hutool全面的IO工具类,国产开源工具集需要中文文档和全面功能支持的项目
FastJSON高性能JSON处理需要高性能JSON序列化/反序列化
Jackson功能全面的JSON处理库企业级应用,需要全面JSON功能
Hutool工具包示例
// 读取文件内容
String content = FileUtil.readUtf8String(new File("test.txt"));

// 写入文件
FileUtil.writeUtf8String("这是测试内容", "output.txt");

// 复制文件
FileUtil.copy(new File("source.txt"), new File("dest.txt"), true);

// 创建多级目录
FileUtil.mkdir("path/to/directory");

// 文件类型判断
boolean isImg = FileTypeUtil.isImage(new File("test.jpg"));
Guava工具包示例
// 读取文本文件内容为字符串
String content = Files.asCharSource(new File("test.txt"), Charsets.UTF_8).read();

// 将字符串写入文件
Files.asCharSink(new File("output.txt"), Charsets.UTF_8).write("这是测试内容");

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

// 递归遍历目录中的所有.java文件
Iterable<File> javaFiles = Files.fileTraverser()
                               .breadthFirst(new File("src"))
                               .filter(f -> f.getName().endsWith(".java"));

8. 面试常见问题

Q1: Java中的IO流分为哪几种?主要特点是什么?

A: Java IO流主要分为字节流和字符流两大类:

  • 字节流:以字节为单位处理数据,适合处理任何类型的数据,包括二进制文件。核心抽象类是InputStreamOutputStream
  • 字符流:以字符为单位处理数据,内部进行了字符编码/解码,适合处理文本数据。核心抽象类是ReaderWriter

每种流又可以根据功能和用途细分为文件流、缓冲流、数据流、对象流等多种类型。

Q2: 为什么要使用缓冲流?它是如何提高性能的?

A: 缓冲流通过减少系统底层的IO操作次数来提高性能。具体工作原理:

  1. 读取数据时,缓冲流一次性从底层流读取大量数据到内存缓冲区,后续读取直接从缓冲区获取,减少磁盘访问
  2. 写入数据时,先写入内存缓冲区,当缓冲区满或刷新时才真正写入底层流,将多次零散写入合并为一次批量写入

性能提升尤其明显在进行小批量、频繁的读写操作时。在文件复制等场景中,使用缓冲流可以提升5-10倍甚至更高的性能。

Q3: 序列化和反序列化是什么?如何实现?有哪些注意事项?

A: 序列化是将Java对象转换为字节序列的过程,反序列化是将字节序列恢复为Java对象的过程。

实现方式

  1. 类需要实现Serializable接口(这是一个标记接口,没有方法需要实现)
  2. 使用ObjectOutputStreamwriteObject()方法将对象序列化
  3. 使用ObjectInputStreamreadObject()方法将字节反序列化为对象

注意事项

  • statictransient修饰的成员变量不会被序列化
  • 为保证版本兼容性,建议手动定义serialVersionUID
  • 所有引用的对象也必须是可序列化的
  • 反序列化不调用构造方法,但会调用最近的非序列化父类的无参构造方法
  • 注意安全风险,序列化可能导致敏感数据泄露

Q4: Java中如何实现文件复制?请给出不同性能级别的实现。

A: 文件复制有多种实现方式,性能从低到高:

  1. 基础字节流:逐字节复制,性能最差
try (FileInputStream in = new FileInputStream(source);
     FileOutputStream out = new FileOutputStream(target)) {
    int c;
    while ((c = in.read()) != -1) {
        out.write(c);
    }
}
  1. 带缓冲的字节数组:使用字节数组作为缓冲,性能有明显提升
try (FileInputStream in = new FileInputStream(source);
     FileOutputStream out = new FileOutputStream(target)) {
    byte[] buffer = new byte[8192];
    int length;
    while ((length = in.read(buffer)) != -1) {
        out.write(buffer, 0, length);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值