Java IO流详解

一、IO 是什么?

IO 就是 内存与硬盘的数据传输通道。

直白解释就是读和写

  1. 输入流 Input(读)
    硬盘里的文件数据,通过IO通道流入程序内存,程序才能拿到文件内容。
  2. 输出流 Output(写)
    程序内存里的文字/数据,通过IO通道流出到硬盘,保存成文件。
【外部文件/硬盘】 ←——(输出流OutputStream)——→ 【程序内存】
【外部文件/硬盘】 ——(输入流InputStream)——→ 【程序内存】

计算机只有两种存储:

  • 硬盘:永久存储,存的是 0101 二进制字节
  • 内存:临时存储,程序运行结束数据消失

IO 流就是连接两者的数据管道:

  • 输入流:硬盘 → 内存(读文件)
  • 输出流:内存 → 硬盘(写文件)
    IO实际上就像一根管子,起到了运输作用。
    注:控制台打印System.out.println()也是输出流,数据流向显示器。
    对应你笔记的流转关系:内存 Memory ↔ 文件 Files ↔ 显示器 display

二、字节流和字符流

                IO所有流顶层父类
    ┌───────────────┴───────────────┐
字节流(所有二进制文件通用)      字符流(只处理纯文本)
InputStream(输入)    OutputStream(输出)    Reader(输入)    Writer(输出)
最小单位:1字节(8位二进制)            最小单位:1字符(专门处理文字)
适用:图片、视频、压缩包、exe程序      适用:txt、java、md等文字文档
特点:不处理文字编码,不会乱码/损坏文件 特点:自带编码转换,中文友好
1)字节流 InputStream / OutputStream

本质
直接读写硬盘原生二进制数据,不做任何处理、不做编码转换。直接操作原始二进制字节,直接读写硬盘上原生的 8 位二进制数据。
最小操作单位:1 byte = 8 bit

  • 底层行为:读取硬盘原生二进制,原封不动放入内存;写入时直接把内存二进制原样刷入硬盘。
  • 编码逻辑:完全不涉及字符编码,没有解码、编码转换过程。

能处理:图片、视频、压缩包、文本、exe 所有文件,同时因为无编码、无转换所以不破坏文件。
例:
硬盘中文 “中” 存储二进制:11100100 10111000 10101101(UTF-8 三字节)
字节流读取:直接把这 3 个字节原封不动读到内存,不会翻译成文字。

2)字符流 Reader / Writer

底层本质

封装了字节流+编码转换器,底层依然靠字节流读写二进制,只是自动增加「编码/解码」转换逻辑。(底层依然读字节)

  • 最小操作单位:1 char = 16 bit(Java内部字符统一使用UTF-16存储)
  • 底层行为:
    读文件(解码):硬盘字节 → 字节流读取 → 根据指定编码翻译成Java内部16位字符
    写文件(编码):Java16位字符 → 根据指定编码转为硬盘字节 → 字节流写入硬盘
  • 编码逻辑:必须经过编码转换,是字节流的上层封装。
    例:
    读取硬盘“中”的3个UTF-8字节:
    字符流自动执行解码:3字节二进制 → 转为Java内存中16位字符'中'

一般使用:

  1. 图片、视频、压缩包 → 只能用字节流
  2. 只读写文字、要正常显示中文 → 优先字符流

一句话总结:字符流是“带翻译的字节流”
注:字符流底层还是字节流,只是包装了文字翻译功能
或者实际上说:”万物皆字节“

三、相对路径 & 绝对路径

前置底层知识

操作系统读取文件时,必须提供文件唯一定位地址,否则系统不知道去哪找文件,路径就是文件的“地址”。
Java中File、IO流底层都会把路径交给操作系统API,操作系统根据路径定位磁盘文件。

1. 绝对路径:

------从磁盘根目录开始的完整唯一地址
原理:操作系统不需要任何推导,直接精准定位文件。
磁盘根分区开始完整、无歧义的完整地址,包含:盘符/根目录、所有父文件夹、文件名。
操作系统拿到路径可以直接定位文件,不需要额外推导。

系统示例

Windows:D:/JavaProject/io/demo/test.txt(从D盘根目录开始)
Mac/Linux:/Users/student/code/test.txt(从系统根目录/开始)

作用

  1. 固定定位文件,无论程序在哪运行,都能精准找到文件
  2. 适合工具类、固定配置文件、本地固定资源

优缺点

  • 优点:不会出现找不到文件(FileNotFoundException),定位稳定
  • 缺点:移植性极差,换电脑、换磁盘分区路径直接失效;代码耦合本地环境

2. 相对路径

------不以盘符开头,以「程序当前运行目录」为基准自动拼接路径
原理:JVM 自动补全路径,最终依然会变成绝对路径去访问文件。
不写完整地址,以「程序当前工作目录」为基准,向后推导文件位置
程序启动时JVM会自动生成一个「当前工作目录」,相对路径会拼接这个基准目录,生成完整绝对路径再交给系统。

基准目录规则(新手必懂)
  1. IDEA/Eclipse直接运行main方法:基准目录 = 项目根文件夹
  2. 打包jar运行:基准目录 = jar包所在文件夹
示例

项目根目录为D:/JavaProject/io/demo
写相对路径test.txt → JVM自动拼接为完整路径:D:/JavaProject/io/demo/test.txt
写相对路径doc/hello.txt → 完整路径:D:/JavaProject/io/demo/doc/hello.txt

作用

  1. 项目整体打包迁移、发给别人运行,不用修改文件地址
  2. 项目资源统一放在项目内部,规范管理

优缺点

  • 优点:移植性强,项目整体复制到其他电脑也能正常运行
  • 缺点:基准目录改变就会找不到文件;不同运行环境基准目录不一致,容易报错

3. 代码区分演示

import java.io.File;

public class PathTest {
    public static void main(String[] args) {
        // 1. 绝对路径(Windows完整磁盘地址)
        File absFile = new File("D:/JavaIO/test.txt");
        // 打印系统最终读取的完整地址
        System.out.println("绝对路径完整地址:" + absFile.getAbsolutePath());

        // 2. 相对路径(以项目根目录为基准)
        File relFile = new File("test.txt");
        System.out.println("相对路径解析后完整地址:" + relFile.getAbsolutePath());
    }
}

四、所有流统一4步操作

不管字节、字符流,读写文件永远固定4步:

  1. 确定文件路径(数据源/保存位置)
  2. 创建流对象,打通内存和文件的通道
  3. 调用read()读取 / write()写入数据
  4. 关闭流close(),释放系统文件资源

小优化:JDK7及以上 try-with-resources 写法,代码执行完自动关流,不用手动写close。

五、常用流简单介绍

1. 基础文件流(直接操作硬盘)

  • 字节流:FileInputStream读文件、FileOutputStream写文件
  • 字符流:FileReader/FileWriter

缺点:字符基础流使用电脑系统默认编码,Windows是GBK,读取UTF-8中文文件会乱码,新手不推荐直接用。

2. 转换流(解决中文乱码核心)

InputStreamReaderOutputStreamWriter
作用:字节流 ↔ 字符流的转换桥梁,可以手动指定UTF-8编码,彻底解决中文乱码。
标准文本读写链路图:

硬盘文件 → FileInputStream(字节通道) → InputStreamReader(转字符+UTF-8) → 程序内存
程序内存 → OutputStreamWriter(转字节+UTF-8) → FileOutputStream(字节通道) → 硬盘文件

3. 缓冲流(提速首选,最常用)

内置内存缓冲区,批量读写数据,减少硬盘访问次数,读写速度更快。

  • 字节缓冲:BufferedInputStream / BufferedOutputStream
  • 字符缓冲:BufferedReader / BufferedWriter
    独有便利功能:BufferedReader.readLine() 一次性读取一整行文字,处理文本超简单。

4. 对象流

ObjectInputStreamObjectOutputStream:把Java对象保存到文件。
硬性要求:实体类必须实现Serializable接口。

例:

案例1:读取文本文件(UTF-8无乱码,字符缓冲流)

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;

public class ReadTextDemo {
    public static void main(String[] args) throws Exception {
        // 1. 搭建通道:文件→字节流→转换流(指定UTF-8)→缓冲流
        BufferedReader br = new BufferedReader(
                new InputStreamReader(new FileInputStream("test.txt"), "UTF-8")
        );
        String line;
        // 2. 循环一行一行读取文字
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
        // 3. 关闭通道
        br.close();
    }
}

使用说明:在项目根目录新建test.txt,写入中文,运行不会乱码。

案例2:写入文本文件(追加文字,不会覆盖原有内容)

import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;

public class WriteTextDemo {
    public static void main(String[] args) throws Exception {
        // new FileOutputStream(路径, true) true代表追加写入
        BufferedWriter bw = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream("out.txt", true), "UTF-8")
        );
        bw.write("今天学习Java IO流");
        bw.newLine(); // 自动换行,跨平台兼容
        bw.flush(); // 把缓冲区数据写入文件
        bw.close();
    }
}

案例3:字节流复制图片(支持所有文件,通用)

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class CopyImgDemo {
    public static void main(String[] args) throws Exception {
        // 输入通道:读取原图
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("1.jpg"));
        // 输出通道:写入新图片
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.jpg"));

        byte[] tempBox = new byte[1024]; // 临时缓存容器,批量传输数据
        int length;
        while ((length = bis.read(tempBox)) != -1) {
            bos.write(tempBox, 0, length);
        }
        bis.close();
        bos.close();
    }
}

使用说明:项目下放一张名为1.jpg的图片,运行后生成复制后的图片。

六、流的打开关闭规则:先开后开、后开先关

底层原理

IO流会向操作系统申请文件句柄(文件资源占用标识),操作系统能同时打开的句柄数量有限,不关闭会导致资源泄漏。
同时缓冲流有内存缓冲区,缓冲区数据存在内存,没有写入磁盘,关闭顺序错误会导致数据丢失。

1. 标准打开顺序

先开输入流,后开输出流
逻辑:先读取硬盘原始数据,再写入新文件,符合文件复制/读取写入的业务逻辑。

// 第一步:打开输入流(占用源文件句柄)
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("src.jpg"));
// 第二步:打开输出流(占用目标文件句柄)
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.jpg"));

2. 标准关闭顺序:后开先关

后创建的流,优先关闭;先创建的流,最后关闭
底层原因:输出流存在缓冲区,如果先关输入流,程序直接结束,输出缓冲区残留数据无法刷新到磁盘,文件空白、数据丢失。

// 读写操作省略
bos.close(); // 后打开的输出流,先关闭(自动flush缓冲区,数据落盘)
bis.close(); // 先打开的输入流,最后关闭

3. 优化方案:try-with-resources

JDK7新增,实现AutoCloseable接口的流会自动逆序关闭,不用手动管理顺序。


七、序列化 & 反序列化 + Serializable接口(底层原理)

1. 底层本质原理

为什么需要序列化?
Java对象存放在JVM内存中,程序停止后直接销毁,无法持久保存到硬盘。

  • 序列化:JVM把内存中对象的成员变量数据,转换成硬盘可存储的二进制字节序列,写入文件持久保存。
    对象内存数据 → 硬盘二进制(保存对象)
  • 反序列化:读取硬盘二进制字节序列,重新在JVM内存中还原出完整Java对象。
    硬盘二进制 → 恢复成内存对象(读取对象)

2. Serializable标记接口底层作用

Serializable 接口到底是什么?

public interface Serializable {}

接口内部没有任何抽象方法,是标记型接口
底层原理:
JVM 默认不会帮你保存对象。
只有标记了 Serializable,JVM 才会:

  • 允许该类对象转为二进制
  • 允许持久化、网络传输

没写直接报错:NotSerializableException

public interface Serializable {}

因此:

  1. 只有类实现Serializable,JVM才会生成该类对象的序列化二进制转换逻辑;
  2. 未实现该接口直接序列化,JVM抛出NotSerializableException
  3. private static final long serialVersionUID:序列化版本号
    • 底层作用:反序列化时校验文件二进制与当前类版本是否匹配;
    • 类新增/删除属性后,版本号不变可以正常反序列化,否则抛版本异常。
  4. transient关键字修饰的成员变量:序列化时JVM直接忽略该字段,不会写入二进制,读取后为默认值。

3. 极简完整案例

步骤1:实体类实现Serializable

import java.io.Serializable;

// 标记接口,告诉JVM该类支持序列化
public class User implements Serializable {
    // 手动定义序列化版本号,避免类修改后反序列化报错
    private static final long serialVersionUID = 1L;

    private String name;
    // transient修饰,不会序列化保存
    private transient int tempNum;

    public User(String name, int tempNum) {
        this.name = name;
        this.tempNum = tempNum;
    }

    @Override
    public String toString() {
        return "name=" + name + ",临时变量tempNum=" + tempNum;
    }
}

步骤2:序列化(对象写入文件,持久化)

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class SerialTest {
    public static void main(String[] args) throws Exception {
        // 相对路径:项目根目录生成user.obj文件
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.obj"));
        User user = new User("张三", 999);
        // 将对象转为二进制写入文件
        oos.writeObject(user);
        oos.close();
    }
}

步骤3:反序列化(读取二进制还原对象)

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class DeserialTest {
    public static void main(String[] args) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.obj"));
        // 读取二进制,还原内存对象
        User user = (User) ois.readObject();
        // 输出:name=张三,临时变量tempNum=0(transient字段不保存,默认值0)
        System.out.println(user);
        ois.close();
    }
}
例:

案例1:字符流读取UTF-8文本(演示编码转换底层)

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;

public class ReadTxt {
    public static void main(String[] args) throws Exception {
        // 相对路径,基准目录为项目根目录
        BufferedReader br = new BufferedReader(
                new InputStreamReader(new FileInputStream("test.txt"), "UTF-8")
        );
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
        br.close();
    }
}

案例2:字节流复制图片(演示纯二进制传输、先开后关)

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class CopyImage {
    public static void main(String[] args) throws Exception {
        // 先开输入流
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("src.jpg"));
        // 后开输出流
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.jpg"));

        byte[] buffer = new byte[1024];
        int len;
        while ((len = bis.read(buffer)) != -1) {
            bos.write(buffer, 0, len);
        }

        // 后开先关
        bos.close();
        bis.close();
    }
}

案例3:追加写入文本文件

import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;

public class WriteTxt {
    public static void main(String[] args) throws Exception {
        // 第二个参数true:开启追加模式,不覆盖原有二进制数据
        BufferedWriter bw = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream("out.txt", true), "UTF-8")
        );
        bw.write("底层区分字节流和字符流");
        bw.newLine();
        bw.flush();
        bw.close();
    }
}

八、新手核心坑点底层原理总结

  1. FileReader中文乱码底层原因
    FileReader默认使用操作系统的系统编码(Windows GBK),解码规则和文件UTF-8二进制不匹配,翻译文字出错;必须使用转换流手动指定UTF-8编码。
  2. 相对路径找不到文件
    程序启动的基准工作目录发生变化,JVM拼接出的完整地址错误,优先使用绝对路径调试。
  3. 序列化报错 NotSerializableException
    实体类没有实现Serializable标记接口,JVM没有生成对象二进制转换逻辑。
  4. 关闭顺序错误文件空白
    输出流缓冲区数据驻留内存,未刷新写入磁盘就关闭输入流,程序直接退出,缓冲区数据丢失。
  5. 字符流读取图片损坏
    字符流会对图片二进制进行编码解码转换,篡改原始二进制数据,图片彻底无法打开。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐