基本概念
什么是IO
IO 即 Input/Output,输入和输出
- 输入:数据输入到计算机内存的过程
比如 把文件从硬盘读取到内存,从网络读取数据到内存等等
- 输出:数据输出到外部存储(比如数据库,文件,远程主机)的过程
比如 把数据从内存写入到文件,把数据从内存输出到网络等等
所有的程序,在执行的时候,都是在内存上进行的,一旦关机,内存中的数据就没了,那如果想要持久化,就需要把内存中的数据输出到外部
比如说文件
IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流
javaIO流的所有类都是从下面四个抽象类基类派生出来的
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流
不管是文件读写还是网络发送接收,信息的最小存储单元都是字节
流的特性
流的特性
- 先进先出
- 顺序存取:不能随机访问中间的数据(RandomAccessFile除外)
- 只读或只写:在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流
字节流
InputStream(字节输入流)
InputStream用于从源头(通常是文件)读取数据(字节信息)到内存中
java.io.InputStream抽象类是所有字节输入流的父类。
InputStream的常用方法:
- read():返回输入流中下一个字节的数据。如果未读取任何字节,则代码返回 -1 ,表示文件结束。
- read(byte b[ ]) : 从输入流中读取一些字节存储到数组 b 中。
- 如果数组 b 的长度为零,则不读取,
- 如果未读取任何字节,则代码返回 -1 ,表示文件结束。
- 如果有可用字节读取,则最多读取的字节数最多等于 b.length , 返回读取的字节数。这个方法等价于 read(b, 0, b.length)。
- read(byte b[], int off, int len):在read(byte b[ ]) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字节数)。
- skip(long n):忽略输入流中的 n 个字节 ,返回实际忽略的字节数。
- available():返回输入流中可以读取的字节数。
- close():关闭输入流释放相关的系统资源。
- readAllBytes():读取输入流中的所有字节,返回字节数组。
- readNBytes(byte[] b, int off, int len):阻塞直到读取 len 个字节。
- transferTo(OutputStream out):将所有字节从一个输入流传递到一个输出流。
常见使用
- 通过 readAllBytes() 读取输入流所有字节并将其直接赋值给一个 String 对象。
// 新建一个 BufferedInputStream 对象 BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt")); // 读取文件的内容并复制到 String 对象中 String result = new String(bufferedInputStream.readAllBytes()); System.out.println(result); - ObjectInputStream 用于从输入流中读取 Java 对象(反序列化),ObjectOutputStream 用于将对象写入到输出流(序列化)。用于序列化和反序列化的类必须实现 Serializable 接口
ObjectInputStream input = new ObjectInputStream(new FileInputStream("object.data")); MyClass object = (MyClass) input.readObject(); input.close();
OutputStream(字节输出流)
OutputStream用于将数据(字节信息)写入到目的地(通常是文件),java.io.OutputStream抽象类是所有字节输出流的父类。
常用方法:
- write(int b):将特定字节写入输出流。
- write(byte b[ ]) : 将数组b 写入到输出流,等价于 write(b, 0, b.length) 。
- write(byte[] b, int off, int len) : 在write(byte b[ ]) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字节数)。
- flush():刷新此输出流并强制写出所有缓冲的输出字节。
- close():关闭输出流释放相关的系统资源。
数据类型输入输出流(序列化与反序列化)
DataInputStream 提供了一系列可以读基本数据类型的方法
DataOutputStream 提供了一系列可以写基本数据类型的方法
Java IO 还提供了其他一些读写基本数据类型和字符串的流类,包括 ObjectInputStream 和 ObjectOutputStream(用于读写对象)
Java 的序列流(ObjectInputStream 和 ObjectOutputStream)是一种可以将 Java 对象序列化和反序列化的流
推荐使用Kryo 替换:Kryo 是一个优秀的 Java 序列化和反序列化库,具有高性能、高效率和易于使用和扩展等特点,有效地解决了 JDK 自带的序列化机制的痛
其实就是一个序列化与反序列化过程,详见:序列化和反序列化
DataInputStream
// 创建一个 DataInputStream 对象,用于从文件中读取数据
DataInputStream dis = new DataInputStream(new FileInputStream("das.txt"));// 读取一个字节,将其转换为 byte 类型
byte b = dis.readByte();// 读取两个字节,将其转换为 short 类型
short s = dis.readShort();// 读取四个字节,将其转换为 int 类型
int i = dis.readInt();// 读取八个字节,将其转换为 long 类型
long l = dis.readLong();// 读取四个字节,将其转换为 float 类型
float f = dis.readFloat();// 读取八个字节,将其转换为 double 类型
double d = dis.readDouble();// 读取一个字节,将其转换为 boolean 类型
boolean bb = dis.readBoolean();// 读取两个字节,将其转换为 char 类型
char ch = dis.readChar();// 关闭 DataInputStream,释放资源
dis.close();
**DataOutputStream **
// 创建一个 DataOutputStream 对象,用于将数据写入到文件中
DataOutputStream das = new DataOutputStream(new FileOutputStream("das.txt"));// 将一个 byte 类型的数据写入到文件中
das.writeByte(10);// 将一个 short 类型的数据写入到文件中
das.writeShort(100);// 将一个 int 类型的数据写入到文件中
das.writeInt(1000);// 将一个 long 类型的数据写入到文件中
das.writeLong(10000L);// 将一个 float 类型的数据写入到文件中
das.writeFloat(12.34F);// 将一个 double 类型的数据写入到文件中
das.writeDouble(12.56);// 将一个 boolean 类型的数据写入到文件中
das.writeBoolean(true);// 将一个 char 类型的数据写入到文件中
das.writeChar('A');// 关闭 DataOutputStream,释放资源
das.close();
ObjectOutputStream 和 ObjectInputStream
public static void main(String[] args) {try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.dat"))) {Person p = new Person("张三", 20);oos.writeObject(p);} catch (IOException e) {e.printStackTrace();}try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.dat"))) {Person p = (Person) ois.readObject();System.out.println(p);} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}
}
字符流
I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
字符流默认采用的是 Unicode 编码
InputStreamReader / OutputStreamWriter 是字符流转换为字节流的桥梁
Reader(字符输入流)
Reader用于从源头(通常是文件)读取数据(字符信息)到内存中,java.io.Reader抽象类是所有字符输入流的父类
Reader 用于读取文本, InputStream 用于读取原始字节。
Reader 常用方法:
- read() : 从输入流读取一个字符。
- read(char[] cbuf) : 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中,等价于 read(cbuf, 0, cbuf.length) 。
- read(char[] cbuf, int off, int len):在read(char[] cbuf) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。
- skip(long n):忽略输入流中的 n 个字符 ,返回实际忽略的字符数。
- close() : 关闭输入流并释放相关的系统资源。
Writer(字符输出流)
Writer用于将数据(字符信息)写入到目的地(通常是文件),java.io.Writer抽象类是所有字符输出流的父类。
Writer 常用方法:
- write(int c) : 写入单个字符。
- write(char[] cbuf):写入字符数组 cbuf,等价于write(cbuf, 0, cbuf.length)。
- write(char[] cbuf, int off, int len):在write(char[] cbuf) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。
- write(String str):写入字符串,等价于 write(str, 0, str.length()) 。
- write(String str, int off, int len):在write(String str) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。
- append(CharSequence csq):将指定的字符序列附加到指定的 Writer 对象并返回该 Writer 对象。
- append(char c):将指定的字符附加到指定的 Writer 对象并返回该 Writer 对象。
- flush():刷新此输出流并强制写出所有缓冲的输出字符
- close():关闭输出流释放相关的系统资源。
字节缓冲流
字节缓冲流这里采用了装饰器模式来增强 InputStream 和OutputStream子类对象的功能。
IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。
字节流和字节缓冲流的性能差别主要体现:
- 调用 write(int b) 和 read() 这两个一次只读取
一个字节的方法的时候,字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率 - 调用 read(byte b[]) 和 write(byte b[], int off, int len) 这两个写入一个
字节数组的方法的话,只要字节数组的大小合适,两者的性能差距其实不大,基本可以忽略
BufferedInputStream(字节缓冲输入流)
BufferedInputStream 从源头(通常是文件)读取数据(字节信息)到内存的过程中,会先将读取到的字节存放在缓存区,并从内部缓冲区中单独读取字节。 大幅减少了 IO 次数,提高了读取效率。
内部维护了一个缓冲区,这个缓冲区实际就是一个字节数组
- 缓冲区的大小默认为 8192 字节
- 可以通过 BufferedInputStream(InputStream in, int size) 这个构造方法来指定缓冲区的大小
BufferedOutputStream(字节缓冲输出流)
BufferedOutputStream 将数据(字节信息)写入到目的地(通常是文件)的过程中,会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了效率
内部维护了一个缓冲区,这个缓冲区实际就是一个字节数组
- 缓冲区的大小默认为 8192 字节
- 可以通过 BufferedOutputStream (outputStream in, int size) 这个构造方法来指定缓冲区的大小
字符缓冲流BufferedReader /BufferedWriter
BufferedReader (字符缓冲输入流)和 BufferedWriter(字符缓冲输出流)类似于 BufferedInputStream(字节缓冲输入流)和BufferedOutputStream(字节缓冲输入流),
内部都维护了一个字节数组作为缓冲区。不过,主要是用来操作字符信息
字节字符 转换流
使用转换流可以方便地在字节流和字符流之间进行转换。
- 在进行文本文件读写时,通常使用字符流进行操作,
- 而在进行网络传输或与设备进行通信时,通常使用字节流进行操作
InputStreamReader 是从字节流到字符流的桥连接,它使用指定的字符集读取字节并将它们解码为字符
InputStreamReader isr = new InputStreamReader(new FileInputStream("demo.txt"));
OutputStreamWriter 将一个字符流的输出对象变为字节流的输出对象,是字符流通向字节流的桥梁
// 创建一个 File 对象 f,表示文件 test.txt
File f = new File("test.txt");// 创建一个 OutputStreamWriter 对象 out,使用 FileOutputStream 对象将数据写入到文件 f 中,并将字节流转换成字符流
Writer out = new OutputStreamWriter(new FileOutputStream(f));
Piped(管道)
Java 中,通信的双方必须在同一个进程中,也就是在同一个 JVM 中,管道为线程之间的通信提供了通信能力。
使用管道流可以实现不同线程之间的数据传输,
可以用于线程间的通信、数据的传递等。
但是,管道流也有一些局限性,比如只能在同一个 JVM 中的线程之间使用,不能跨越不同的 JVM 进程
一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的 PipedInputStream 读取出来
示例如下
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;public class PipedStreamExample {public static void main(String[] args) throws IOException {// 创建管道输入流和输出流PipedOutputStream out = new PipedOutputStream();PipedInputStream in = new PipedInputStream(out);// 写数据的线程Thread writerThread = new Thread(() -> {try {String data = "Hello from PipedOutputStream!";out.write(data.getBytes());out.close();} catch (IOException e) {e.printStackTrace();}});// 读数据的线程Thread readerThread = new Thread(() -> {try {int data;while ((data = in.read()) != -1) {System.out.print((char) data);}in.close();} catch (IOException e) {e.printStackTrace();}});// 启动线程writerThread.start();readerThread.start();}
}
打印流PrintStream
如下
System.out.print("Hello!");
System.out.println("Hello!");
System.out 实际是用于获取一个 PrintStream 对象,print方法实际调用的是 PrintStream 对象的 write 方法。
PrintStream 属于字节打印流,与之对应的是 PrintWriter (字符打印流)
PrintStream 是 OutputStream 的子类,PrintWriter 是 Writer 的子类
文件流
File
java.io.File 类是专门对文件进行操作的类,注意只能对文件本身进行操作,不能对文件内容进行操作
想要操作内容,必须借助输入输出流
随机访问流RandomAccessFile
基本概念
RandomAccessFile 比较常见的一个应用就是实现大文件的 断点续传
上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。
分片(先将文件切分成多个文件分片)上传是断点续传的基础。
RandomAccessFile :支持随意跳转到文件的任意位置进行读写
RandomAccessFile 的构造方法
// openAndDelete 参数默认为 false 表示打开文件并且这个文件不会被删除
public RandomAccessFile(File file, String mode)throws FileNotFoundException {this(file, mode, false);
}
// 私有方法
private RandomAccessFile(File file, String mode, boolean openAndDelete) throws FileNotFoundException{// 省略大部分代码
}
读写模式
- r : 只读模式。
- rw: 读写模式
- rws: 相对于 rw,rws 同步更新对“文件的内容”或“元数据”的修改到外部存储设备。
- rwd : 相对于 rw,rwd 同步更新对“文件的内容”的修改到外部存储设备
文件内容指的是文件中实际保存的数据
元数据则是用来描述文件属性比如文件的大小信息、创建和修改时间。
可以通过 RandomAccessFile 的 seek(long pos) 方法来设置文件指针的偏移量(距文件开头 pos 个字节处)
指针用来表示下一个将要被写入或者读取的字节所处的位置
解决大文件上传问题
RandomAccessFile 能有效解决大文件上传的核心痛点,结合 分片+断点续传+并行,保障上传的 高效性 和 可靠性。
- 文件分片处理
- 将文件按 固定大小(如5MB) 切分,降低单次上传内存压力(避免OOM)。
- 前端通过 Blob.slice 方法切割文件,后端使用 RandomAccessFile 按偏移量写入文件块
- 断点续传机制
- 记录已上传分片:服务端保存分片索引(如Redis存储),中断后仅上传缺失部分。
- 进度恢复:前端通过本地存储(LocalStorage)或服务端查询已传进度。
- 唯一标识与秒传:文件Hash计算:使用 MD5/SHA256 计算文件指纹,上传前校验服务端是否存在完整文件,实现秒传。
- 并发与错误处理
- 并行上传控制:限制并发数(如3-5线程),防止带宽占满。
- 分片重试策略:单个分片上传失败后自动重试(最多3次)。
后端
// 分片写入逻辑(RandomAccessFile)
File targetFile = new File("path/to/file");
try (RandomAccessFile raf = new RandomAccessFile(targetFile, "rw")) {raf.seek(chunkIndex * CHUNK_SIZE); // 定位分片偏移量raf.write(chunkData); // 写入分片数据
}
前端:React示例
// 分片上传函数
const uploadChunk = async (chunk, index, fileHash) => {const formData = new FormData();formData.append('chunk', chunk);formData.append('index', index);formData.append('hash', fileHash);await axios.post('/upload', formData);
};// 并发控制(p-limit)
const limitConcurrency = limit(3); // 限制3个并发
chunks.forEach(chunk => {limitConcurrency(() => uploadChunk(chunk));
});
IO涉及的设计模式
装饰器(Decorator)模式 :详细见这篇博客
- FilterInputStream (对应输入流)和FilterOutputStream(对应输出流)是
装饰器模式的核心,分别用于增强 InputStream 和OutputStream子类对象的功能 - BufferedInputStream(字节缓冲输入流)、DataInputStream 等等都是FilterInputStream 的子类,BufferedOutputStream(字节缓冲输出流)、DataOutputStream等等都是FilterOutputStream的子类。
适配器(Adapter Pattern)模式:详细见这篇博客,用于接口互不兼容的类的协调工作
- 通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。
- InputStreamReader 和 OutputStreamWriter 就是两个适配器(Adapter)
- InputStreamReader 使用 StreamDecoder (流解码器)对字节进行解码,实现字节流到字符流的转换
- OutputStreamWriter 使用StreamEncoder(流编码器)对字符进行编码,实现字符流到字节流的转换。
- InputStream 和 OutputStream 的子类是被适配者, InputStreamReader 和 OutputStreamWriter是适配器
// InputStreamReader 是适配器,FileInputStream 是被适配的类
InputStreamReader isr = new InputStreamReader(new FileInputStream(fileName), "UTF-8");
// BufferedReader 增强 InputStreamReader 的功能(装饰器模式)
BufferedReader bufferedReader = new BufferedReader(isr);
工厂模式用于创建对象,可看这篇博客
- NIO 中大量用到了工厂模式,
- 比如 Files 类的 newInputStream 方法用于创建 InputStream 对象(静态工厂)、
- Paths 类的 get 方法创建 Path 对象(静态工厂)、
- ZipFileSystem 类(sun.nio包下的类,属于 java.nio 相关的一些内部实现)的 getPath 的方法创建 Path 对象(简单工厂)。
观察者模式:可看这篇博客,NIO 中的文件目录监听服务使用到了观察者模式。
- NIO 中的文件目录监听服务基于 WatchService 接口和 Watchable 接口。
- WatchService 属于观察者:WatchService 用于监听文件目录的变化,同一个 WatchService 对象能够监听多个文件目录
- Watchable 属于被观察者:Watchable 接口定义了一个用于将对象注册到 WatchService(监控服务) 并绑定监听事件的方法 register
IO 模型
详细见:这篇博客