概述
IO是Input(输入)和Output(输出)的首字母缩写。
I(输入Input):指向Java程序中输入数据,即Java程序从外部获取数据。
O(输出Output):指的是Java程序向外部输出数据,即Java程序向外部发送数据。
流:**在Java程序和外部之间,数据像水流一样按照顺序传输。Java中,流有两种形式,字节流和字符流。
外部(也就是数据源)包括:源设备 和 目标设备。
源设备:Java程序使用Input(输入)获取数据的来源。
目标设备:Java程序使用Output(输出)发送数据的目的地。
作用
Java IO用在Java程序和外部进行数据交互,Java程序运行在内存中,要与外部(如:磁盘、网络、数据库等)地方交互数据则需要使用Java IO。
比如:在本地磁盘的某个txt文件上读写运行日志、读写MySQL数据库的内容等。
IO分类
Java中,**流有两种形式,字节流和字符流。**所对照的四大抽象类分别为:InputStream、OutputStream 和 Reader、Writer。这四个抽象类一般不进行实例化,使用的时候一般通过它们的子类调用继承自父类的方法。IO完成之后,一般要使用close方法,否则会造成不必要的资源浪费和卡顿麻烦。
字节流和字符流的区别
- 字节流读取单个字节,字符流读取单个字符(一个字符根据编码的不同,对应的字节也不同,如 UTF-8 编码中文汉字是 3 个字节,GBK编码中文汉字是 2 个字节。)
- 字节流用来处理二进制文件(图片、MP3、视频文件),字符流用来处理文本文件(可以看做是特殊的二进制文件,使用了某种编码,人可以阅读)。
字节流
字节流下有两大抽象类:InputStream、OutputStream。
字符流
字符流下有两大抽象类:Reader、Writer
字节流转字符流
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
- GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
- UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
- UTF-16be 编码中,中文字符和英文字符都占 2 个字节。
Java 使用双字节编码 UTF-16be,是指的char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节。UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
数据来源/操作对象进行划分
文件(file)
FileInputStream、FileOutputStream、FileReader、FileWriter
数组
- 字节数组(byte[]): ByteArrayInputStream、ByteArrayOutputStream
- 字符数组(char[]): CharArrayReader、CharArrayWriter
管道通讯
PipedInputStream、PipedOutputStream、PipedReader、PipedWriter
**操作有原子性,可以实现普通文件的操作原子性。可以以流方式操作的文件,具备缓冲特性。遵循先入先出原则。**在Linux系统中,管道是进程间通信的媒介。管道是内核里面的一串缓存,通过管道的文件描述符可以找到它,所以拿到管道的文件描述符的进程之间就可以通信。
基本数据类型
DataInputStream、DataOutputStream 基本数据类型的字节输入输出流。
缓冲操作(按照缓冲处理数据)
BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
打印(按照格式化输出)
PrintStream、PrintWriter
对象序列化/反序列化
ObjectInputStream、ObjectOutputStream
日常使用直接实现序列化接口Serializable。serialVersionUID
的字段。这是一个版本控制机制,用于在反序列化过程中检查序列化的类和反序列化的类是否兼容。在生产环境中,为每个可序列化的类分配唯一的 serialVersionUID 是一个好习惯。
实例对象为什么要实现序列化接口?
序列化就是对实例对象的状态(State 对象属性而不包括对象方法)进行通用编码(如格式化的字节码)并保存,以保证对象的完整性和可传递性。
简而言之:序列化,就是为了在不同时间或不同平台的JVM之间共享实例对象,尤其是避免中文乱码问题。
转换(字节字符转换流)
InputStreamReader、OutputStreamWriter 把字节流转换成字符流.
注意:
**当不涉及到网路传入或其他场景时,可以通过字符流中的另外两个子类,FileReader和FileWriter,直接把数据读成字符流;**因为使用字节流,操作中文的话,会出现乱码问题。
IO设计模式
装饰器模式
装饰器模式就是不改变接口,但加入责任。
场景:广泛应用于 FilterInputStream、FilterOutputStream、FilterReader 和 FilterWriter 这些类。这些类充当装饰器角色,可以包装其他流对象并为它们提供额外的功能,如缓冲、数据处理等。
适配器模式
将一个接口转换为另一个接口
场景:字节流转字符流
观察者模式
当每一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。
场景:信号渠道I/O类似于观察者模式,内核就是个观察者,信号回调就是个异步通知,用户进程发起I/O操作时,通过系统调用sigaction函数,在对于的套接字注册一个信号回调,此时不阻塞用户进程,当内核数据准备就绪时,内核会为该进程生成SIGIO信号,通过信号回调通知进行I/O操作。
IO常见操作类
Java 的 I/O 大概可以分成以下几类:
- 磁盘操作: File
- 字节操作: InputStream 和 OutputStream
- 字符操作: Reader 和 Writer
- 对象操作: Serializable
- 网络操作: Socket
File相关
File 类可以用于表示文件和目录的信息,但是它不表示文件的内容
序列化 & Serializable & transient
序列化就是将一个对象转换成字节序列,方便存储和传输。
- 序列化: ObjectOutputStream.writeObject()
- 反序列化: ObjectInputStream.readObject()
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
Serializable
序列化的类需要实现 Serializable 接口,它只是一个标准,但是如果不去实现它的话而进行序列化,会抛出异常。
transient
transient 关键字可以使一些属性不会被序列化。
ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为数组是动态扩展的,是按照1.5倍扩展,有部分空间并未使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
Java 中的网络支持:
- InetAddress: 用于表示网络上的硬件资源,即 IP 地址;
- URL: 统一资源定位符;
- Sockets: 使用 TCP 协议实现网络通信;
- Datagram: 使用 UDP 协议实现网络通信。
InetAddress
没有公有的构造函数,只能通过静态方法来创建实例。
URL
可以直接从 URL 中读取字节流数据。
Sockets
- ServerSocket: 服务器端类
- Socket: 客户端类
- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
Datagram
- DatagramSocket: 通信类
- DatagramPacket: 数据包类
IO模型
介绍
一个输入操作通常包括两个阶段:
- 等待数据准备好
- 从内核向进程复制数据
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
IO模型一共有5种:同步阻塞I/O、同步非阻塞I/O,I/O多路复用、信号驱动I/O和异步I/O。这也是常用的5种IO模型。
Java中的NIO是在Java 1.4引入,对应java.io包,提供了Channel,Selector,Buffer等抽象。NIO中的N理解为Non-blocking,不单纯是New,它支持面向缓冲,基于通道的I/O操作方法,对于高负载、高并发的网络与应用,应使用NIO。Java中的NIO可以看作为是I/O多路由复用。也有人认为,Java中的NIO属于同步非阻塞IO模型。
Unix 下有五种 I/O 模型:
- 阻塞式 I/O
- 非阻塞式 I/O
- I/O 复用(select 和 poll)
- 信号驱动式 I/O(SIGIO)
- 异步 I/O(AIO)
阻塞式 I/O
**应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。**在阻塞的过程中,其它程序还可以执行,不消耗 CPU 时间,这种模型的执行效率会比较高。
非阻塞式 I/O
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。由于 CPU 要处理更多的系统调用,因此这种模型是比较低效的。
I/O 复用
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回;再使用 recvfrom 把数据从内核复制到进程中。
Linux下目前暂不支持异步IO技术,所以使用的是epoll(多路复用IO技术)对异步IO进行模拟。
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。其中epoll 的描述符事件有两种触发模式: LT(level trigger)和 ET(edge trigger)。
1. LT 模式
LT模式是会有两次通知。当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
2. ET 模式
ET模式是通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。
信号驱动 I/O
应用进程使用 sigaction 系统调用,内核立即返回,应用进程才可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。
异步 I/O
进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
同步 I/O 与异步 I/O
- 同步 I/O: 应用进程在调用 recvfrom 操作时会阻塞。
- 异步 I/O: 不会阻塞。
阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O,虽然非阻塞式 I/O 和信号驱动 I/O 在等待数据阶段不会阻塞,但是在之后的将数据从内核复制到应用进程这个操作会阻塞。
总结
在 Java I/O 编程中,选择合适的流非常重要。
数据类型:
首先确定要处理的数据类型。如果要处理的是字节数据(如图像、音频、视频等),则选择字节流(如 FileInputStream、FileOutputStream 等)。如果要处理的是字符数据(如文本文件),则选择字符流(如 FileReader、FileWriter 等)。
输入还是输出:
确定是需要读取数据(输入)还是写入数据(输出)。对于输入操作,选择输入流(如 FileInputStream、FileReader 等),对于输出操作,选择输出流(如 FileOutputStream、FileWriter 等)。
缓冲还是非缓冲:
考虑是否需要缓冲来提高 I/O 性能。对于大量连续的 I/O 操作,使用缓冲流(如 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter 等)通常会提高性能。缓冲流在内部使用缓冲区,可以减少实际的磁盘或网络访问次数,从而提高效率。
数据处理需求:
根据需要处理的数据类型,选择合适的流。例如,如果需要处理基本数据类型(如整数、浮点数等),可以使用 DataInputStream 和 DataOutputStream。如果需要处理对象序列化,可以使用 ObjectInputStream 和 ObjectOutputStream。
特定场景:
针对特定场景选择合适的流。例如,在处理多个文件合并时,可以使用 SequenceInputStream;在处理线程间通信时,可以使用管道流,如 PipedInputStream 和 PipedOutputStream,或 PipedReader 和 PipedWriter。
以下是一些常见场景及建议使用的流:
处理文本文件:
使用 FileReader、FileWriter、BufferedReader 和 BufferedWriter
处理二进制文件:
使用 FileInputStream、FileOutputStream、BufferedInputStream 和 BufferedOutputStream
读取或写入基本数据类型:
使用 DataInputStream 和 DataOutputStream
对象序列化和反序列化:
使用 ObjectInputStream 和 ObjectOutputStream
合并多个文件:
使用 SequenceInputStream
线程间通信(管道通讯):
使用 PipedInputStream 和 PipedOutputStream,或 PipedReader 和 PipedWriter