因为工作室给新人培训要将到网络编程,想着布置一道 Socket 实现聊天平台的作业,想自己先写写。过程中遇到了 io 流 选择的问题,觉得这个应该挺重要的,花点时间研究了一下各种 IO 流 的源码,来记录一下,以后可以用到。

IO 流

流是一种抽象概念,可以看成一个水流,而 JavaIO 流工具分为 输入流输出流输入流 是外界向你输入数据,而你可以不断地读,这里数据不是分包的,是水流状的,每次读都可能读到不同长度的流,也可以读不到任何数据。而对于 输出流 则相反,是你向外界输出数据,你可以不断地写,写入的数据长度可以是任意的。

IO 流 是一个高度抽象的工具,其可以应用在有数据流动的场景,比如在 Socket 连接中,当连接确立后,你可以拿到 输入输出 流,输入流 你可以不断地读,就能读到 另一方 写给你的数据,而 输出流 可以写入数据给对方,这里内部具体怎么实现数据传输地已经封装在 Java 内部,IO 流 就是面向开发者的一个工具,可以让你很方便的处理数据流动问题。

Java IO 流架构

Java 内部有很多个 IO 流,具体如下图,其中,字节流(InputStream/OutputStream) 以字节为单位读和写,字符流(Reader/Writer) 以字符为单位。

}T3~`W86R2YE49D8498IB{4.png

并且 JavaIO 流 使用了 装饰者模式,你可以使用不同的派生类来 装饰 你拿到的基类,关于设计模式之后可能会讲,这里不做说明。

基类

先来看看基类有哪些方法:

具体实现省略,仅给出方法和说明

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.Serializablejava.io.Externalizable 的可实例化对象,而传输的过程实际上是实例化和反实例化的过程。

ByteArrayOutputStream 与 ByteArrayInputStream

这两个派生类会创建一个缓冲 ByteArray ,对于 OutputStream,可以将写入的数据转换成字节数组,在调用 Write 写入数据后调用 toByteArray 方法将数据转换成字节数组。对于 InputStream,在实例化的时候传入一个 ByteArray ,然后可以读这个数组里数据。

PipedOutputStream 与 PipedInputStream

这两个派生类可以连接,连接后,可调用 OutputStreamWrite 方法写入数据,此时数据会放入缓冲区,而 InputStreamRead 方法就能从缓冲区读取数据,可以理解为消费者生产者模型,只不过是以字节为单位,并且消费者和生产者都只能有一个,在使用之前需要先连接。

FilterInputStream 与 FilterOnputStream

这两个派生类本身也是抽象类,主要是提供了装饰者的功能,其子类都是可以装饰其他 IO流 的,关于装饰者,可以理解为其构造方法要传入要装饰的对象,而其本身也实现了 基类 接口,并在实现的方法中调用传入对象的各种方法实现对应功能,其实现类有很多,这里主要讲三个

输入流:

  • BufferedInputStream
  • DataInputStream
  • PushbakInputStream

输出流:

  • BufferedOutputStream
  • DataOutputStream
  • PrintStream

BufferedInputStream 与 BufferedOutputStream

这两个装饰类可以为被装饰的类提供一个缓冲区,写入的时候是先写入缓冲区,然后内部会从缓冲区调用被装饰类的写入方法写入。读取的时候是先读入缓冲区,然后在读取。

DataInputStream 与 DataOutputStream

这两个装饰类主要是做转接,其同时实现了 DataOutputDataIntput ,主要是可以写入和读取各种类型,int, boolean 等数据,装饰类的主要作用是在内部进行了与字节流之间的转换,功能上还是调用被装饰的流写入和读取字节流的方法。

PushbakInputStream

这个装饰类主要是可以实现 撤销读取 的操作,其内部实现了一个缓冲区,当调用 read 的时候,如果缓冲区有数据,则从缓冲区读取,如果缓冲区无数据,才调用被装饰者的 read 方法,调用装饰类的 unread 方法可以将数据写入缓冲区,即 撤销读取

PrintStream

这个装饰类主要是实现打印的功能,其特点有两个:

  1. 内部实现了字符集,并且支持各种数据类型与字符串之间的转换。
  2. 不会主动抛出 IOException,可以调用 checkError 方法查看当前是否有异常

不过不建议使用这个,建议使用字符流 PrintWriter,因为 PrintWriter 可以指定输出的编码,而 PrintStream 只能使用系统默认编码。

我们常用的 System.out 就是由该装饰类装饰

BufferedReader 和 BufferedWriter

同上,就是实现了一个缓冲区,不过注意,虽然这两个类没有继承 FilterReaderFilterWriter ,但是这两个类自己实现了装饰者模式,可以装饰其他的字符流

InputStreamReader 和 OutputStreamWriter

这两个的作用是将字节流转换成字符流,可以指定自己的编码,构造方法传入 字节流 ,然后可以当成字符流使用,内部会实现按照传入的编码来进行字符和字节的转换。

FileReader 和 FileWriter

继承于 InputStreamReaderOutputStreamWriter ,主要是用与读写文件,将 FileInputStreamFileOutputStream 转换成字符流。

PipedReader 和 PipedWriter

PipedInputStreamPipedOutputStream 类似,这两个字符流可以连接,然后一边写入的数据会被另一边读出。

StringReader 和 StringWriter、CharArrayWriter

Reader 构造方法传入一个 String 然后可以读取这个 String 的字符,另外两个则写入后可以生成对应的类型,这里 StringWriter 内部使用 StringBuffer 实现。

PrintWriter

PrintStream 类似,API 也一致,唯一的区别在于,因为 PrintWriter 是字符流,其内部可以指定不同的字符编码,而 PrintStream 作为字节流,其在打印的时候只会按照系统默认编码写入字节。因为 PrintWriter 出现的时间较晚,因此为了兼容性 System.out 依然使用的是 PrintStream

FilterReader 和 FilterWriter

同样的,这俩是作为装饰者的 字符流 的基类,不过对于字符流,其用于装饰的较少,目前是只有一个。

PushbackReader

PushbakInputStream 类似,可以做到 撤销读取