OIO (BIO) 简单分析
因为工作室给新人培训要将到网络编程,想着布置一道 Socket
实现聊天平台的作业,想自己先写写。过程中遇到了 io 流
选择的问题,觉得这个应该挺重要的,花点时间研究了一下各种 IO 流
的源码,来记录一下,以后可以用到。
IO 流
流是一种抽象概念,可以看成一个水流,而 Java
的 IO
流工具分为 输入流
和 输出流
,输入流
是外界向你输入数据,而你可以不断地读,这里数据不是分包的,是水流状的,每次读都可能读到不同长度的流,也可以读不到任何数据。而对于 输出流
则相反,是你向外界输出数据,你可以不断地写,写入的数据长度可以是任意的。
IO 流
是一个高度抽象的工具,其可以应用在有数据流动的场景,比如在 Socket
连接中,当连接确立后,你可以拿到 输入输出
流,输入流
你可以不断地读,就能读到 另一方 写给你的数据,而 输出流
可以写入数据给对方,这里内部具体怎么实现数据传输地已经封装在 Java
内部,IO 流
就是面向开发者的一个工具,可以让你很方便的处理数据流动问题。
Java IO 流架构
Java
内部有很多个 IO 流
,具体如下图,其中,字节流(InputStream/OutputStream)
以字节为单位读和写,字符流(Reader/Writer)
以字符为单位。
![}T3~`W86R2YE49D8498IB{4.png][1]
并且 Java
的 IO 流
使用了 装饰者模式
,你可以使用不同的派生类来 装饰 你拿到的基类,关于设计模式之后可能会讲,这里不做说明。
基类
先来看看基类有哪些方法:
具体实现省略,仅给出方法和说明
InputStream
class InputStream{ // 实际为抽象类,这里只说明 接口
/**
* 读一个字节,当没数据的时候会阻塞
* @return 读到的数据,如果读到末尾则为 -1
*/
int read();
/**
* 读 n 个字节,读到传入的参数里,当没数据的时候会阻塞
* @param b 放读到的数据
* @return 读到的数据长度
*/
int read(byte[]) throws IOException ;
/**
* 读 n 个字节,从 offset 开始,最多读 len 个字节,当没数据的时候会阻塞
* @param b 放读到的数据
* @param offset 偏移量,从第几个字节开始读
* @param len 最多读的长度
* @return 读到的数据长度
*/
int read(byte[], int offset, int len) throws IOException ;
/**
* 跳过 l 个字节
* @param l 跳过的字节数
* @return 实际跳过的字节数
*/
int skip(long l) throws IOException ;
/**
* 获取当前可读的字节数
* @return 当前可读的字节数
*/
int available() throws IOException ;
/**
* 关闭流
*/
void close() throws IOException ;
/**
* 标记当前位置,标记后在重置之前只能读 li 个字节
* @param li 标记后读限制
*/
void mark(int);
/**
* 回到标记的位置
*/
void reset() throws IOException;
/**
* 当前流是否支持标记
* @return 是否支持标记
*/
boolean markSupported();
}
OutputStream
class OutputStream{
/**
* 写入一个字节
* @param b 写入的字节
*/
void write(int) throws IOException ;
/**
* 写入一个字节数组
* @param b 写入的字节数组
*/
void write(byte[]) throws IOException ;
/**
* 写入一个字节数组
* @param b 写入的字节
* @param offset 写入第一个字节的下标
* @param len 写入长度
*/
void write(byte[] b, int offset, int len) throws IOException ;
/**
* 刷新输出流,一般为清除之前写入的缓存,准备再次写入
*/
void flush() throws IOException ;
/**
* 关闭流
*/
void close() throws IOException ;
}
Writer
class Writer{
/**
* 无参构造方法
*/
public Writer();
/**
* 构造方法,传入锁对象
*/
public Writer(Object lock);
/**
* 写入字符,具体参数顾名思义把
*/
void write(int) throws IOException;
void write(char[]) throws IOException;
void write(char[], int offset, int len) throws IOException;
void write(String) throws IOException;
void write(String, int offset, int len) throws IOException;
/**
* 写入字符,返回值为链式调用,如果传入的 char 队列为空,则写入字符串 "null"
* @return 链式调用
*/
Writer append(CharSequence) throws IOException ;
Writer append(CharSequence, int offset, int len) throws IOEXception;
Writer append(char) throws IOException ;
/**
* 刷新缓存与关闭流
*/
void flush() throws IOException;
void close() throws IOException;
}
Reader
class Reader{
/**
* 构造方法,同 Writer
*/
public Reader();
public Reader(Object lock);
/**
* 读取字符
* 返回值为读取长度或读取的数据,具体与 InputStream 类似
*/
int read(CHarBuffer) throws IOException ;
int read() throws IOException ;
int read(char[])throws IOException ;
int read(char[],int offset, int len)throws IOException ;
/**
* 跳过,标记,重置,是否支持标记,关闭,具体与 InputStream 类似
*/
long skip(long l)throws IOException ;
void mark(int li) throws IOException ;
void reset()throws IOException ;
boolean markSupported();
void close()throws IOException ;
/**
* 当前有无字符可以读,即与下一次调用 read 会不会阻塞相反
* @return 有无字符可以读
*/
boolean ready()throws IOException ;
}
派生类
主要选取部分派生类做介绍
FileInputStream 与 FileOutputStream
这两个类主要是与文件打交道,可以通过这两个流来读取和写入文件。
ObjectInputStream 与 ObjectOutputStream
这两个类主要是可以在原本的基础上写入和读取对象,不过这里的对象必须是实现了 java.io.Serializable
或 java.io.Externalizable
的可实例化对象,而传输的过程实际上是实例化和反实例化的过程。
ByteArrayOutputStream 与 ByteArrayInputStream
这两个派生类会创建一个缓冲 ByteArray
,对于 OutputStream
,可以将写入的数据转换成字节数组,在调用 Write
写入数据后调用 toByteArray
方法将数据转换成字节数组。对于 InputStream
,在实例化的时候传入一个 ByteArray
,然后可以读这个数组里数据。
PipedOutputStream 与 PipedInputStream
这两个派生类可以连接,连接后,可调用 OutputStream
中 Write
方法写入数据,此时数据会放入缓冲区,而 InputStream
中 Read
方法就能从缓冲区读取数据,可以理解为消费者生产者模型,只不过是以字节为单位,并且消费者和生产者都只能有一个,在使用之前需要先连接。
FilterInputStream 与 FilterOnputStream
这两个派生类本身也是抽象类,主要是提供了装饰者的功能,其子类都是可以装饰其他 IO流
的,关于装饰者,可以理解为其构造方法要传入要装饰的对象,而其本身也实现了 基类 接口,并在实现的方法中调用传入对象的各种方法实现对应功能,其实现类有很多,这里主要讲三个
输入流:
- BufferedInputStream
- DataInputStream
- PushbakInputStream
输出流:
- BufferedOutputStream
- DataOutputStream
- PrintStream
BufferedInputStream 与 BufferedOutputStream
这两个装饰类可以为被装饰的类提供一个缓冲区,写入的时候是先写入缓冲区,然后内部会从缓冲区调用被装饰类的写入方法写入。读取的时候是先读入缓冲区,然后在读取。
DataInputStream 与 DataOutputStream
这两个装饰类主要是做转接,其同时实现了 DataOutput
与 DataIntput
,主要是可以写入和读取各种类型,int, boolean
等数据,装饰类的主要作用是在内部进行了与字节流之间的转换,功能上还是调用被装饰的流写入和读取字节流的方法。
PushbakInputStream
这个装饰类主要是可以实现 撤销读取
的操作,其内部实现了一个缓冲区,当调用 read
的时候,如果缓冲区有数据,则从缓冲区读取,如果缓冲区无数据,才调用被装饰者的 read
方法,调用装饰类的 unread
方法可以将数据写入缓冲区,即 撤销读取
。
PrintStream
这个装饰类主要是实现打印的功能,其特点有两个:
- 内部实现了字符集,并且支持各种数据类型与字符串之间的转换。
- 不会主动抛出
IOException
,可以调用checkError
方法查看当前是否有异常
不过不建议使用这个,建议使用字符流 PrintWriter
,因为 PrintWriter
可以指定输出的编码,而 PrintStream
只能使用系统默认编码。
我们常用的 System.out
就是由该装饰类装饰
BufferedReader 和 BufferedWriter
同上,就是实现了一个缓冲区,不过注意,虽然这两个类没有继承 FilterReader
和 FilterWriter
,但是这两个类自己实现了装饰者模式,可以装饰其他的字符流
InputStreamReader 和 OutputStreamWriter
这两个的作用是将字节流转换成字符流,可以指定自己的编码,构造方法传入 字节流 ,然后可以当成字符流使用,内部会实现按照传入的编码来进行字符和字节的转换。
FileReader 和 FileWriter
继承于 InputStreamReader
和 OutputStreamWriter
,主要是用与读写文件,将 FileInputStream
和 FileOutputStream
转换成字符流。
PipedReader 和 PipedWriter
和 PipedInputStream
和 PipedOutputStream
类似,这两个字符流可以连接,然后一边写入的数据会被另一边读出。
StringReader 和 StringWriter、CharArrayWriter
Reader
构造方法传入一个 String
然后可以读取这个 String
的字符,另外两个则写入后可以生成对应的类型,这里 StringWriter
内部使用 StringBuffer
实现。
PrintWriter
和 PrintStream
类似,API
也一致,唯一的区别在于,因为 PrintWriter
是字符流,其内部可以指定不同的字符编码,而 PrintStream
作为字节流,其在打印的时候只会按照系统默认编码写入字节。因为 PrintWriter
出现的时间较晚,因此为了兼容性 System.out
依然使用的是 PrintStream
FilterReader 和 FilterWriter
同样的,这俩是作为装饰者的 字符流 的基类,不过对于字符流,其用于装饰的较少,目前是只有一个。
PushbackReader
和 PushbakInputStream
类似,可以做到 撤销读取