美文网首页
第9章 - Java IO

第9章 - Java IO

作者: vwFisher | 来源:发表于2019-11-04 16:54 被阅读0次

    第9章 - Java IO

    作者:vwFisher
    时间:2019-09-04
    GitHub代码:https://github.com/vwFisher/JavaBasicGuide

    目录



    1 IO模型介绍

    1.1 Linux的网络IO模型

    Linux内核将所有外部设备都可以看成一个文件操作(那么对外部设备的操作都可以看成对文件进行操作)

    对一个文件的读写,都通过调用内核提供的系统调用。

    内核给我们返回一个 file descriptor(fd, 文件描述符)。而对一个 socket 的读写也会有相应的描述符,称为 socketfd (socket描述符),描述符就是一个数字,指向内核中一个结构体(文件路径, 数据区等一些属性)。

    先对一些方法进行讲解:

    一、recvfrom:经 Socket 接收数据, 函数原型如下:

    ssize_t recvfrom(
        int sockfd, // 标识一个已连接套接口的描述字
        void *buf, // 接收数据缓冲区
        size_t len, // 缓冲区长度
        unsigned int flags,  // 调用操作方式(一个或多个标志组合)
        struct sockaddr *from, // [可选]指针, 指向装有源地址的缓冲区
        socket_t *fromlen // [可选]指针, 指向from缓冲区长度值
    ); 
    

    二、select:同步阻塞方式。几乎所有 unix、linux 都支持的一种 多路IO 方式, 通过 select 函数发出 IO 请求后, 线程阻塞, 一直到数据准备完毕, 然后才能把数据从核心空间拷贝到用户空间

    三、poll:poll 对 select 的使用方法进行了一些改进, 突破了最大文件数的限制, 同时使用更加方便一些

    四、epoll:是为了解决 select/poll 的性能问题。基本思路如下:

    1. 专门的内核线程来不停地扫描 fd列表
    2. 有结果后,把结果放到 fd 相关的链表中
    3. 用户线程只需要定期从该 fd 对应的链表中读取事件就可以了
    4. 为了节省把数据从核心空间拷贝到用户空间的消耗,采用了 mmap 方式,允许程序在用户空间直接访问数据所在的内核空间,不需要把数据 copy一份

    服务器端编程经常需要构造高性能的 IO模型,常见的 IO模型 有四种,Unix 提供了 五种 IO模型

    一、同步阻塞IO (Blocking IO)

    传统的 IO模型,Java 中老的 BIO 便是这种模式。在接到事件(数据到达、数据拷贝完成等)前程序需阻塞等待。

    优点:编码简单。

    缺点:效率低,处理程序阻塞会导致 cpu利用率 很低

    二、同步非阻塞IO (Non-blocking IO)

    默认创建的 socket 都是阻塞的,非阻塞IO 要求 socket 被设置为 NONBLOCK。

    在未接到事件时处理程序一直主动轮询,这样处理程序无需阻塞,可以在轮询间歇去干别的,但是轮询会造成重复请求,同样浪费资源。以前 Java 中实现的的 伪异步(伪AIO) 模式就是采用这种思想

    三、IO多路复用 (IO Multiplexing) 异步阻塞IO

    经典的 Reactor 设计模式,Java 中的 Selector 和 Linux 中的 epoll 都是这种模型。

    增加了对 socket 的事件监听器(selector),从而把处理程序和对应的 socket事件 解耦,所用的 socket连接 都注册在 监听器,在等待阶段只有监听器会阻塞,处理线程从监听器获取事件对 socket连接处理 即可,而且一个处理线程可以对应多个连接(前两种一般都是一个 socket 连接起一个线程,这就是为什么叫复用),优点是节省资源,由于处理程序能够被多个连接复用,因此少数的线程就能处理大量连接。缺点同样因为复用,如果是大量费时处理的连接(如大量连接上传大文件),很容易造成线程占满而导致新连接失败

    四、信号驱动IO模型

    在数据准别阶段无需阻塞,只需向系统注册一个信号,在数据准备好后,系统会响应该信号。该模型依赖于系统实现,而且信号通信使用比较麻烦,因此 Java 中未有对应实现

    五、异步IO (Asynchronous IO) 异步非阻塞IO

    经典的 Proactor 设计模式,与 信号驱动IO 很类似,而且在数据拷贝阶段(指数据从系统缓冲区拷贝至程序自己的缓冲区,其他模型改阶段程序都需要阻塞等待)同样可以异步处理.

    优点:效率很高;

    缺点:依赖系统底层实现。JDK1.7 之后在 concurrent 包中提供

    1.1.1 BIO - 同步阻塞I/O模型

    BIO-同步阻塞IO模型.png

    同步阻塞IO 模型是最常用的 IO模型, 用户线程在内核进行 IO操作 时被阻塞, 默认所有文件操作都是阻塞的.

    以 Socket(套接字) 为例来讲解此模型. 在进程空间中调用 recvfrom, 其系统调用直到数据报到达且被拷贝到应用进程的缓冲区或者发生错误才返回, 期间一直在等待. 我们就说进程从调用 recvfrom 开始到它返回的整段时间内是被阻塞的.

    用户线程通过系统调用 read 发起 IO 读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成 read 操作。所以用户需要等待 read 将 socket 中的数据读取到 buffer 后,才能继续处理数据。整个过程用户线程都是被阻塞的。则阻塞中,CPU 的利用效率就很低了

    用户线程使用 同步阻塞IO模型 的伪代码描述为:

    {
        read(socket, buffer); // 读取 socket 数据到 buffer
        process(buffer);  // 处理 buffer 数据
    }
    

    1.1.2 N-BIO - 同步非阻塞IO模型

    NBIO-同步非阻塞IO模型.png

    同步非阻塞IO 是在 同步阻塞IO 的基础上, 将 socket 设置为 NONBLOCK. 这样做用户线程可以在发起 IO请求 后可以立即返回。recvfrom 从应用层到内核的时候,如果缓冲区没有数据的话,就直接返回一个 EWOULDBLOCK 错误,一般都对非阻塞IO模型 进行轮询检查这个状态,看内核是不是有数据到来

    由于 socket 是非阻塞的方式,用户线程需要不断read,尝试读取 socket 数据,直到读取成功,才能继续处理。

    优点:调用 read 立即返回,如果读取失败,可以做其他事

    缺点:需要不断轮询请求,直至成功才能读取数据继续处理,会消耗大量 CPU资源

    用户线程使用 同步非阻塞IO模型 的伪代码描述为:

    {
        while(read(socket, buffer) != SUCCESS); // 不断循环 读取 socket 数据到 buffer
        process(buffer);  // 处理 buffer 数据
    }
    

    1.1.3 IO多路复用模型

    IO多路复用模型.png

    建立在内核提供的多路分离函数 select 基础之上的,避免同步非阻塞IO模型中轮询等待的问题。

    Linux 提供 select/poll,进程通过将一个或多个 fd 传递给 select 或 poll 系统调用,阻塞在 select。由 select/poll 帮我们侦测 fd 是否就绪。但 select/poll 是顺序扫描 fd 是否就绪, 且支持的 fd 数量有限.

    用户线程,先将需要进行 IO操作 的 socket 添加到 select 中,然后阻塞等待 select 系统调用返回。当数据到达时, socket 被激活,select 函数返回。用户线程正式发起 read 请求,读取数据并继续执行

    优点:可以在一个线程内同时处理多个 Socket 的 IO请求,用户可以注册多个 Socket,然后不断调用 select 读取被激活的 Socket。可以提高 CPU 利用率

    缺点:使用 select 函数进行 IO请求 与 BIO 没什么区别,而且还多了监视 Socket 等操作。效率更差

    其中 while 循环前将 socket 添加到 select 监视中, 然后在 while 内一直调用 select 获取被激活的 socket, 一旦 socket 可读, 便调用 read函数 将 socket 中的数据读取出来.

    用户线程使用select函数的伪代码描述为:

    {
        select(socket);
        while(1) {
            sockets = select();
            for(socket in sockets) {
                if(can_read(socket)) {
                    read(socket, buffer);
                    process(buffer);
                }
            }
        }
    }
    

    1.1.3.1 Reactor模型

    Reactor设计模式

    IO多路复用模型 使用了 Reactor 设计模式实现了这一机制。Linux 还提供了一个 epoll系统 调用,epoll 是基于事件驱动方式,而不是顺序扫描,当有 fd 就绪时,立即回调函数 rollback.

    通过 Reactor 方式,用户线程可以将轮询IO操作状态的工作统一交给 handle_events 事件循环进行处理. 用户线程注册事件处理器之后可以继续执行做其他的工作(异步)。

    Reactor 负责调用内核的 select 函数检查 socket 状态。当有 socket 被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行 handle_event 进行数据读取、处理的工作。由于 select 函数是阻塞的,因此 多路IO复用模型也 被称为 异步阻塞IO模型。

    注意:这里的所说的阻塞是指 select 函数执行时线程被阻塞,而不是指 socket. 一般在使用 IO多路复用模型 时, socket 都是设置为 NONBLOCK 的,不过这并不会产生影响,因为用户发起 IO请求 时,数据已经到达了,用户线程一定不会被阻塞

    IO多路复用 是最常使用的 IO模型, 但是其异步程度还不够 彻底, 因为它使用了会阻塞线程的 select 系统调用. 因此 IO多路复用 只能称为 异步阻塞IO, 而非真正的 异步IO

    一、EventHandler:表示IO事件处理器, 它拥有IO文件句柄Handle, 以及对Handle的操作

    1. get_handler():获取IO文件句柄
    2. handle_evnet():处理读/写事件

    二、Concrete:继承EventHandler。对事件处理器的行为进行定制

    三、Reactor:用于管理EventHandler,主要是注册、删除、和处理

    1. register_event():注册事件处理器
    2. remove_event():移除事件处理器
    3. handle_events():进行读取、处理工作。主要是调用Synchronous Event Demutiplexer的select()

    四、Synchronous Event Demutiplexer:同步事件多路分离器,一般是内核

    select():只要某个文件句柄被激活(可读/写等),select就返回(阻塞),Reactor 的 handle_events 就会调用与文件句柄关联的事件处理器的 handle_event 进行相关操作.

    用户线程使用 IO多路复用模型 的伪代码描述为:

    void UserEventHandler::handle_event() {
        if(can_read(socket)) {
            read(socket, buffer);
            process(buffer);
        }
    }
    {
        Reactor.register(new UserEventHandler(socket));
    }
    

    用户需要重写 EventHandler 的 handle_event 函数进行读取数据、处理数据的工作, 用户线程只需要将自己的 EventHandler 注册到 Reactor 即可. Reactor 中 handle_events 事件循环的伪代码大致如下

    Reactor::handle_events() {
        while(1) {
            sockets = select();
            for(socket in sockets) {
                get_event_handler(socket).handle_event();
            }
        }
    }
    

    1.1.4 信号驱动IO模型

    首先开启套接口 信号驱动IO功能,并通过系统调用 sigaction 执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个 SIGIO 信号。随即可以在信号处理程序中调用 recvfrom 来读数据,并通知循环函数处理数据。如图

    信号驱动IO模型.png

    1.1.5 异步IO模型

    异步IO模型.png

    告知内核启动某个操作,并让内核在某个操作完成后(包括将数据从内核拷贝到用户自己的缓冲区)通知我们

    这种模型与信号驱动模型的主要区别是:

    1. IO复用:事件循环将文件句柄状态事件给用户线程,由用户处理。
    2. 信号驱动IO:由内核通知我们何时可以启动一个IO操作
    3. 异步IO:由内核通知我们IO操作何时完成

    "真正"的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可

    异步IO模型使用了Proactor设计模式实现了这一机制

    Proactor设计模式.png

    Proactor模式 和 Reactor模式 在结构上比较相似。在用户线程(Client)使用方式差别很大。

    在 Reactor模式 中,用户线程向 Reactor 对象注册感兴趣的事件监听,然后事件触发时调用事件处理函数.

    在 Proactor模式 中,用户线程将 AsynchronousOperation(读/写等)、Proactor 以及操作完成时的 CompletionHandler 注册到 AsynchronousOperationProcessor。AsynchronousOperationProcessor 使用 Facade 模式提供了一组 异步操作API(读/写等) 供用户使用, 当用户线程调用 异步API 后,便继续执行自己的任务。 AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,实现真正的异步。当 异步IO操作 完成时, AsynchronousOperationProcessor 将用户线程 与 AsynchronousOperation 一起注册的 Proactor 和 CompletionHandler 取出,然后将 CompletionHandler 与 IO操作 的结果数据一起转发给 Proactor,Proactor 负责回调每一个异步操作的事件完成处理函数 handle_event。虽然 Proactor 模式中每个异步操作都可以绑定一个 Proactor 对象, 但是一般在操作系统中, Proactor 被实现为 Singleton模式, 以便于集中化分发操作完成事件

    异步IO模型中,用户线程直接使用内核提供的 异步IO API 发起 read 请求,且发起后立即返回,继续执行用户线程代码。 不过此时用户线程已经将调用的 AsynchronousOperation 和 CompletionHandler 注册到内核,然后操作系统开启独立的内核线程去处理 IO操作。当 read 请求的数据到达时,由内核负责读取 socket 中的数据,并写入用户指定的缓冲区中。最后内核将 read 的数据和用户线程注册的 CompletionHandler 分发给内部 Proactor,Proactor 将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步IO

    用户线程使用 异步IO模型 的伪代码描述为:

    void UserCompletionHandler::handle_event(buffer) {
        process(buffer);
    }
    {
        aio_read(socket, new UserCompletionHandler);
    }
    

    用户需要重写 CompletionHandler 的 handle_event 函数进行处理数据的工作,参数 buffer 表示 Proactor 已经准备好的数据,用户线程直接调用内核提供的 异步IO API,并将重写的 CompletionHandler 注册即可

    相比于 IO多路复用模型,异步IO 并不十分常用, 不少高性能并发服务程序使用 IO多路复用模型 + 多线程任务 处理的架构基本可以满足需求。况且目前操作系统对 异步IO 的支持并非特别完善,更多的是采用 IO多路复用模型 模拟 异步IO 的方式(IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中)。

    1.2 epoll - IO复用技术

    Java NIO 的核心类库 多路复用器 Selector 就是基于 epoll 的多路复用技术实现在IO编程过程中。

    当需要处理多个请求时, 可以利用 多线程 或 IO多路复用技术 进行处理. I/O多路复用技术 通过把多个 I/O 的阻塞复用同一个 select 阻塞上, 从而使得系统在单线程的情况下可以同时处理多个客户端请求. 与传统的多线程/多进程模型比, I/O多路复用 的最大是系统开销小, 系统不需要建立新的进程或线程, 也不需要维护这些线程和进程的运行, 降低了系统的维护工作量, 节省了系统资源. IO多路复用 的主要应用场景:

    1. 服务器需要同时处理多个处于监听状态和多个连接状态的套接字
    2. 服务器需要处理多种网络协议的套接字

    目前支持 I/O多路复用 的系统调用有 select、pselect、poll、epoll。

    epoll 是为了克服 select/poll 的缺点出现的,具体如下

    一、支持一个进程打开的socket描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数)

    select:一个进程打开的FD有一定限制,由 FD_SETSIZE(默认2048) 设置。对于那些需要支持的上万TCP连接数目的大型服务器来说显然太少了

    1. 可以选择修改这个宏,重新编译内核。但会带来网络效率的下降.
    2. 可以选多进程的解决方案。虽然 Linux 上面创建进程的代价比较小,但仍旧是不可忽视的。其次进程间的数据交换非常麻烦,Java 由于没有共享内存,需要通过 Socket 通信或其他方式进行数据同步,带来了额外的性能损耗, 增加了程序复杂的, 所以也不是一种完美的解决方案.

    epoll:支持FD上限是最大可以打开文件的数目,这个数字一般远大于2048。一般在1GB内存的机器上大约是10万左右, 具体数目可以 cat/proc/sys/fs/file- max 察看,通常情况下这个数目和系统内存关系很大

    二、I/O效率不会随着FD数目增加而线性下降

    select/poll:当拥有很大的 Socket 集合,由于网络延时,任一时间只有部分的 Socket 是"活跃"的, 但是 select/poll 每次调用都会线性扫描全部的集合,导致效率呈现线性下降。

    epoll:它只对"活跃"的 Socket 进行操作。在内核实现中, epoll 是根据每个 fd 的 callback 函数实现的。只有活跃的 Socket 才会主动调用 callback 函数, 其他idle状态Socket则不会. 在这点上, epoll实现了一个"伪"AIO"。

    三、使用mmap加速内核与用户控件的消息传递

    select、poll、epoll:都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,epoll是通过内核与用户空间mmap同一块内存实现的

    四、epoll的API更加简单

    创建epoll描述符、添加监听事件、阻塞等待所监听的事件发生、关闭epoll描述符.

    当然用来克服select/poll缺点的方法不只有epoll(epoll是Linux的实现方案)。

    1. freeBSD下有kqueue
    2. Solaris下由dev/poll



    2. Java IO概述

    Java I/O操作类在包java.io下,大概可以分为如下4组:

    1. 基于 字节 操作的I/O操作:InputStream和OutputStream
    2. 基于 字符 操作的I/O操作:Writer和Reader
    3. 基于 磁盘 操作的I/O操作:File
    4. 基于 网络 操作的I/O操作:Socket

    前2组主要是传输数据的数据格式,后两组主要是传输数据的方式。

    I/O的核心问题在于,影响I/O操作因素,要么是数据格式,要么是传输数据方式

    2.1 流概述

    流是一组有序的数据序列,I/O(Input/Output)流提供了一条通道程序,可以通过这条通道把源中的字节序列送到目的地。

    1. IO流用来处理设备之间的数据传输。Java用于操作流的对象都在IO包中
    2. Java对数据的操作是通过流的方式
    3. 流按 操作数据 分为两种, 字节流(InputStream、OutputStream)、字符流(Reader、Writer)
    4. 流按 流向 分为: 输入流(InputStream、Reader)、输出流(OutputStream、Writer)
    5. 按照 流的角色 分为:节点流、处理流。

    输入:外部媒介(文件、网络、压缩包、其他数据源) -> InputStream数据 -> 计算机,

    输出:计算机 -> OutputStream数据 -> 外部媒介(文件、网络、压缩包、其他数据源)

    在Java IO库虽然庞大,单在实际应用中,一般都是使用它们的子类,大致Java提供的各子类用途汇总:

    1. 文件访问
    2. 网络访问
    3. 内存缓存访问
    4. 线程内部通信(管道)
    5. 缓冲
    6. 过滤
    7. 解析
    8. 读写文本 (Reader/Writer)
    9. 读写对象、基本类型数据

    按操作方式分类结构图

    JavaIO按操作方式分类结构图.png

    按操作对象分类结构图

    JavaIO按操作对象分类结构图.png

    2.2 基于字节的IO操作接口

    只支持8位字节(1byte)流, 不能很好控制16位Unicode字符

    基于字节IO操作接口输入和输出分别是InputStream和OutputStream

    2.2.1 InputStream - 输入字节流

    InputStream类是字节输入流的抽象类, 是所有字节输入流的抽象父类

    InputStream类常用的方法 (标记阻塞: 在某个字符可用、发生 I/O 错误或者已到达流的末尾前,方法一直阻塞)

    public abstract int read() throws IOException
        阻塞,从输入流中读取数据的下一个字节. 返回0~255范围内的int字节值. 如果因为已经到达流末尾而没有可用的字节, 则返回值-1
    
    public read(byte[] b) throws IOException
        阻塞,从输入流中读入一定长度的字节, 并以整数形式返回字节数
    
    public int read(byte b[], int off, int len) throws IOException
        阻塞,在off偏移位置开始, 从输入流中读入长度len的字节, 并以整数形式返回字节数
    
    public long skip(long n) throws IOException
        跳过输入流上的n个字节并返回实际跳过的字节数
    
    public int available() throws IOException
        不阻塞,从输入流读取/跳过的估计字节数;如果到达输入流末尾,则返回 0
    
    public void close() throws IOException
        关闭此输入流并释放与该流关联的所有系统资源
    
    public synchronized void mark(int readlimit) throws IOException
        在输入流的当前位置放置一个标记, readlimit参数告知此输入流在标记位置失效之前允许读取的字节数
    
    public synchronized void reset() throws IOException
        将输入流指针返回到当前所做的标记处
    
    public boolean markSupported() throws IOException
        如果当前支持的n个字节并返回实际跳过的字节数
    

    [注:不是所有InputStream类的子类都支持所有方法, 如skip(), mark(), reset()等, 这些方法只对某些子类有用]

    InputStream:是所有的输入字节流的抽象父类
        |-- ByteArrayInputStream:介质类,Byte数组
        |-- FileInputStream:介质类,本地文件读取数据
        |-- ObjectInputStream
        |-- PipedInputStream:是从与其他线程的管道中读取数据
        |-- SequenceInputStream
        |-- StringBufferInputStream:已过时,介质类,底层使用StringBuffer。推荐使用StringReader
        |-- FilterInputStream:装饰类(装饰器模式主角)
            |-- BufferInputStream
            |-- DataInputStream
            |-- LineNumberInputStream
            |-- PushbackInputStream
    

    类 介绍

    ByteArrayInputStream
        功能:将内存中的Byte数组适配为一个InputStream
        构造:从内存中的Byte数组创建该对象(2种方法)
        使用:一般作为数据源, 会使用其他装饰流提供额外的功能, 一般都建议加个缓冲功能
    
    FileInputStream
        功能:最基本的文件输入流. 主要用于从文件中读取信息  构造:通过一个代表文件路径的 String、File对象或者 FileDescriptor对象创建
        使用:一般作为数据源, 同样会使用其它装饰器提供额外的功能
        
    PipedInputStream
        功能:读取从对应PipedOutputStream写入的数据. 在流中实现了管道的概念
        构造:利用对应的PipedOutputStream创建
        使用:在多线程程序中作为数据源, 同样会使用其它装饰器提供额外的功能
        
    ObjectInputStream
        功能:对象输入流
    
    SequenceInputStream
        功能:将2个或者多个InputStream 对象转变为一个InputStream    构造:使用两个InputStream(或子类)对象创建该对象
        使用:一般作为数据源, 同样会使用其它装饰器提供额外的功能
        
        
    装饰、输入字节流FilterInputStream:给其它被装饰对象提供额外功能的抽象类
    
    DataInputStream
        功能:一般与DataOutputStream配对使用,完成基本数据类型的读写
        构造:利用一个InputStream构造
        使用:提供了大量的读取基本数据类新的读取方法
        
    BufferedInputStream
        功能:阻止每次读取一个字节都会频繁操作IO. 将字节读取一个缓存区,从缓存区读取    构造:利用一个InputStream、或者带上一个自定义的缓存区的大小构造
        使用:使用InputStream的方法读取, 多一个缓存的功能. 设计模式中透明装饰器的应用
        
    LineNumberInputStream
        功能:跟踪输入流中的行号
        构造:利用一个InputStream构造
        使用:增加getLineNumber()和 setLineNumber(int)方法得到和设置行号
        
    PushbackInputStream
        功能:可以在读取最后一个byte 后将其放回到缓存中  构造:利用一个InputStream构造
        使用:仅仅会在设计compiler的scanner时会用到这个类, 在我们的java语言的编译器中使用它. 很多程序员可能一辈子都不需要
    

    2.2.2 OutputStream - 输出字节流

    OutputStream类是字节输出流的抽象类, 是所有字节输出流的抽象父类。常用的方法

    public abstract void write(int b) throws IOException
        将指定的字节写入此输出流
    public void write(byte b[]) throws IOException
        将指定byte数组写入此输出流
    public void write(byte b[], int off, int len) throws IOException
        将指定byte数组中从偏移量off开始的len个字节写入此输出流
    public void flush() throws IOException
        彻底完成输出并清空缓存区
    public void close() throws IOException
        关闭输出流
    
    OutputStream:是所有的输出字节流的父类
        |-- ByteArrayOutputStream:介质类,Byte数组
        |-- FileOutputStream:介质类,本地文件读取数据
        |-- ObjectOutputStream
        |-- PipedOutputStream:向与其它线程共用的管道中写入数据
        |-- FilterOutputStream:装饰类(装饰器模式主角)
            |-- BufferOutputStream
            |-- DataOutputStream
            |-- PrintStream
    

    类 介绍

    ByteArreaOutputStream
        功能:在内存中创建一个buffer, 所有写入此流中的数据都被放入到此buffer中  构造:无参或者使用一个可选的初始化buffer的大小的参数构造
        使用:将其和FilterOutputStream套接得到额外的功能, 建议首先和BufferedOutputStream套接实现缓冲功能. 通过toByteArray方法可以得到流中的数据. 不通过装饰器的用法
        
    FileOutputStream
        功能:将信息写入文件中 
        构造:使用代表文件路径的String、File对象或者 FileDescriptor对象创建. 还可以加一个代表写入的方式是否为append的标记
        使用:一般将其和FilterOutputStream套接得到额外的功能
        
    PipedOutputStream
        功能:任何写入此对象的信息都被放入对应PipedInputStream 对象的缓存中, 从而完成线程的通信, 实现了"管道"的概念.具体在后面详细讲解 
        构造:利用PipedInputStream构造, 在多线程程序中数据的目的地的
        使用:一般将其和FilterOutputStream套接得到额外的功能
        
    ObjectOutputStream
        功能:对象输出流
    
    装饰输出字节流FilterOutputStream:实现装饰器功能的抽象类, 为其它OutputStream对象增加额外的功能
    
    DataOutputStream
        功能:通常和DataInputStream配合使用, 使用它可以写入基本数据类新    构造:使用OutputStream构造
        使用:包含大量的写入基本数据类型的方法
        
    PrintStream
        功能:产生具有格式的输出信息
        构造:使用OutputStream和一个可选的表示缓存是否在每次换行时是否flush的标记构造. 还提供很多和文件相关的构造方法
        使用:一般是一个终极("final")的包装器, 很多时候我们都使用它
        
    BufferedOutputStream
        功能:使用它可以避免频繁地向IO写入数据, 数据一般都写入一个缓存区, 在调用flush方法后会清空缓存、一次完成数据的写入  
        构造:从一个OutputStream或者和一个代表缓存区大小的可选参数构造
        使用:提供和其它OutputStream一致的接口, 只是内部提供一个缓存的功能
    

    2.2.3 字节流输入与输出的对应

    InputStream:                                         OutputStream
      |-- ByteArrayInputStream              ByteArrayOutputStream --|
      |-- FileInputStream                        FileOutputStream --|
      |-- ObjectInputStream ---[ both use ]--- ObjectOutputStream --|
      |-- PipedInputStream  ---[ both use ]---  PipedOutputStream --|
      |-- FilterInputStream                    FilterOutputStream --|
        |-- BufferInputStream                  BufferOutputStream --|
        |-- DataInputStream ---[ both use ]---   DataOutputStream --|
        |-- LineNumberInputStream                                   |
        |-- PushbackInputStream                                     |
      |-- SequenceInputStream                                       |
      |-- StringBufferInputStream                                   |
                                                      PrintStream --|
    

    平行代表对应关系,both use 代表需要搭配使用

    而没有对应关系的流

    1.LineNumberInputStream

    主要完成从流中读取数据时,会得到相应的行号。至于什么时候分行、在哪里分行是由该类主动确定的, 并不是在原始中有这样一个行号。

    在输出部分没有对应的部分, 我们完全可以自己建立一个LineNumberOutputStream, 在最初写入时会有一个基准的行号, 以后每次遇到换行时会在下一行添加一个行号.

    2.PushbackInputStream

    查看最后一个字节, 不满意就放入缓冲区. 主要用在编译器的语法、词法分析部分. 输出部分的BufferedOutputStream几乎实现相近的功能

    3.StringBufferInputStream

    已经被Deprecated(过时), 本身就不应该出现在InputStream部分, 主要因为String应该属于字符流的范围. 已经被废弃了, 当然输出部分也没有必要需要它了! 还允许它存在只是为了保持版本的向下兼容而已.

    4.SequenceInputStream

    可以认为是一个工具类, 将两个或者多个输入流当成一个输入流依次读取. 完全可以从IO包中去除, 还完全不影响IO包的结构, 却让其更"纯洁"--纯洁的Decorator(装饰)模式

    5.PrintStream

    可以认为是一个辅助工具. 主要可以向其他输出流, 或者FileInputStream写入数据, 本身内部实现还是带缓冲的. 本质上是对其它流的综合运用的一个工具而已. System.out和Systm.out就是PrintStream的实例

    需要搭配使用

    1.ObjectInputStream/ObjectOutputStream

    要求写/读对象的次序要保持一致, 否则轻则不能得到正确的数据, 重则抛出异常(一般会如此)

    2.DataInputStream/DataOutputStream

    主要是要求写/读数据的次序要保持一致, 否则轻则不能得到正确的数据, 重则抛出异常(一般会如此);
    3.PipedInputStream/PipedOutputStream

    在创建时一般就一起创建, 调用它们的读写方法时会检查对方是否存在, 或者关闭!需要双方管道都存在才能交互

    2.2.4 ByteArray 流

    (basic.io.bytestream.byteArray.ByteArrayStreamDemo)

    2.2.4.1 ByteArrayInputStream

    构造函数:使用buf作为其缓冲区数组

    ByteArrayInputStream(byte buf[])     
    // pos=offset, count = Math.min(offset + length, buf.length)
    ByteArrayInputStream(byte buf[], int offset, int length)
    

    内部维护属性

    protected byte buf[]:流的创建者提供的byte数组
    protected int pos:要从输入流中读取的下一个字符的索引
    protected int mark:流中手动标记的位置,用于reset(),默认为0
    protected int count:buf最后一个有效字符的索引+1的索引值
    

    方法

    synchronized int read():读取下一个数据字节
    synchronized int read(byte b[], int off, int len):将最多len个数据字节从此输入流读入byte 数组
    synchronized long skip(long n):跳过n个输入字节
    synchronized int available():返回剩余字节数
    boolean markSupported():测试此 InputStream 是否支持 mark/reset
    void mark(int readAheadLimit):设置流中的当前标记位置,即mark
    synchronized void reset():将缓冲区的位置重置为标记位置,回滚到mark
    void close():关闭流无效, 关闭后仍可被调用
    

    字节数组流, 用于操作字节数组, 它的内部缓冲区就是一个字节数组

    2.2.4.2 ByteArrayOutputStream

    构造函数:创建一个新的byte输出流(默认size=32)

    ByteArrayOutputStream()
    ByteArrayOutputStream(int size)
    

    内部维护属性

    protected byte buf[]:   存储数据的缓冲区
    protected int count:缓冲区中的有效字节数
    

    扩容机制:当write超过设定的size。默认进行2倍扩容,最大数量 <= Integer.MAX_VALUE

    方法

    synchronized void write(int b):将指定的字节写入此byte数组输出流
    synchronized void write(byte b[], int off, int len):将指定byte数组中从偏移量off开始的len个字节写入此byte数组输出流
    synchronized void writeTo(OutputStream out):将此byte数组输出流的全部内容写入到指定的输出流参数中, 这与使用out.write(buf, 0, count)调用该输出流的write方法效果一样
    synchronized void reset():将此byte数组输出流的count字段重置为零, 从而丢弃输出流中目前已累积的所有输出
    synchronized byte toByteArray()[]:创建一个新分配的byte数组
    synchronized int size():返回缓冲区的当前大小
    synchronized String toString():使用平台默认的字符集, 通过解码字节将缓冲区内容转换为字符串
    synchronized String toString(String charsetName):使用指定的charsetName, 通过解码字节将缓冲区内容转换为字符串
    void close():   关闭流无效, 关闭后仍可被调用
    synchronized String toString(int hibyte):已过时, 此方法无法将字节正确转换为字符
    

    2.2.5 文件I/O流(FileInputStream与FileOutputStream类)

    (basic.io.bytestream.file.FileStreamDemo)

    2.2.5.1 FileInputStream

    构造函数:文件名/文件对象/文件描述对象,最终都是new一个FileDescriptor,然后调用open()
        public FileInputStream(String name) throws FileNotFoundException
        public FileInputStream(File file) throws FileNotFoundException
        public FileInputStream(FileDescriptor fdObj)
    内部维护属性:
        private final FileDescriptor fd;
        private final String path;
        private FileChannel  channel = null;
        private final Object closeLock = new Object();
        private volatile boolean closed = false;
    方法:
        public int read() throws IOException
        public int read(byte b[]) throws IOException
        public int read(byte b[], int off, int len) throws IOException
        public long skip(long n) throws IOException
        public int available() throws IOException
        public void close() throws IOException 
        public final FileDescriptor getFD() throws IOException
        public FileChannel getChannel()
        protected void finalize() throws IOException
    本地方法:
        private native void open0(String name) throws FileNotFoundException
        private void open(String name) throws FileNotFoundException
        private native int read0() throws IOException
        private native int readBytes(byte b[], int off, int len) throws IOException
        private native long skip0(long n) throws IOException;
        private native int available0() throws IOException
        private static native void initIDs():设置类中(也就是FileInputStream)的属性的内存地址偏移量,便于在必要时操作内存给它赋值
        private native void close0() throws IOException
    

    2.2.5.2 FileOutputStream

    构造函数:
        public FileOutputStream(String name) throws FileNotFoundException
        public FileOutputStream(String name, boolean append) throws FileNotFoundException
        public FileOutputStream(File file) throws FileNotFoundException
        public FileOutputStream(File file, boolean append) throws FileNotFoundException
        public FileOutputStream(FileDescriptor fdObj)
    内部维护属性:
        private final FileDescriptor fd;
        private final boolean append;
        private FileChannel channel;
        private final String path;
        private final Object closeLock = new Object();
        private volatile boolean closed = false;
    方法:
        public void write(int b) throws IOException
        public void write(byte b[]) throws IOException
        public void write(byte b[], int off, int len) throws IOException
        public void close() throws IOException 
        public final FileDescriptor getFD()  throws IOException
        public FileChannel getChannel()
        protected void finalize() throws IOException
        private static native void initIDs();
        private void open(String name, boolean append) throws FileNotFoundException
    本地方法:
        private native void write(int b, boolean append) throws IOException;
        private native void open0(String name, boolean append) throws FileNotFoundException;
        private native void writeBytes(byte b[], int off, int len, boolean append) throws IOException;
        private native void close0() throws IOException;
    

    2.2.6 操作对象

    (basic.io.bytestream.objects.ObjectStreamTest)

    ObjectInputStream与ObjectOutputStream,被操作的对象需要实现Serializable(标记接口)

    职责:从序列化文档中读取并解析出一个类。这个序列化文档是由 ObjectOutputStream 写入的。这一功能允许我们持久化一个内存对象,这便是序列化。Java IO 库规定了序列化的存储规范,从上层到下层是,对象,块存储,块内元素(块数据类型,块长度,数据)

    实例:

    /** Serializable:用于给被序列化的类加入ID号, 用于判断类和对象是否是同一个版本. */
    class Box implements Serializable {
        private int width;   
        private int height; 
        private String name;   
        public Box(String name, int width, int height) {
            this.name = name;
            this.width = width;
            this.height = height;
        }
        @Override
        public String toString() {
            return "["+name+": ("+width+", "+height+") ]";
        }
    }
    

    对象序列化

        private static void testWrite() {   
            try {
                ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(TMP_FILE));
                out.writeBoolean(true);
                out.writeByte((byte)65);
                out.writeChar('a');
                out.writeInt(20131015);
                out.writeFloat(3.14F);
                out.writeDouble(1.414D);
                // 写入HashMap对象
                HashMap map = new HashMap();
                map.put("one", "red");
                map.put("two", "green");
                map.put("three", "blue");
                out.writeObject(map);
                // 写入自定义的Box对象,Box实现了Serializable接口(对象序列化, 被序列化的对象必须实现Serializable接口)
                Box box = new Box("desk", 80, 48);
                out.writeObject(box);
    
                out.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    

    对象反序列化

        private static void testRead() {
            try {
                ObjectInputStream in = new ObjectInputStream(new FileInputStream(TMP_FILE));
                System.out.printf("boolean:%b\n" , in.readBoolean());
                System.out.printf("byte:%d\n" , (in.readByte()&0xff));
                System.out.printf("char:%c\n" , in.readChar());
                System.out.printf("int:%d\n" , in.readInt());
                System.out.printf("float:%f\n" , in.readFloat());
                System.out.printf("double:%f\n" , in.readDouble());
                // 读取HashMap对象
                HashMap map = (HashMap) in.readObject();
                Iterator iter = map.entrySet().iterator();
                while (iter.hasNext()) {
                    Map.Entry entry = (Map.Entry)iter.next();
                    System.out.printf("%-6s -- %s\n" , entry.getKey(), entry.getValue());
                }
                // 读取Box对象,Box实现了Serializable接口
                Box box = (Box) in.readObject();
                System.out.println("box: " + box);
    
                in.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    2.2.7 管道流

    (basic.io.bytestream.pipe.PipedStreamDemo)

    PipedInputStream和PipedOutputStream, 输入输出可以直接进行连接, 通过结合线程使用

    2.2.8 装饰类流(FilterInputStream/FilterOutputStream)

    2.2.8.1 缓存输入/输出流

    (basic.io.bytestream.filter.buffered.BufferedStreamDemo)

    BufferedInputStream与BufferedOutputStream类。缓存可以说是I/O的一种性能优化, 缓存流为I/O流增加了内存缓存区, 有了缓存区, 使得在流上执行skip(), mark()和reset()方法都称为了可能。

    1).BufferedInputStream:可以对任意的InputStream类进行带缓存区的包装以达到性能的优化.

    构造函数:传入InputStream,构建带有size(默认32)个字节的缓存流,最优的缓存区大小,取决于操作系统,可用内存空间及及其配置

        BufferedInputStream(InputStream in)
        BufferedInputStream(InputStream in, int size)
    

    BufferedInputStream读取文件过程:文件 -> InputStream -> BufferedInputStream --> 目的地

    2).BufferedOutputStream:输出信息和向OutputStream输入信息完全一样, 只不过BufferedOutputStream有一个flush()方法用来将缓存区的数据强制输出完

    构造函数:传入OutputStream,构建带有size(默认32)个字节的缓存流,最优的缓存区大小,取决于操作系统,可用内存空间及及其配置

        BufferedOutputStream(OutputStream out)
        BufferedOutputStream(OutputStream out, int size)
    

    说明:flush()方法就是用于即使缓存区没有满的情况下, 也将缓存区的内容强制写入到外设, 习惯上称这个过程为刷新, flush方法只对使用缓存区的OutputStream类的子类有效, 当调用close()方法时, 系统在关闭流之前, 也会将缓存区中信息刷新到磁盘文件中

    2.2.8.2 数据流

    (basic.io.bytestream.filter.data.DataStreamDemo)

    数据输入流/输出流(DataInputStream/DataOutputStream类), 允许应用程序以及其无关的方式从底层输入流中读取基本Java数据类型, 也就是说, 当读取一个数据时, 不必关心这个数值应当是什么字节

    1.DataInputStream类与DataOutputStream类的构造方法如下:

    DataInputStream(InputStream in): 使用指定的基础InpuStream创建一个DataInputStream
    DataOutputStream(OutputStream out):创建一个新的数据输出流, 将数据写入指定基础输出流
    

    2.DataOutputStream类提供如下3种写入字符串的方法

    writeBytes(String s)
    writeChars(String s)
    writeUTF(String s)
    

    由于Java中的字符是Unicode编码, 是双字节的, writeBytes只是将字符串中的每一个字符的低字节内容写入目标设备中. 而writeChars将字符串中的每一个字符的两个字节的内容都写到目标设备中, writeUTF将字符串按照UTF编码后的字节长度写入目标设备, 然后才是每一个字节的UTF编码

    DataInputStream类只提供了一个readUTF()方法返回字符串. 这是因为要在一个连续的字节流读取一个字符串, 如果没有特殊的标记作为一个字符串的结尾, 并且事先也不知道这个字符串的长度, 也就无法知道读取到什么位置才是这个字符串的结束

    DataOutputStream类中只有writeUTF()方法向目标设备写入字符串的长度, 所以只能准确地读回写入字符串

    2.2.9 序列流SequenceInputStream

    对多个流进行合并:1).文件切割 2).文件合并 3).文件切割合并+配置文件

    2.2.10 ZIP压缩输入/输出流

    使用ZIP压缩管理文件(ZIP archive), 是一种典型的文件压缩格式, 可以节省存储空间. 关于ZIP压缩的I/O实现, 在Java的内置类中, 提供了非常好用的相关类, 所以实际的实现方式非常容易

    Java实现了I/O数据流与网络数据流的单一接口, 因此数据的压缩、网络传输和解压缩的实现比较容易.

    ZipEntry类产生的对象, 是用来代表一个ZIP压缩文件内的进入点(entry), ZipInputStream类用来读取ZIP压缩格式的文件, 所支持的包括已压缩及未压缩的进入点(entry), ZipOutputStream类用来写出ZIP压缩格式的文件, 而且所支持的包括已压缩及未压缩的进入点(entry)

    2.2.10.1 压缩文件

    利用ZipInputStream类对象,可将文件压缩为.zip文件。

    ZipOutputStream类常用的方法

    ZipOutputStream(OutputStream out)   构造函数
    putNextEntry(ZipEntry e)    开始写一个新的ZipEntry, 并且将流内的位置移至此entry所指数据的开头
    write(byte[] b, int off, int len)   将字节数组写入当前ZIP条目数据
    finish()    完成写入ZIP输出流的内容, 无序关闭它所配合的OutputStream
    setComment(String comment)  可设置此ZIP文件的注释文字
    

    2.2.10.2 解压缩ZIP文件

    ZipInputStream类可读取ZIP压缩格式的文件,包括对已压缩和未压缩条目的支持(entry)。

    ZipInputStream类常用的方法

    ZipInputStream(InputStream in)  构造函数
    read(byte[] b, int off, int len)    读取目标b数组off偏移量的长度, 长度是len字节
    available() 判断是否已读完目前entry所指定的数据, 已读完返回0, 否则返回1
    closeEntry()    关闭目前ZIP条目并定位流以读取下一个条目
    skip(long n)    跳过当前ZIP条目中指定的字节数
    getNextEntry()  读取下一个ZipEntry, 并将流内的位置移至该entry所指数据的开头
    createZipEntry(String name) 以指定的name参数新建一个ZipEntry对象
    

    2.3 基于字符的IO操作接口

    2.3.1 Reader - 输入字符流

    Java中的字符是Unicode编码(双字节). InputStream是用来处理字节的, 在处理字符文本时不是很方便.

    Java为字符文本的输入提供Reader,但Reader类并不是InputStream类的替换者。直是在处理字符串时简化了编程. Reader类是字符输入流的抽象类, 所有字符输入流的实现都是它的子类

    Reader类常用的方法 (标记阻塞: 在某个字符可用、发生 I/O 错误或者已到达流的末尾前,方法一直阻塞)

    public int read(java.nio.CharBuffer target) throws IOException  将字符读入指定的字符缓冲区。缓冲区可照原样用作字符的存储库:所做的唯一改变是 put 操作的结果。不对缓冲区执行翻转或重绕操作
    public int read() throws IOException    阻塞,读取单个字符, 范围在 0 到 65535 之间 (0x00-0xffff), 如果位于缓冲区末端, 则返回-1
    public int read(char cbuf[]) throws IOException 阻塞,将字符读入数组
    abstract public int read(char cbuf[], int off, int len) throws IOException  阻塞,将字符读入数组的某一部分 [cbuf:目标缓冲区, off:开始存储字符处的偏移量, len:读取的最多字符数]
    public long skip(long n) throws IOException 阻塞,跳过字符
    public boolean ready() throws IOException   判断是否准备读取此流
    public boolean markSupported()  判断此流是否支持mark()操作, 模式实现始终返回false, 子类应该重写此方法
    public void mark(int readAheadLimit) throws IOException 标记流中的当前位置。对reset()的后续调用将尝试将该流重新定位到此点。如果该流支持mark()操作。
    public void reset() throws IOException  配合mark(),reset回到mark标记的位置。如果该流支持reset()操作
    abstract public void close() throws IOException 关闭该流并释放与之关联的所有资源。在关闭该流后,再调用 read()、ready()、mark()、reset() 或 skip() 将抛出 IOException。关闭以前关闭的流无效
    
    Reader:所有的输入字符流的抽象父类
        |-- BufferedReader:很明显就是一个装饰器, 它和其子类负责装饰其它Reader对象
            |-- LineNumberReader
        |-- CharArrayReader:基本的介质流,将Char数组中读取数据
        |-- InputStreamReader:一个连接字节流和字符流的桥梁, 它将字节流转变为字符流
            |-- FileReader:源码中明显使用了将FileInputStream转变为Reader的方法
        |-- PipedReader:是从与其它线程共用的管道中读取数据
        |-- StringReader:基本的介质流,将String中读取数据
        |-- FilterReader:所有自定义具体装饰流的父类
            |-- PushbackReader:对Reader对象进行装饰, 会增加一个行号
        Reader中各个类的用途和使用方法基本和InputStream中的类使用一致
    

    2.3.2 Writer - 输出字符流

    Writer类是字符输出流的抽象类, 所有字符输出类的实现都是它的子类

    Writer类常用的方法(标记阻塞: 在某个字符可用、发生 I/O 错误或者已到达流的末尾前,方法一直阻塞)

    public void write(int c) throws IOException 写入单个字符. 要写入的字符包含在给定整数值的16个低位中, 16 高位被忽略, 用于支持高效单字符输出的子类应重写此方法
    public void write(char cbuf[]) throws IOException   写入字符数组
    abstract public void write(char cbuf[], int off, int len) throws IOException    写入字符数组的某一部分
    [cbuf: 目标数组, off:偏移量, len:写入长度]
    public void write(String str) throws IOException    写入字符串
    public void write(String str, int off, int len) throws IOException  写入字符串的某一部分
    public Writer append(CharSequence csq) throws IOException   将指定字符序列添加到此 writer
    public Writer append(CharSequence csq, int start, int end) throws IOException   将指定字符序列添加到此 writer
    public Writer append(char c) throws IOException 将指定字符添加到此 writer
    abstract public void flush() throws IOException 彻底完成输出并清空缓存区
    abstract public void close() throws IOException 关闭此流,但要先刷新它
    
    Writer:所有的输出字符流的抽象父类
        |-- BufferedWriter:是一个装饰器为Writer提供缓冲功能
        |-- CharArrayWriter:基本的介质流,将Char数组中写入数据
        |-- OutputStreamWriter:OutputStream到Writer转换的桥梁, 它将字节流转变为字符流
            |-- FileWriter:源码中明显使用了将FileOutStream转变为Writer的方法
        |-- PipedWriter:是从与其它线程共用的管道中写入数据
        |-- PrintWriter:和PrintStream极其类似,功能和使用也非常相似
        |-- StringWriter:基本的介质流,将String中写入数据
        |-- FilterWriter:所有自定义具体装饰流的父类
        Writer中各个类的用途和使用方法基本和InputStream中的类使用一致.后面会有Writer与InputStream的对应关系
    

    2.3.3 字符流的输入与输出的对应

    Reader                                         Writer
      |-- BufferedReader              BufferedWriter --|
        |-- LineNumberReader                           |
      |-- CharArrayReader             CharArrayWriter--|
      |-- InputStreamReader        OutputStreamWriter--|
        |-- FileReader                  FileWriter --|
      |-- PipedReader ---[ both use ]--- PipedWriter --|
      |-- StringReader                  StringWriter --|
      |-- FilterReader                  FilterWriter --|
        |-- PushbackReader                             |
                                         PrintWriter --|
    

    2.3.4 FileReader与FileWriter类

    FileInputStream和FileOutputStream只体用了对字节或字节数组的读写方法, 由于汉子在文件中占用两个字节, 如果使用字节流, 读取不好可能会出现乱码现象.

    对于读取字符流, 采用字符流FileReader和FileWriter避免乱码的情况

    FileReader对应FileInputStream,FileWriter对应FileOutputStream

    2.3.5 打印流

    PrintWriter(纯文本)与PrintStream(二进制文件, 如图片、音乐、视频等)可以直接操作输入流和文件

    1. 提供了打印方法可以对多重数据类型值进行打印. 并保持数据的表示形式
    2. 它不抛IOException

    构造函数

    1. 字符串路径
    2. File对象
    3. 字节输出流
    4. 字符输出流

    2.3.6 BufferedReader与BufferedWriter类

    (basic.io.charstream.buffered.BufferedDemo)

    BufferedReader类与BufferedWriter类分别继承Reader类与Writer类, 这两个类具有内部缓存机制, 并可以为单位进行输入/输出,BudderedReader类读取文件的过程:

    字符数据 -> BufferedWriter -> OutputStreamWriter -> OutputStream -> 文件
    BufferedReader类常用方法:

    read(): 读取单个字符
    readLine(): 读取一个文本行, 并将返回为字符串; 若无数据可读, 则返回null
    write(String s, int off, int len): 写入字符串的某一部分
    flush(): 刷新该流的缓存
    newLine(): 写入一个行分隔符
    

    在使用BufferedWriter类的Write()方法时, 数据并没有立刻被写入至输出流中, 而是首先先进入缓存区中, 如果想立刻将缓存区中的数据写入输出流中, 要调用flush()

    自定义BufferReader:

    (basic.io.charstream.buffered.CustomBufferedReader)

    自定义的读取缓冲区. 其实就是模拟一个BufferedReader

    分析: 缓冲区无非就是封装了一个数组, 并对外提供了更多的方法对数组进行访问.

    缓冲的原理: 其实就是从源中获取一批数据装进缓冲区中, 再从缓冲区中不断的取出一个一个数据, 在此次取完后, 再从源中继续取一批数据进缓冲区, 当源中的数据取光时, 用-1作为结束标记

    2.4 字节流和字符流

    2.4.1 输入的对应

    InputStream                                      Reader
      |-- ByteArrayInputStream         CharArrayReader --|
      |-- PipedInputStream                 PipedReader --|
      |-- ObjectInputStream                              |
      |                               InputStreamReader--| 
      |-- FileInputStream                 FileReader --|
      |-- FilterInputStream               FilterReader --|
        |-- PushbackInputStream       PushbackReader --|
        |-- BufferedInputStream         BufferedReader --|
        |-- LineNumberInputStream   LineNumberReader --|
        |-- DataInputStream                              |
      |                                   StringReader --|
      |-- SequenceInputStream
      |-- StringBufferInputStream 
    

    注意:即使对应, 它们的继承关系也是不太对应的

    2.4.2 输出的对应

    OutputStream                                   Writer
      |-- ByteArrayOutputStream      CharArrayWriter --|
      |-- ObjectOutputStream                           | 
      |                           OutputStreamWriter --|
      |-- FileOutputStream               FileWrite --|
      |-- PipedOutputStream              PipedWriter --|
      |-- FilterOutputStream            FilterWriter --|
        |-- BufferedOutputStream      BufferedWriter --|
        |-- PrintStream                  PrintWriter --|
        |-- DataOutputStream                           |
        |                               StringWriter --|
    

    2.4.3 各个类所负责的媒介

    | | Byte Based | Character Based |
    Input Output Input Output

    InputStream OutputStream Reader Writer
    Basic InputStream OutputStream Reader </br> InputStreamReader Writer
    OutputStramWriter
    Arrays ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
    Files FileInputStream RandomAccessFile FileOutputStream RandomAccessFile FileReader FileWriter
    Pipes PipedInputStream PipedOutputStream PipedReader PipedWriter
    Buffering BufferedInputStream BufferOutputStream BufferedReader BufferedWriter
    Fitering FilterInputStream FilterOutputStream FilterReader FilterWriter
    Parsing PushbackInputStream StreamTokenizer PushbackReader LineNumberReader
    Strings StringReader StringWriter
    Data DataInputStream DataOutputStream
    Data-Formatted PrintStream PrintWriter
    Objects ObjectInputStream ObjectOutputStream
    Utilities SequenceInputStream

    2.4.4 转换流(字节流->字符流)(TransStreamDemo)

    Java的I/O中存在输入、输出的对应和字节流和字符流的对应,它们之间的转化桥梁

    1. InputStreamReader:字节到字符的桥梁、解码
    2. OutputStreamWriter:字符到字节的桥梁、编码
    public class ConvertDemo {
        public static void main(String[] args) throws IOException  {
            ConvertDemo demo = new ConvertDemo();
            demo.byte2char();
        }
    
        /** 字节流 -> 字符流  */
        public void byte2char() throws IOException  {
            // 源头,可以是System.in,可以是文件,可以是固定byte
    //      InputStream inputStream = System.in;
            InputStream inputStream = new ByteArrayInputStream("Hello World".getBytes());
            // 将字节转成字符的桥梁, 装换流
            InputStreamReader reader = new InputStreamReader(inputStream);
    //        InputStreamReader reader = new InputStreamReader(inputStream, "UTF-8");
            // int ch = isr.read();
            // System.out.println((char)ch);
    
            BufferedReader buffer = new BufferedReader(reader);
            // 目标,可以是System.out,可以是文件,可以是其他
            OutputStream out = System.out;
            OutputStreamWriter writer = new OutputStreamWriter(out);
            BufferedWriter bufferedWriter = new BufferedWriter(writer);
    
            String line = null;
            while ((line = buffer.readLine()) != null) {
                if ("over".equals(line))
                    break;
                // System.out.println(line.toUpperCase());
                // osw.write(line.toUpperCase()+"\r\n");
                // osw.flush();
                bufferedWriter.write(line.toUpperCase());
                bufferedWriter.newLine();
                bufferedWriter.flush();
            } 
        }
    }
    

    2.4.5 流的操作规律

    流的操作规律: 明确4点

    1.明确源和目的(汇) 
        源: InputStream(字节输入流)  Reader(字符输入流)
        汇: OutPutStream(字节输出流)  Writer(字符输出流)
    2.明确数据是否纯文本数据(这步可以明确需求中具体要使用哪个体系)
        源: 是纯文本:Reader    否:InputStream
        汇: 是纯文本:Writer    否:OutputStream
    3.明确具体的设备
        硬盘:File     键盘:System.in        内存:数组       网络:Socket流
    4.是否需要其他额外功能
        1.是否需要高效(缓冲区),需要就加上Buffer 
        2.转换流, 什么时候使用转换流呢?
            1).源或目的对应的设备是字节流, 但操作的却是文本数据, 使用转换作为桥梁. 提高对文本操作的便捷. 
            2).一旦操作文本涉及到具体的指定编码表时, 必须使用转换流 . 
    

    2.5 File类

    (basic.io.FileDemo)

    File类是io包中唯一代表磁盘文件本身的对象. File类定义了一些与平台无关的方法, 可以通过调用File类种的方法, 实现创建, 删除, 重命名文件等.

    数据流可以将数据写入到文件中, 而文件也是数据流最常用的数据媒体

    [注: 对于Microsoft Windows平台, 包含盘符的路径名前缀由驱动器号和一个":"组成, 如果路径名是绝对路径名, 还可能后跟"\"]

    构造函数:文件名/文件对象/文件描述对象,最终都是new一个FileDescriptor,然后调用open()

        public File(String pathname)
        public File(String parent, String child)
        public File(File parent, String child)
        public File(URI uri)
        private File(String pathname, int prefixLength)
        private File(String child, File parent)
    

    内部维护属性:

        private static final FileSystem fs = DefaultFileSystem.getFileSystem():对应JDK获取文件系统
        public static final char separatorChar = fs.getSeparator():路径分隔符(Unix:'/',Windows:'\\')
        public static final String separator = "" + separatorChar
        public static final char pathSeparatorChar = fs.getPathSeparator():path分割符(Unix:':', Windows:';')
        public static final String pathSeparator = "" + pathSeparatorChar
        private final String path
        private static enum PathStatus { INVALID, CHECKED }
        private transient PathStatus status = null
        private final transient int prefixLength
        int getPrefixLength():绝对路径的前缀长度
    
    路径方法:
        String getParent()  获取上一级的目录相对路径
        String getPath()    获取文件的目录相对路径
        String getAbsolutePath()    获取文件的绝对路径
        String getCanonicalPath()   返回此抽象路径名的规范路径名字符串
    判断方法:
        boolean isAbsolute()    是否为绝对路径名
        boolean canRead()   是否可读
        boolean canWrite()  是否可写
        boolean exists()    是否存在
        boolean isDirectory()   是否是目录
        boolean isFile()    是否是文件
        boolean isHidden()  是否是隐藏文件
        boolean canExecute()    是否可执行
        boolean equals(Object obj)  与文件对象比较是否相同
    获取方法:
        long length()   文件的长度
        String getName()    文件或目录名称
        long lastModified() 最后修改时间
        long getTotalSpace()    指定的分区大小
        long getFreeSpace() 指定的分区中未分配的字节数
        long getUsableSpace()   指定的分区上可用于此虚拟机的字节数
        int compareTo(File pathname)    按字母顺序比较两个抽象路径名
        int hashCode()  哈希码
        String toString()   路径名字符串
        File getParentFile()    父目录的抽象路径名;如果此路径名没有指定父目录,则返回 null
        File getAbsoluteFile()  抽象路径名的绝对路径名形式
        File getCanonicalFile() 抽象路径名的规范形式
        URL toURL() 过时, 
        URI toURI() 构造一个表示此抽象路径名的 file: URI
        String[] list() 此抽象路径名表示的目录中的文件和目录
        String[] list(FilenameFilter filter)    此抽象路径名表示的目录中满足指定过滤器的文件和目录
        File[] listFiles()  同上,返回File对象列表
        File[] listFiles(FilenameFilter filter) 同上
        File[] listFiles(FileFilter filter) 同上
        static File[] listRoots()   列出可用的文件系统根
        Path toPath()   
    操作方法:
        boolean mkdir() 指定的目录
        boolean mkdirs()    指定的目录,包括所有必需但不存在的父目录
        boolean createNewFile() 创建文件
        boolean renameTo(File dest) 重命名
        boolean delete()    删除文件
        boolean setLastModified(long time)  设置最后修改时间
        boolean setReadOnly()   设置只读
        boolean setWritable(boolean writable, boolean ownerOnly)    设置写状态
        boolean setWritable(boolean writable)   设置写状态
        boolean setReadable(boolean readable, boolean ownerOnly)    设置读状态
        boolean setReadable(boolean readable)   设置读状态
        boolean setExecutable(boolean executable, boolean ownerOnly)    设置执行状态
        boolean setExecutable(boolean executable)   设置执行状态
        void deleteOnExit() 在虚拟机终止时,请求删除此抽象路径名表示的文件或目录
        synchronized void writeObject(java.io.ObjectOutputStream s) 
        synchronized void readObject(java.io.ObjectInputStream s)   
    静态方法:
        static File createTempFile(String prefix, String suffix, File directory)    在默认临时文件目录中创建一个空文件,使用给定前缀和后缀生成其名称
        static File createTempFile(String prefix, String suffix)    在指定目录中创建一个新的空文件,使用给定的前缀和后缀字符串生成其名称
    

    2.6 磁盘IO工作机制

    读取和写入文件,IO操作都调用操作系统提供的接口。而只要是系统调用就涉及到内核空间地址和用户空间地址切换的问题,这是操作系统为了保护系统本身的运行安全而将内核程序运行使用内存空间和用户程序运行的内存空间隔离造成的。保证了内核程序运行的安全性,但也就存在,从内核空间向用户空间复制的问题。

    这里就会涉及到几个概念:

    1. 物理磁盘:如硬盘
    2. 内核地址空间:内核使用的地址空间
    3. 内核地址空间的高速页缓存:为了减小IO响应时间,对磁盘读取的文件进行缓存,当读取同一个地址的空间数据,直接从缓存读取
    4. 用户地址空间:用户线程(程序)操作的空间,与内核空间隔离
    5. 用户地址空间的应用缓存

    应用程序访问文件的几种访问文件方式:

    1).标准访问文件方式
        read():物理磁盘 -> 内存地址空间(高速页缓存) -> 用户地址空间(应用缓存) -> read()
        write():write() -> 内存地址空间(高速页缓存) -> 用户地址空间(应用缓存) -> 物理磁盘
    2).直接IO的方式
        read():物理磁盘 -> 用户地址空间(应用缓存) -> read()
        write():write() -> 用户地址空间(应用缓存) -> 物理磁盘
    3).同步访问文件方式:就是数据读取和写入都是同步操作的
    4).异步访问文件方式
        当访问数据的线程发出请求后,线程会接着去处理其他事情,而不是阻塞等待,当请求的数据返回后继续处理下面的操作。提高应用程序的效率,而不会改变访问文件的效率
    5).内存映射方式
        read():物理磁盘 -> 内存地址空间(高速页缓存) -- 地址映射 --> 用户地址空间(应用缓存) -> read()
        write():write() -> 内存地址空间(高速页缓存) -- 地址映射 --> 用户地址空间(应用缓存) -> 物理磁盘
        操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要访问内存中一段数据时,转换为访问文件的某一段数据。减少数据从内核空间缓存到用户空间缓存的数据复制操作,因为这两个空间的数据时共享的。
    

    2.6.1 Java访问磁盘文件

    数据在磁盘中的唯一最小描述就是文件,简单的说,上层应用程序只能通过文件来操作磁盘上的数据。文件也是操作系统和磁盘驱动器交互的最小单元。

    在Java中,File类并不代表一个真实存在的文件对象,他只是代表这个路径的一个虚拟对象。当真正读取这个文件时,就会检查这个文件是否存在。

    Java访问磁盘文件.png

    当读取文件时,会根据File来创建真正读取文件的操作对象FileDescriptor。通过这个对象可以直接控制这个磁盘文件。[如:getFD()来获取真正操作的底层操作系统关联的文件描述。且sync()方法将操作系统缓存中的数据强制刷新到物理磁盘中]

    而由于需要读取的事字符格式,所以需要StreamDecoder类将byte解码为char格式。至于如何从磁盘驱动器上读取一段数据,操作系统会帮我们完成

    2.6.2 磁盘IO优化

    2.6.2.1 性能检测

    磁盘I/O通常都很耗时,有一些参数指标可以判断I/O是否是一个瓶颈

    1. 压力测试应用程序查看系统的I/O wait指标是否正常,例如:测试有4个CPU,那么理解I/O wait参数不应该超过25%。若超过则可能成为瓶颈,Linux可以通过iostat命令查看
    2. IOPS:应用程序需要的最低IOPS,磁盘提供的IOPS。每个磁盘IOPS通常都在一个范围内,这和存储在磁盘上的数据块的大小和访问方式也有关,但主要是由磁盘的转速决定的,转速越高,IOPS也越高

    为了提高磁盘I/O性能,通常采用RAID技术,就是将不同的磁盘组合起来提高I/O性能。目前有多种RAID技术,每种RAID技术对I/O性能的提高会有不同,可以用一个RAID因子来代表,磁盘的读写吞吐量可以通过iostat命令来获取,于是可以计算出一个理论的IOPS值,公式如下:

    IOPS = (磁盘数 * 每块磁盘的IOPS) / (磁盘读的吞吐量 + RAID因子 * 磁盘写的吞吐量)
    

    2.6.2.2 提高I/O性能

    主要方法:

    1. 增加缓存,减少磁盘访问次数
    2. 优化磁盘的管理系统,设计最优的磁盘方式策略,以及磁盘的寻址策略,这是底层操作系统层面考虑
    3. 设计合理的磁盘存储数据块,以及访问这些数据块的策略,这是应用层面考虑的。例如:给存放的数据设计索引,通过寻址索引来加快和减少磁盘的访问量,也可以采用异步和非阻塞方式加快磁盘的访问速度
    4. 应用合理的RAID策略提升磁盘IO,如下表
    磁盘阵列 说明
    RAID 0 数据被平均写到多少个磁盘阵列中,写数据和杜数据都是并行的,所以磁盘的IOPS可以提高一倍
    RAID 1 提高数据的安全性,将一份数据分别复制到多个磁盘阵列中,并不能提升IOPS,但是相同的数据有多个备份。用于对数据安全性较高的场合中
    RAID 5 这种方式,将数据平均写到所有磁盘阵列总数减一的磁盘中,往另外一个磁盘中写入这份数据的奇偶校验信息。如果其中一个磁盘损坏,可以通过其他磁盘的数据和这个数据的奇偶校验信息来恢复这份数据
    RAID 0 + 1 根据数据备份情况进行分组,一份数据同时写到多个备份磁盘分组中,同时多个分组也会并行读写

    2.7 IO包中的其他类

    2.7.1 RandomAccessFile(不是IO体系中的子类, 是Object的子类)

    特点:

    1. 该对象既能读, 又能写
    2. 该对象内部维护一个byte数组, 并通过指针可以操作数组中的元素
    3. 可以通过getFilePointer方法获取指针的位置, 和通过seek方法设置指针的位置
    4. 其实该对象就是将字节输入流和输出流进行了封装
    5. 该对象的源或汇只能是文件. 通过构造函数就可以看出
    (basic.demo18.RandomAccessFileDemo)
    import java.io.IOException;
    import java.io.RandomAccessFile;
    
    /**
     * RandomAccessFile(不是IO体系中的子类, 是Object的子类)
     * 特点: 
     *  1).该对象既能读, 又能写
     *  2).该对象内部维护一个byte数组, 并通过指针可以操作数组中的元素
     *  3).可以通过getFilePointer方法获取指针的位置, 和通过seek方法设置指针的位置
     *  4).其实该对象就是将字节输入流和输出流进行了封装
     *  5).该对象的源或汇只能是文件. 通过构造函数就可以看出
     */
    public class RandomAccessFileDemo {
        public static void main(String[] args) throws IOException {
    //      writeFile();
    //      readFile();
            randomWrite();
        }
        
        public static void randomWrite() throws IOException {
            RandomAccessFile raf = new RandomAccessFile("ranacc.txt", "rw");
            // 往指定位置写入数据。
            raf.seek(3 * 8);
            raf.write("哈哈".getBytes());
            raf.writeInt(108);
            raf.close();
        }
        
        public static void readFile() throws IOException {
            RandomAccessFile raf = new RandomAccessFile("ranacc.txt", "r");
            // 通过seek设置指针的位置。
            raf.seek(1 * 8);// 随机的读取。只要指定指针的位置即可。
            byte[] buf = new byte[4];
            raf.read(buf);
            String name = new String(buf);
            int age = raf.readInt();
            System.out.println("name=" + name);
            System.out.println("age=" + age);
            System.out.println("pos:" + raf.getFilePointer());
            raf.close();
        }
    
        // 使用RandomAccessFile对象写入一些人员信息,比如姓名和年龄。
        public static void writeFile() throws IOException {
            // 如果文件不存在,则创建,如果文件存在,不创建
            RandomAccessFile raf = new RandomAccessFile("ranacc.txt", "rw");
            raf.write("张三".getBytes());
            raf.writeInt(97);
            raf.write("小强".getBytes());
            raf.writeInt(99);
            raf.close();
        }
    }
    

    相关文章

      网友评论

          本文标题:第9章 - Java IO

          本文链接:https://www.haomeiwen.com/subject/uumdbctx.html