Java IO流详解
Java IO流详解
一、IO 是什么?
IO 就是 内存与硬盘的数据传输通道。
直白解释就是读和写
- 输入流 Input(读)
硬盘里的文件数据,通过IO通道流入程序内存,程序才能拿到文件内容。 - 输出流 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位字符'中'。
一般使用:
- 图片、视频、压缩包 → 只能用字节流
- 只读写文字、要正常显示中文 → 优先字符流
一句话总结:字符流是“带翻译的字节流”
注:字符流底层还是字节流,只是包装了文字翻译功能
或者实际上说:”万物皆字节“
三、相对路径 & 绝对路径
前置底层知识
操作系统读取文件时,必须提供文件唯一定位地址,否则系统不知道去哪找文件,路径就是文件的“地址”。
Java中File、IO流底层都会把路径交给操作系统API,操作系统根据路径定位磁盘文件。
1. 绝对路径:
------从磁盘根目录开始的完整唯一地址
原理:操作系统不需要任何推导,直接精准定位文件。
从磁盘根分区开始完整、无歧义的完整地址,包含:盘符/根目录、所有父文件夹、文件名。
操作系统拿到路径可以直接定位文件,不需要额外推导。
系统示例
Windows:D:/JavaProject/io/demo/test.txt(从D盘根目录开始)
Mac/Linux:/Users/student/code/test.txt(从系统根目录/开始)
作用
- 固定定位文件,无论程序在哪运行,都能精准找到文件,
- 适合工具类、固定配置文件、本地固定资源
优缺点
- 优点:不会出现找不到文件(FileNotFoundException),定位稳定
- 缺点:移植性极差,换电脑、换磁盘分区路径直接失效;代码耦合本地环境
2. 相对路径
------不以盘符开头,以「程序当前运行目录」为基准自动拼接路径
原理:JVM 自动补全路径,最终依然会变成绝对路径去访问文件。
不写完整地址,以「程序当前工作目录」为基准,向后推导文件位置。
程序启动时JVM会自动生成一个「当前工作目录」,相对路径会拼接这个基准目录,生成完整绝对路径再交给系统。
基准目录规则(新手必懂)
- IDEA/Eclipse直接运行main方法:基准目录 = 项目根文件夹
- 打包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
作用
- 项目整体打包迁移、发给别人运行,不用修改文件地址
- 项目资源统一放在项目内部,规范管理
优缺点
- 优点:移植性强,项目整体复制到其他电脑也能正常运行
- 缺点:基准目录改变就会找不到文件;不同运行环境基准目录不一致,容易报错
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步:
- 确定文件路径(数据源/保存位置)
- 创建流对象,打通内存和文件的通道
- 调用read()读取 / write()写入数据
- 关闭流close(),释放系统文件资源
小优化:JDK7及以上 try-with-resources 写法,代码执行完自动关流,不用手动写close。
五、常用流简单介绍
1. 基础文件流(直接操作硬盘)
- 字节流:
FileInputStream读文件、FileOutputStream写文件 - 字符流:
FileReader/FileWriter
缺点:字符基础流使用电脑系统默认编码,Windows是GBK,读取UTF-8中文文件会乱码,新手不推荐直接用。
2. 转换流(解决中文乱码核心)
InputStreamReader、OutputStreamWriter
作用:字节流 ↔ 字符流的转换桥梁,可以手动指定UTF-8编码,彻底解决中文乱码。
标准文本读写链路图:
硬盘文件 → FileInputStream(字节通道) → InputStreamReader(转字符+UTF-8) → 程序内存
程序内存 → OutputStreamWriter(转字节+UTF-8) → FileOutputStream(字节通道) → 硬盘文件
3. 缓冲流(提速首选,最常用)
内置内存缓冲区,批量读写数据,减少硬盘访问次数,读写速度更快。
- 字节缓冲:
BufferedInputStream/BufferedOutputStream - 字符缓冲:
BufferedReader/BufferedWriter
独有便利功能:BufferedReader.readLine()一次性读取一整行文字,处理文本超简单。
4. 对象流
ObjectInputStream、ObjectOutputStream:把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 {}
因此:
- 只有类实现
Serializable,JVM才会生成该类对象的序列化二进制转换逻辑; - 未实现该接口直接序列化,JVM抛出
NotSerializableException; private static final long serialVersionUID:序列化版本号- 底层作用:反序列化时校验文件二进制与当前类版本是否匹配;
- 类新增/删除属性后,版本号不变可以正常反序列化,否则抛版本异常。
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();
}
}
八、新手核心坑点底层原理总结
- FileReader中文乱码底层原因
FileReader默认使用操作系统的系统编码(Windows GBK),解码规则和文件UTF-8二进制不匹配,翻译文字出错;必须使用转换流手动指定UTF-8编码。 - 相对路径找不到文件
程序启动的基准工作目录发生变化,JVM拼接出的完整地址错误,优先使用绝对路径调试。 - 序列化报错 NotSerializableException
实体类没有实现Serializable标记接口,JVM没有生成对象二进制转换逻辑。 - 关闭顺序错误文件空白
输出流缓冲区数据驻留内存,未刷新写入磁盘就关闭输入流,程序直接退出,缓冲区数据丢失。 - 字符流读取图片损坏
字符流会对图片二进制进行编码解码转换,篡改原始二进制数据,图片彻底无法打开。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)