Skip to content

Java IO流完全指南:字节流、字符流与序列化

目录


一、IO流基础概念

1.1 什么是IO流

专业角度定义: 将数据从一个设备传输到另外一个设备上的技术。

JavaSE角度定义: 将数据从内存写到硬盘上,然后还能从硬盘上将数据读回来的技术。

1.2 输入与输出

  • 输出(Output): 将数据从内存中写到硬盘上(谁发数据谁就是输出)
  • 输入(Input): 将数据从硬盘上读到内存中(谁接数据谁就是输入)

IO流输入输出示意图

1.3 IO流分类

分类说明适用场景
字节流一切皆字节(万能流),侧重复制操作文件复制、图片、视频等二进制文件
字符流主要对文本文档进行读写操作文本文件读写

IO流四大基类:

  • OutputStream(字节输出流)
  • InputStream(字节输入流)
  • Writer(字符输出流)
  • Reader(字符输入流)

二、字节流

2.1 FileOutputStream(字节输出流)

2.1.1 概述

  • 作用: 往硬盘上写数据
  • 父类: OutputStream(抽象类)

2.1.2 构造方法

构造方法说明
FileOutputStream(File file)创建文件输出流,写入到指定File对象
FileOutputStream(String path)创建文件输出流,写入到指定路径文件
FileOutputStream(String path, boolean append)append为true时追加写入,不覆盖原文件

2.1.3 常用方法

方法说明
write(int a)一次写一个字节
write(byte[] bytes)一次写一个字节数组
write(byte[] bytes, int offset, int count)一次写一个字节数组的一部分
close()关闭资源

2.1.4 特点

  1. 如果文件不存在,运行时会自动创建
  2. 默认情况下,每次执行都会产生新文件,覆盖老文件

2.1.5 代码示例

示例1:一次写一个字节

java
FileOutputStream fos = new FileOutputStream("day14_io/1.txt");
fos.write(97);  // 写入字符 'a'
fos.close();

示例2:一次写一个字节数组

java
FileOutputStream fos = new FileOutputStream("day14_io/1.txt");
byte[] bytes = {97, 98, 99, 100};
fos.write(bytes);  // 写入 "abcd"
fos.close();

示例3:写入字符串

java
FileOutputStream fos = new FileOutputStream("day14_io/1.txt");
byte[] bytes = "我爱中国".getBytes();
fos.write(bytes);
fos.close();

示例4:换行与续写

java
FileOutputStream fos = new FileOutputStream("day14_io/1.txt", true);
fos.write("白日依山尽\r\n".getBytes());
fos.write("黄河入海流\r\n".getBytes());
fos.write("欲穷千里目\r\n".getBytes());
fos.write("更上一层楼\r\n".getBytes());
fos.close();

换行符说明:

  • Windows: \r\n
  • Linux: \n
  • Mac OS: \r

2.2 FileInputStream(字节输入流)

2.2.1 概述

  • 作用: 读数据,将硬盘上的数据读到内存中
  • 父类: InputStream(抽象类)

2.2.2 构造方法

构造方法说明
FileInputStream(File file)创建文件输入流,读取指定File对象
FileInputStream(String path)创建文件输入流,读取指定路径文件

2.2.3 常用方法

方法说明
int read()一次读一个字节,返回读取的字节
int read(byte[] bytes)一次读一个字节数组,返回读取的字节个数
int read(byte[] bytes, int offset, int count)一次读一个字节数组的一部分
close()关闭资源

2.2.4 代码示例

示例1:一次读一个字节

java
FileInputStream fis = new FileInputStream("day14_io/2.txt");
int len = 0;
while((len = fis.read()) != -1) {
    System.out.println((char) len);
}
fis.close();

示例2:一次读一个字节数组

java
FileInputStream fis = new FileInputStream("day14_io/2.txt");
byte[] bytes = new byte[1024];
int len = 0;
while((len = fis.read(bytes)) != -1) {
    System.out.println(new String(bytes, 0, len));
}
fis.close();

字节数组读取过程

2.2.5 注意事项 ⭐

  1. 流中的数据读完之后,就不能再继续读了,如果还想重新读,需要再new一个对象
  2. 读取的过程中,不要连续写多个read
  3. 流关闭之后,不能再次使用,否则会报错:java.io.IOException: Stream Closed

2.3 字节流实现文件复制

2.3.1 复制原理

字节流复制图片分析

2.3.2 代码实现

java
public class Demo03Copy {
    public static void main(String[] args) throws Exception {
        FileInputStream fis = new FileInputStream("F:\\idea\\io\\8.jpg");
        FileOutputStream fos = new FileOutputStream("F:\\idea\\io\\智妍.jpg");
        
        byte[] bytes = new byte[1024];
        int len = 0;
        while((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }
        
        fos.close();
        fis.close();
    }
}

复制流程:

  1. 创建输入流,读取源文件
  2. 创建输出流,写入目标文件
  3. 创建字节数组(一般长度为1024或其倍数)
  4. 边读边写
  5. 关闭流(先开后关)

三、字符流

3.1 为什么需要字符流

字节流读取中文的问题:

  • 一个汉字在GBK中占2个字节
  • 一个汉字在UTF-8中占3个字节
  • 字节流是万能流,但读取中文时可能出现乱码

解决方案: 使用字符流,将文本文档内容看成一个一个的字符进行操作。

3.2 FileReader(字符输入流)

3.2.1 概述

  • 作用: 读数据
  • 父类: Reader(抽象类)

3.2.2 构造方法

构造方法说明
FileReader(File file)创建字符输入流,读取指定File对象
FileReader(String path)创建字符输入流,读取指定路径文件

3.2.3 常用方法

方法说明
int read()一次读一个字符
int read(char[] chars)一次读一个字符数组,返回读取个数
int read(char[] chars, int offset, int count)一次读一个字符数组的一部分
close()关闭资源

3.2.4 代码示例

示例1:一次读一个字符

java
FileReader fr = new FileReader("day14_io/3.txt");
int len = 0;
while((len = fr.read()) != -1) {
    System.out.println((char) len);
}
fr.close();

示例2:一次读一个字符数组

java
FileReader fr = new FileReader("day14_io/3.txt");
char[] chars = new char[1024];
int len = 0;
while((len = fr.read(chars)) != -1) {
    System.out.println(new String(chars, 0, len));
}
fr.close();

3.3 FileWriter(字符输出流)

3.3.1 概述

  • 作用: 写数据
  • 父类: Writer(抽象类)

3.3.2 构造方法

构造方法说明
FileWriter(File file)创建字符输出流,写入到指定File对象
FileWriter(String path)创建字符输出流,写入到指定路径文件
FileWriter(String path, boolean append)append为true时追加写入

3.3.3 常用方法

方法说明
write(int c)一次写一个字符
write(char[] cbuf)一次写一个字符数组
write(char[] cbuf, int off, int len)一次写一个字符数组的一部分
write(String str)一次写一个字符串
flush()刷新缓冲区
close()关闭资源

3.3.4 代码示例

java
FileWriter fw = new FileWriter("day14_io/4.txt", true);
fw.write("风萧萧兮易水寒\r\n");
fw.write("壮士一去兮不复还\r\n");
fw.close();

3.4 flush()与close()的区别 ⭐⭐⭐

方法功能后续使用
flush()将缓冲区中的数据刷到文件中流对象还能继续使用
close()先刷新,后关闭流对象不能继续使用

重要说明: 字符输出流底层有一个缓冲区,需要将内容从缓冲区刷到文件中。

3.5 IO异常处理

3.5.1 JDK7之前的方式

java
FileWriter fw = null;
try {
    fw = new FileWriter("day14_io/4.txt");
    fw.write("我爱祖国");
} catch (Exception e) {
    e.getStackTrace();
} finally {
    if (fw != null) {
        try {
            fw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.5.2 JDK7的try-with-resources

java
try (FileWriter fw = new FileWriter("day14_io/4.txt")) {
    fw.write("我爱我的祖国");
} catch (Exception e) {
    e.printStackTrace();
}

要求:

  • 资源必须实现 java.io.Closeable 接口
  • 在try子句中声明并初始化资源对象
  • 该资源对象必须是final的

3.5.3 JDK9的改进

java
FileWriter fw = new FileWriter("day14_io/4.txt");
try (fw) {
    fw.write("我爱我的祖国111");
} catch (Exception e) {
    e.printStackTrace();
}

改进: 可以直接使用已初始化的资源对象。


四、序列化流与打印流

序列化流与打印流

4.1 序列化流(ObjectOutputStream)

4.1.1 概述

  • 作用: 写对象
  • 父类: OutputStream

4.1.2 构造方法

构造方法说明
ObjectOutputStream(OutputStream os)创建序列化流对象

4.1.3 常用方法

方法说明
writeObject(Object obj)写入对象

4.1.4 代码示例

Person类:

java
public class Person implements Serializable {
    private static final long serialVersionUID = 42L;
    private String name;
    private Integer age;

    public Person() {}

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

序列化操作:

java
ObjectOutputStream oos = new ObjectOutputStream(
    new FileOutputStream("day14_io/person.txt"));
Person p1 = new Person("张三", 18);
oos.writeObject(p1);
oos.close();

重要: 如果想要序列化一个对象,此对象必须实现 Serializable 接口。

4.2 反序列化流(ObjectInputStream)

4.2.1 概述

  • 作用: 读对象
  • 父类: InputStream

4.2.2 构造方法

构造方法说明
ObjectInputStream(InputStream is)创建反序列化流对象

4.2.3 常用方法

方法说明
readObject()读取对象

4.2.4 代码示例

java
ObjectInputStream ois = new ObjectInputStream(
    new FileInputStream("day14_io/person.txt"));
Object o = ois.readObject();
Person p = (Person) o;
System.out.println(p);
ois.close();

4.3 序列号冲突问题 ⭐⭐⭐

4.3.1 问题描述

如果修改了对象的源码,没有重新序列化,直接反序列化,会出现"序列号冲突问题"。

4.3.2 原因

修改源代码后,class文件中的新序列号和文件中存储的序列号不一致。

序列号冲突问题

4.3.3 解决方案

方案1: 修改源码后,重新序列化

方案2(推荐): 在被操作的对象中将序列号定死

java
static final long serialVersionUID = 42L;

4.4 反序列化多个对象

4.4.1 问题

如果读取对象的次数和存储的对象个数不一致,会出现 EOFException(文件意外到达结尾异常)。

4.4.2 解决方案

将多个对象放到一个集合对象中,然后将这一个集合对象序列化到文件。

java
ObjectOutputStream oos = new ObjectOutputStream(
    new FileOutputStream("day14_io/person.txt"));

Person p1 = new Person("张三", 18);
Person p2 = new Person("李四", 20);
Person p3 = new Person("王五", 22);

ArrayList<Person> list = new ArrayList<>();
list.add(p1);
list.add(p2);
list.add(p3);

oos.writeObject(list);
oos.close();

读取集合:

java
ObjectInputStream ois = new ObjectInputStream(
    new FileInputStream("day14_io/person.txt"));
Object o = ois.readObject();
ArrayList<Person> list = (ArrayList<Person>) o;
for (Person person : list) {
    System.out.println(person);
}
ois.close();

4.5 打印流(PrintStream)

4.5.1 概述

  • 作用: 将数据打印到控制台或指定文件
  • 父类: OutputStream

4.5.2 构造方法

构造方法说明
PrintStream(String path)创建打印流,输出到指定路径文件

4.5.3 常用方法

方法说明
println()原样输出,自带换行效果
print()原样输出,不带换行效果

4.5.4 代码示例

基本使用:

java
PrintStream ps = new PrintStream("day14_io/print.txt");
ps.println("床前明月光");
ps.println("疑是地上霜");
ps.println("举头望明月");
ps.println("低头思故乡");
ps.close();

改变输出流向:

java
PrintStream ps = new PrintStream("day14_io/log.txt");
System.setOut(ps);  // 改变流向

System.out.println("出现了一个问题:NullPointerException");
System.out.println("问题出现在代码的第10行");
System.out.println("原因是字符串为null了");

使用场景: 将输出内容保存到日志文件中,实现永久保存。


五、常见面试题

5.1 基础概念题 ⭐

Q1:什么是IO流?

答案:

  • 专业角度:将数据从一个设备传输到另外一个设备上的技术
  • JavaSE角度:将数据从内存写到硬盘上,然后还能从硬盘上将数据读回来的技术

Q2:字节流和字符流的区别?

答案:

对比项字节流字符流
处理单位字节(byte)字符(char)
适用场景二进制文件(图片、视频等)文本文件
是否万能是(万能流)
中文处理可能乱码不会乱码(编码一致时)

Q3:什么时候使用字节流,什么时候使用字符流?

答案:

  • 字节流: 处理二进制文件(图片、视频、音频等),或进行文件复制操作
  • 字符流: 处理文本文件,需要读取或写入中文内容时

5.2 进阶应用题 ⭐⭐

Q4:flush()和close()的区别?

答案:

  • flush():将缓冲区中的数据刷到文件中,流对象还能继续使用
  • close():先刷新缓冲区,然后关闭流,流对象不能继续使用

Q5:什么是序列化和反序列化?

答案:

  • 序列化: 将对象转换为字节序列,写入到文件中(ObjectOutputStream)
  • 反序列化: 将字节序列恢复为对象(ObjectInputStream)
  • 要求: 对象必须实现 Serializable 接口

Q6:什么是序列号冲突问题?如何解决?

答案:

  • 问题: 修改对象源码后,没有重新序列化直接反序列化,会报错
  • 原因: class文件中的新序列号和文件中存储的序列号不一致
  • 解决: 在类中定义固定的序列号:static final long serialVersionUID = 42L;

5.3 高级深入题 ⭐⭐⭐

Q7:为什么字符流需要缓冲区,而字节流不需要?

答案:

  • 字符流处理的是字符,需要考虑编码问题
  • 字符流底层使用字节流读取字节,然后按照编码转换为字符
  • 缓冲区可以提高效率,减少IO操作次数
  • 字节流直接操作字节,不需要编码转换

Q8:如何实现文件复制?有哪些方式?

答案:

方式1:字节流一次读一个字节

java
FileInputStream fis = new FileInputStream("source.txt");
FileOutputStream fos = new FileOutputStream("target.txt");
int len = 0;
while((len = fis.read()) != -1) {
    fos.write(len);
}
fos.close();
fis.close();

方式2:字节流一次读一个字节数组(推荐)

java
FileInputStream fis = new FileInputStream("source.txt");
FileOutputStream fos = new FileOutputStream("target.txt");
byte[] bytes = new byte[1024];
int len = 0;
while((len = fis.read(bytes)) != -1) {
    fos.write(bytes, 0, len);
}
fos.close();
fis.close();

方式3:字符流(仅适用于文本文件)

java
FileReader fr = new FileReader("source.txt");
FileWriter fw = new FileWriter("target.txt");
char[] chars = new char[1024];
int len = 0;
while((len = fr.read(chars)) != -1) {
    fw.write(chars, 0, len);
}
fw.close();
fr.close();

Q9:如何将System.out.println()的输出重定向到文件?

答案:

java
PrintStream ps = new PrintStream("log.txt");
System.setOut(ps);  // 改变输出流向
System.out.println("这行内容会输出到文件中");

使用场景: 日志记录,将程序输出永久保存到文件。

Q10:如何处理IO异常?不同JDK版本的区别?

答案:

JDK7之前: 传统try-catch-finally

java
FileWriter fw = null;
try {
    fw = new FileWriter("file.txt");
    fw.write("content");
} catch (Exception e) {
    e.printStackTrace();
} finally {
    if (fw != null) {
        try {
            fw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

JDK7: try-with-resources

java
try (FileWriter fw = new FileWriter("file.txt")) {
    fw.write("content");
} catch (Exception e) {
    e.printStackTrace();
}

JDK9: 改进的try-with-resources

java
FileWriter fw = new FileWriter("file.txt");
try (fw) {
    fw.write("content");
} catch (Exception e) {
    e.printStackTrace();
}

六、实用代码示例

6.1 文件复制工具类

java
public class FileCopyUtil {
    public static void copyFile(String source, String target) throws IOException {
        try (FileInputStream fis = new FileInputStream(source);
             FileOutputStream fos = new FileOutputStream(target)) {
            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = fis.read(bytes)) != -1) {
                fos.write(bytes, 0, len);
            }
        }
    }
}

6.2 文本文件读取工具

java
public class TextFileReader {
    public static String readTextFile(String path) throws IOException {
        StringBuilder sb = new StringBuilder();
        try (FileReader fr = new FileReader(path)) {
            char[] chars = new char[1024];
            int len = 0;
            while ((len = fr.read(chars)) != -1) {
                sb.append(chars, 0, len);
            }
        }
        return sb.toString();
    }
}

6.3 对象序列化工具

java
public class ObjectSerializeUtil {
    public static void serialize(Object obj, String path) throws IOException {
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(path))) {
            oos.writeObject(obj);
        }
    }

    public static Object deserialize(String path) 
            throws IOException, ClassNotFoundException {
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(path))) {
            return ois.readObject();
        }
    }
}

6.4 日志记录工具

java
public class LoggerUtil {
    private static PrintStream ps;

    static {
        try {
            ps = new PrintStream("app.log");
            System.setOut(ps);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    public static void log(String message) {
        System.out.println(new Date() + " - " + message);
    }
}

七、经典练习题

7.1 基础题

题目1:文件写入 编写程序,将以下内容写入到文件中:

静夜思
李白
床前明月光,疑是地上霜。
举头望明月,低头思故乡。

参考答案:

java
public class Exercise01 {
    public static void main(String[] args) throws IOException {
        FileWriter fw = new FileWriter("静夜思.txt");
        fw.write("静夜思\r\n");
        fw.write("李白\r\n");
        fw.write("床前明月光,疑是地上霜。\r\n");
        fw.write("举头望明月,低头思故乡。\r\n");
        fw.close();
    }
}

题目2:文件读取 读取一个文本文件,统计文件中字符的个数。

参考答案:

java
public class Exercise02 {
    public static void main(String[] args) throws IOException {
        FileReader fr = new FileReader("test.txt");
        int count = 0;
        int len = 0;
        while ((len = fr.read()) != -1) {
            count++;
        }
        fr.close();
        System.out.println("字符个数:" + count);
    }
}

7.2 进阶题

题目3:文件复制 编写程序,实现图片文件的复制功能。

参考答案:

java
public class Exercise03 {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("source.jpg");
        FileOutputStream fos = new FileOutputStream("target.jpg");
        
        byte[] bytes = new byte[1024];
        int len = 0;
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }
        
        fos.close();
        fis.close();
        System.out.println("复制成功!");
    }
}

题目4:学生对象序列化 创建Student类,包含name、age、score属性,将多个Student对象序列化到文件,然后反序列化读取。

参考答案:

java
class Student implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    private double score;

    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{name='" + name + "', age=" + age + ", score=" + score + "}";
    }
}

public class Exercise04 {
    public static void main(String[] args) throws Exception {
        // 序列化
        ArrayList<Student> list = new ArrayList<>();
        list.add(new Student("张三", 18, 90.5));
        list.add(new Student("李四", 19, 85.0));
        list.add(new Student("王五", 20, 92.5));

        ObjectOutputStream oos = new ObjectOutputStream(
            new FileOutputStream("students.dat"));
        oos.writeObject(list);
        oos.close();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(
            new FileInputStream("students.dat"));
        ArrayList<Student> readList = (ArrayList<Student>) ois.readObject();
        for (Student s : readList) {
            System.out.println(s);
        }
        ois.close();
    }
}

7.3 挑战题

题目5:文件内容合并 将多个文本文件的内容合并到一个文件中。

参考答案:

java
public class Exercise05 {
    public static void main(String[] args) throws IOException {
        String[] files = {"file1.txt", "file2.txt", "file3.txt"};
        FileWriter fw = new FileWriter("merged.txt", true);

        for (String file : files) {
            FileReader fr = new FileReader(file);
            char[] chars = new char[1024];
            int len = 0;
            while ((len = fr.read(chars)) != -1) {
                fw.write(chars, 0, len);
            }
            fw.write("\r\n");
            fr.close();
        }

        fw.close();
        System.out.println("合并完成!");
    }
}

题目6:日志记录系统 实现一个简单的日志记录系统,将程序运行信息记录到日志文件中。

参考答案:

java
public class Exercise06 {
    public static void main(String[] args) throws FileNotFoundException {
        PrintStream ps = new PrintStream("system.log");
        System.setOut(ps);

        log("程序启动");
        log("正在加载数据...");
        log("数据处理完成");
        log("程序结束");
    }

    private static void log(String message) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String time = sdf.format(new Date());
        System.out.println("[" + time + "] " + message);
    }
}

八、学习建议

8.1 学习重点

  1. 掌握IO流四大基类: OutputStream、InputStream、Writer、Reader
  2. 理解字节流和字符流的区别: 适用场景、处理单位
  3. 熟练使用文件流: FileInputStream、FileOutputStream、FileReader、FileWriter
  4. 理解序列化机制: Serializable接口、serialVersionUID
  5. 掌握异常处理: try-with-resources语法

8.2 常见错误

  1. 忘记关闭流: 导致资源泄漏
  2. 混淆输入输出: 记住"读入内存是输入,写出内存是输出"
  3. 字符编码问题: 确保编码和解码一致
  4. 序列号冲突: 记得定义固定的serialVersionUID
  5. 缓冲区未刷新: 字符输出流记得flush或close

8.3 最佳实践

  1. 使用try-with-resources: 自动关闭资源
  2. 使用缓冲区: 提高读写效率
  3. 及时关闭流: 释放系统资源
  4. 选择合适的流: 字节流处理二进制,字符流处理文本
  5. 异常处理: 捕获并处理IO异常

8.4 学习路线

IO流基础概念

字节流(FileInputStream/FileOutputStream)

字符流(FileReader/FileWriter)

缓冲流(BufferedReader/BufferedWriter)[后续学习]

转换流(InputStreamReader/OutputStreamWriter)[后续学习]

序列化流(ObjectInputStream/ObjectOutputStream)

打印流(PrintStream/PrintWriter)

NIO(New IO)[高级内容]

附录:IO流体系结构图

IO流
├── 字节流
│   ├── 输入流(InputStream)
│   │   ├── FileInputStream
│   │   ├── ObjectInputStream
│   │   └── BufferedInputStream [后续学习]
│   └── 输出流(OutputStream)
│       ├── FileOutputStream
│       ├── ObjectOutputStream
│       ├── PrintStream
│       └── BufferedOutputStream [后续学习]
└── 字符流
    ├── 输入流(Reader)
    │   ├── FileReader
    │   └── BufferedReader [后续学习]
    └── 输出流(Writer)
        ├── FileWriter
        ├── PrintWriter
        └── BufferedWriter [后续学习]