美文网首页中北软院创新实验室
Java I/O源码分析 - InputStream,Outpu

Java I/O源码分析 - InputStream,Outpu

作者: HikariCP | 来源:发表于2018-09-29 14:49 被阅读25次

    说明

    整个系列的文章全部参考或直接照搬下面两位作者的文章,这里只是根据自己需要对原作者的文章梳理的总结,仅给自己日后复习时提供思路,如有读者看到学习时建议移步原作。再次重申并非我所写

    另一篇本人总结的IO系列

    HikariCP:Java IO源码分析 - Reader,Writer系列(一)

    HikariCP:Java IO源码分析 - Reader,Writer系列(二)

    IntputStream,OutputStream 简介

    • 所有字节输入流的类的父类 IntputStream。
    • 所有字节输出流的类的父类 OutputStream。

    助于理解

    无论是输入流还是输出流,都是相对于内存的,即内存数据的输入还是输出,所以InputStream就是往内存输入数据的输入流。对于内存的动作就是read读取。相对的OutputStream就是从内存中往外输出数据,对于内存的动作的就是write操作。

    public abstract class InputStream implements Closeable {
    }
    
    public abstract class OutputStream implements Closeable, Flushable {
    }
    

    所有字节输入流的父类 InputStream 有这样一个抽象方法:

    public abstract int read() throws IOException;
    

    所以字节输入流必须提供返回下一个输入字节的read()方法。

    ByteArrayInputStream

    • ByteArrayInputStream 支持 mark/reset。
    • ByteArrayInputStream的close方法无效,无法关闭此输入流。
    public void mark(int readAheadLimit) {
        // 设置流中的当前标记位置
        mark = pos;
    }
    
    public synchronized void reset() {
        // 将缓冲区的位置重置为标记位置
        pos = mark;
    }
    
    public void close() throws IOException {
    }
    

    实现了父类InputStream的read方法。

    /*
     * 返回一个 0 到 255 范围内的 int 字节值。
     * 负数用补码进行计算
     */
    public synchronized int read() {
        return (pos < count) ? (buf[pos++] & 0xff) : -1;
    }
    

    需要注意的是,如果buf数组中有负数的话,负数在取出时&与运算用负数的补码(除符号位全部取反并+1)进行计算。

    ByteArrayOutputStream

    public class ByteArrayOutputStream extends OutputStream {
    
    /**
     * The buffer where data is stored.
     * 存储数据的缓冲区
     */
    protected byte buf[];
    
    /**
     * The number of valid bytes in the buffer.
     * 缓冲区中的有效字节数
     */
    protected int count;
    }
    
    // 缓冲区容量初始化为32,如有必要可扩容。通过ensureCapacity
    public ByteArrayOutputStream() {
        this(32);
    }
    
    public ByteArrayOutputStream(int size) {
        if (size < 0) {
            throw new IllegalArgumentException("Negative initial size: "
                                               + size);
        }
        buf = new byte[size];
    }
    

    ensureCapacity,grow,hugeCapacity

    // 确保缓冲区可以存放多少元素,必要时扩容
    private void ensureCapacity(int minCapacity) {
        // overflow-conscious code
        if (minCapacity - buf.length > 0)
            grow(minCapacity);
    }
    
    // 增加缓冲区容量,使其至少可以存放minCapacity个元素
    // minCapacity : 期望最小容量
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = buf.length;
        // 扩容2倍
        int newCapacity = oldCapacity << 1;
        if (newCapacity - minCapacity < 0)
            // 如果扩容两倍还是小,那么容量赋值成该期望容量
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            // 如果还是不够,那么最大提升到 Integer.MAX_VALUE 舍弃到了头信息
            newCapacity = hugeCapacity(minCapacity);
        buf = Arrays.copyOf(buf, newCapacity);
    }
    
    // 计算允许分配给byte数组的最大容量
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    
    // 一些虚拟机会存一些头信息到数组中,如数组的地址类型等,提升性能
    // JVM默认规定数组最大容量就是Integer.MAX_VALUE,再打会内存溢出
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    

    write,writeTo

    // 将指定的字节写入输出流
    public synchronized void write(int b) {
        ensureCapacity(count + 1);
        buf[count] = (byte) b;
        count += 1;
    }
    
    implement了父类OutputStream抽象类的write方法
    public abstract void write(int b) throws IOException;
    
    // 将指定byte数组中从偏移量off开始的len个字节写入输出流
    public synchronized void write(byte b[], int off, int len) {
        if ((off < 0) || (off > b.length) || (len < 0) ||
            ((off + len) - b.length > 0)) {
            throw new IndexOutOfBoundsException();
        }
        ensureCapacity(count + len);
        System.arraycopy(b, off, buf, count, len);
        count += len;
    }
    
    // 将此byte数组输出流的全部内容写入到指定的输出流参数out中
    public synchronized void writeTo(OutputStream out) throws IOException {
        out.write(buf, 0, count);
    }
    

    重要函数

    // 将输出流的count字段重置为零,从而丢弃输出流中目前已累积的所有输出
    public synchronized void reset() {
        count = 0;
    }
    
    // 使用指定的charsetName,通过解码字节将缓冲区内容转换为字符串并返回
    public synchronized String toString(String charsetName) throws UnsupportedEncodingException {
        return new String(buf, 0, count, charsetName);
    }
    
    public void close() throws IOException {
    }
    

    总结

    • ByteArrayOutputStream中的数据被写入到一个byte数组里。byte数组会随着被写入其中的数据的增长而增长。
    • 表示字节输出流的类必须提供至少一种可写入一个输出字节的方法。ByteArrayOutputStream提供了两种。加上继承自父类OuputStream类的write方法是3种
    • ByteArrayOutputStream可以将缓冲区中的数据转化为byte数组或字符串并返回。
    • ByteArrayOutputStream可以通过writeTo( OutputStream out)实现输出流之间数据的复制
    • ByteArrayOutputStream 的close方法无效,无法关闭此输出流。

    PipedInputStream,PipedOutputStream

    PipedInputStream与PipedOutputStream分别为管道输入流和管道输出流。管道输入流通过连接到管道输出流实现了类似管道的功能,用于线程之间的通信。

    通常,由某个线程向管道输出流中写入数据。根据管道的特性,这些数据会自动发送到与管道输出流对应的管道输入流中。这时其他线程就可以从管道输入流中读取数据,这样就实现了线程之间的通信。

    public class PipedInputStream extends InputStream
    
    

    initPipe,connect,

    /**
     * 初始化PipedInputStream的缓冲区大小
     *
     * @param pipeSize 管道缓冲区容量
     */
    private void initPipe(int pipeSize) {
        if (pipeSize <= 0) {
            throw new IllegalArgumentException("Pipe Size <= 0");
        }
        buffer = new byte[pipeSize];
    }
    
    /**
     * 将PipedInputStream连接到指定的PipedOutputStream。
     *
     * 如果 PipedInputStream 已经被连接到了其他 PipedOutputStream,
     * 或者PipedOutputStream 已经被连接到其他PipedInputStream 
     * 抛出IOException。
     */
    public void connect(PipedOutputStream src) throws IOException {
        src.connect(this);
    }
    

    receive,awaitSpace,checkStateForReceive

    // 接收一个数据字节,将其插入到缓冲区。如果没有可用的输入,方法会阻塞
    protected synchronized void receive(int b) throws IOException {
        // 接受前的状态检查
        checkStateForReceive();
        // 设置负责向管道缓冲区输入数据的线程是当前线程
        writeSide = Thread.currentThread();
        // 如果缓冲区被塞满的时候
        if (in == out)
            // 缓冲区被写入线程塞满的时候,唤醒读取线程,并阻塞当前写入线程
            awaitSpace();
        // 初次接受前初始化
        if (in < 0) {
            in = 0;
            out = 0;
        }
        buffer[in++] = (byte) (b & 0xFF);
        // 从头复入
        if (in >= buffer.length) {
            in = 0;
        }
    }
    
    // 检查PipedInputStream是否可以接收数据
    private void checkStateForReceive() throws IOException {
        if (!connected) {
            throw new IOException("Pipe not connected");
        } else if (closedByWriter || closedByReader) {// 输入输出流都不能被关闭
            throw new IOException("Pipe closed");
        } else if (readSide != null && !readSide.isAlive()) {// switSpace 需要用到读线程,读线程不能为空且不alive
            throw new IOException("Read end dead");
        }
    }
    
    // 等待可用缓冲区
    private void awaitSpace() throws IOException {
        while (in == out) {
            checkStateForReceive();
    
            /* full: kick any waiting readers */
            notifyAll();
            try {
                wait(1000);
            } catch (InterruptedException ex) {
                throw new java.io.InterruptedIOException();
            }
        }
    }
    
    // 接受指定字节数组的数据
    synchronized void receive(byte b[], int off, int len) throws IOException {
        // 检查接受状态
        checkStateForReceive();
        // 身份
        writeSide = Thread.currentThread();
        // 写入总量
        int bytesToTransfer = len;
        while (bytesToTransfer > 0) {
            // 判断缓冲区满没满
            if (in == out)
                awaitSpace();
            // 下次插入量
            int nextTransferAmount = 0;
            if (out < in) {
                nextTransferAmount = buffer.length - in;
            } else if (in < out) {
                // 初次写入
                if (in == -1) {
                    in = out = 0;
                    nextTransferAmount = buffer.length - in;
                } else {
                    // 再次写入
                    nextTransferAmount = out - in;
                }
            }
            // 如果 可插入的量 > 要插入的量,那么一次插入结束,此次插入的量就是写入线程要插入数据的总量,
            // 否则off记录偏移量,in记录存入位,bytesToTransfer记录剩余插入量,while循环批次执行。
            if (nextTransferAmount > bytesToTransfer)
                nextTransferAmount = bytesToTransfer;
            assert (nextTransferAmount > 0);
            // 写入
            System.arraycopy(b, off, buffer, in, nextTransferAmount);
            bytesToTransfer -= nextTransferAmount;
            off += nextTransferAmount;
            in += nextTransferAmount;
            if (in >= buffer.length) {
                in = 0;
            }
        }
    }
    

    receivedLast

    // 管道输出流关闭时(PipedOutputStream.close()中会调用此方法),通知其已经关闭。
    synchronized void receivedLast() {
        // 状态设置
        closedByWriter = true;
        notifyAll();
    }
    

    read

    public synchronized int read() throws IOException {
        // 状态检测
        if (!connected) {// 输入输出流是否连接上
            throw new IOException("Pipe not connected");
        } else if (closedByReader) {// 管道输入流被关闭
            throw new IOException("Pipe closed");
        } else if (writeSide != null && !writeSide.isAlive() // 写线程存在但不alive,管道输出流没关 且现在管道中没数据
                && !closedByWriter && (in < 0)) {
            throw new IOException("Write end dead");
        }
    
        // 状态设置,当前线程为读取线程
        readSide = Thread.currentThread();
        // 尝试次数
        int trials = 2;
        // 管道中没数据
        while (in < 0) {
            // 如果管道输出流关闭,且此时in<0 管道缓冲区没机会再写入内容了,read 返回 return -1
            if (closedByWriter) {
                /* closed by writer, return EOF */
                return -1;
            }
            // 如果写入数据的线程不为null且不活跃且trials<=0,说明管道损坏,抛出异常
            if ((writeSide != null) && (!writeSide.isAlive()) && (--trials < 0)) {
                throw new IOException("Pipe broken");
            }
            /* might be a writer waiting */ // 唤醒写入
            notifyAll();
            try {
                wait(1000);
            } catch (InterruptedException ex) {
                throw new java.io.InterruptedIOException();
            }
        }
        int ret = buffer[out++] & 0xFF;
        // 读完一轮,复位
        if (out >= buffer.length) {
            out = 0;
        }
        // 管道缓冲区为空,读完
        if (in == out) {
            /* now empty */
            in = -1;
        }
    
        return ret;
    }
    
    public synchronized int read(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }
    
        /* possibly wait on the first character */
        // 尝试读一个字节,看缓冲区情况
        int c = read();
        if (c < 0) {
            return -1;
        }
        b[off] = (byte) c;
        // readLength
        int rlen = 1;
        while ((in >= 0) && (len > 1)) {
    
            int available;
    
            if (in > out) {
                // 可读数
                available = Math.min((buffer.length - out), (in - out));
            } else {
                available = buffer.length - out;
            }
    
            // A byte is read beforehand outside the loop
            // 可读数 > 要读数
            if (available > (len - 1)) {
                available = len - 1;
            }
            System.arraycopy(buffer, out, b, off + rlen, available);
            // 一次读取影响到的变量统一变更
            out += available;
            rlen += available;
            len -= available;
    
            // 继续从头读
            if (out >= buffer.length) {
                out = 0;
            }
            // 缓冲区读完
            if (in == out) {
                /* now empty */
                in = -1;
            }
        }
        return rlen;
    }
    

    available,close

    public synchronized int available() throws IOException {
        if (in < 0)
            return 0;
        else if (in == out)// 读完被置为-1 所以这里肯定是满了
            return buffer.length;
        else if (in > out)
            return in - out;
        else
            return in + buffer.length - out;
    }
    
    /**
     * 关闭此管道输入流,并释放与该流相关的所有系统资源。
     */
    public void close()  throws IOException {
        // 状态设置
        closedByReader = true;
        synchronized (this) {
            in = -1;// 
        }
    }
    

    PipedOutputStream

    public class PipedOutputStream extends OutputStream {
        
        private PipedInputStream sink; // 与PipedOutputStream相连接的管道输入流
        
        // 创建连接到指定输入流的管道输出流
        public PipedOutputStream(PipedInputStream snk)  throws IOException {
            connect(snk);
        }
        
        // 创建没有连接到输入流的管道输出流。
        // 在使用前,它必须连接到管道输入流。
        public PipedOutputStream() {
        }
    }
    

    connect,write

    public synchronized void connect(PipedInputStream snk) throws IOException {
        if (snk == null) {
            throw new NullPointerException();
        } else if (sink != null || snk.connected) {// 此管道输出流已经与某管道输入流连接 或 该管道输入流已经被连接
            throw new IOException("Already connected");
        }
        sink = snk;
        snk.in = -1;
        snk.out = 0;
        snk.connected = true;
    }
    
    // 将指定数据字节写入管道输出流
    public void write(int b)  throws IOException {
        if (sink == null) {
            throw new IOException("Pipe not connected");
        }
        sink.receive(b);
    }
    
    // 将指定字节数组的数组写入到管道缓冲区中
    public void write(byte b[], int off, int len) throws IOException {
        // 数据校验
        if (sink == null) {
            throw new IOException("Pipe not connected");
        } else if (b == null) {
            throw new NullPointerException();
        } else if ((off < 0) || (off > b.length) || (len < 0) ||
                   ((off + len) > b.length) || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }
        // 调用管道输入流的receive函数处理
        sink.receive(b, off, len);
    }
    

    close,receivedLast,flush

    public void close()  throws IOException {
        if (sink != null) {
            sink.receivedLast();
        }
    }
    
    // PipedInputStream类的receivedLast函数
    synchronized void receivedLast() {
        // 状态设置,负责缓冲区数据写入的流被关闭了。
        closedByWriter = true;
        notifyAll();
    }
    
    /**
     * 刷新此输出流并强制写出所有缓冲的输出字节。
     * 这将通知所有读取数据的线程,告知它们管道中的字符处于读取等待中。
     */
    public synchronized void flush() throws IOException {
        if (sink != null) {
            synchronized (sink) {
                sink.notifyAll();
            }
        }
    }
    

    总结

    首先在看PipedInputStream和PipedOutputStream的时候,我刚开始没搞懂为什么PipedInputStream技能read又能recieve。后来看了PipedOutputStream的源码的时候才知道,原来PipedInputStream类中的recieve函数是给PipedOutputStream类中的write函数调用的,后来才串明白,这是一个“管道”的输入输出流,用一个容量默认为1024的byte数组来做管道的缓冲容量,最终的输入输出实现都落实到了PipedInputStream类中,这样状态都由一个类来控制才能做到,某个线程通过管道输出流向管道中写入数据,另一端管道输入流能立马从管道中取出对应存储到的数据。

    • PipedInputStream与PipedOutputStream分别为管道输入流和管道输出流。管道输入流通过连接到管道输出流实现了类似管道的功能,用于线程之间的通信。
    • 通常,由某个线程向管道输出流中写入数据。根据管道的特性,这些数据会自动发送到与管道输出流对应的管道输入流中。这时其他线程就可以从管道输入流中读取数据,这样就实现了线程之间的通信。
    • 不建议对这两个流对象尝试使用单个线程,因为这样可能死锁线程。
    • PipedOutputStream是数据的发送者;PipedInputStream是数据的接收者。
    • PipedInputStream缓冲区大小默认为1024,写入数据时写入到这个缓冲区的,读取数据也是从这个缓冲区中读取的。
    • PipedInputStream通过read方法读取数据。PipedOutputStream通过write方法写入数据,write方法其实是调用PipedInputStream中的receive方法来实现将数据写入缓冲区的的,因为缓冲区是在PipedInputStream中的。
    • PipedOutputStream和PipedInputStream之间通过connect()方法连接。
    • 使用后要关闭输入输出流

    FilterInputStream,FilterOutputStream

    FilterInputStream、FilterOutputStream是过滤器字节输入输出流。它们的主要用途在于封装其他的输入输出流,为它们提供一些额外的功能。 - 装饰者模式

    package java.io;
    
    public class FilterInputStream extends InputStream {
        protected volatile InputStream in;
    
        protected FilterInputStream(InputStream in) {
            this.in = in;
        }
    
        public int read() throws IOException {
            return in.read();
        }
    
        public int read(byte b[]) throws IOException {
            return read(b, 0, b.length);
        }
    
        public int read(byte b[], int off, int len) throws IOException {
            return in.read(b, off, len);
        }
    
        public long skip(long n) throws IOException {
            return in.skip(n);
        }
    
        public int available() throws IOException {
            return in.available();
        }
    
        public void close() throws IOException {
            in.close();
        }
    
        public synchronized void mark(int readlimit) {
            in.mark(readlimit);
        }
    
        public synchronized void reset() throws IOException {
            in.reset();
        }
    
        public boolean markSupported() {
            return in.markSupported();
        }
    }
    

    可以从源码看出,FilterInuptStream类本身并没有对构造时传入的InputStream抽象类的实例进行装饰,只是简单的重写了父类InputStream类的所有方法。

    可以看出FilterInputStream在这里做的是装饰抽象类。而InputStream做的是抽象构建。

    根据装饰模式的设计思想我们可以得知,虽然FilterInutStream类并不为具体构建提供装饰功能。但其子类在装饰模式中充当的是具体装饰类,可以进一步重写这些方法中的一些方法,来提供装饰功能。它的常用子类有BufferedInputStreamDataInputStream。比如,BufferedInputStream的作用就是为它装饰的输入流提供缓冲功能。

    至此:

    • InputStream:抽象构建
    • ***InputStream:具体构建
    • FilterInputStream:抽象装饰类
    • BufferedInputStream:具体装饰类

    filterOutputStream

    public class FilterOutputStream extends OutputStream {
     
        protected OutputStream out;
    
        public FilterOutputStream(OutputStream out) {
            this.out = out;
        }
    
        public void write(int b) throws IOException {
            out.write(b);
        }
    
        public void write(byte b[]) throws IOException {
            write(b, 0, b.length);
        }
    
        public void write(byte b[], int off, int len) throws IOException {
            if ((off | len | (b.length - (len + off)) | (off + len)) < 0)
                throw new IndexOutOfBoundsException();
    
            for (int i = 0 ; i < len ; i++) {
                write(b[off + i]);
            }
        }
    
        public void flush() throws IOException {
            out.flush();
        }
    
        @SuppressWarnings("try")
        public void close() throws IOException {
            try (OutputStream ostream = out) {
                flush();
            }
        }
    }
    

    需要注意的是看了FilterInputStream源码后不要想当然的认为FilterOutputStream也和它一样全篇方法调用装饰的OutputStream的子类的原方法。其也重写的父类的OutputStream全部方法,并有一些赋予了自己的处理逻辑。

    总结

    • FilterInputStream、FilterOutputStream是过滤器字节输入输出流。它们的主要用途在于封装其他的输入输出流,为它们提供一些额外的功能。
    • FilterInputStream、FilterOutputStream并没有提供什么装饰功能。FilterInputStream、FilterOutputStream的子类可进一步重写这些方法中的一些方法,来提供装饰功能。
    • FilterInputStream装饰功能的实现的关键在于类中有一个InputStream字段,依赖这个字段它才可以对InputStream的子类提供装饰功能。FilterOutputStream也是如此。

    BufferedInputStream,BufferedOutputStream

    public class BufferedInputStream extends FilterInputStream {
    
    
    
        private static int DEFAULT_BUFFER_SIZE = 8192;// 1024 << 3; 缓冲区默认的默认大小
        private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;// 分派给arrays的最大容量
        protected volatile byte buf[];// 存放数据的内部缓冲数组。,如果有必要,它可能被不同大小的数组替代
        // 当前缓冲区的有效字节数。
        // 注意,这里是指缓冲区的有效字节数,而不是输入流中的有效字节数。
        protected int count;
        // 当前缓冲区的位置索引
        // 注意,这里是指缓冲区的位置索引,而不是输入流中的位置索引。
        protected int pos;
        // 当前缓冲区的标记位置
        // markpos和reset()配合使用才有意义。操作步骤:
        // (01) 通过mark() 函数,保存pos的值到markpos中。
        // (02) 通过reset() 函数,会将pos的值重置为markpos。接着通过read()读取数据时,就会从mark()保存的位置开始读取。
        // 可以理解为,mark位置之后的数据是保留数据,即有效数据。mark确立了有效数据和无效数据。
        protected int markpos = -1;
        // 相当于从输入流中一次读取数据的大小。当buffer.length小于这个值的时候就需要频繁的扩容,当大于这个值的时候就可以直接从输入流中读取数据。
        protected int marklimit;
         // 缓存数组的原子更新器。
         // 该成员变量与buf数组的volatile关键字共同组成了buf数组的原子更新功能实现,
         // 即,在多线程中操作BufferedInputStream对象时,buf和bufUpdater都具有原子性(不同的线程访问到的数据都是相同的)
        private static final
            AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
            AtomicReferenceFieldUpdater.newUpdater
            (BufferedInputStream.class,  byte[].class, "buf");
    }
    

    BufferedInputStream的作用是为其它输入流提供缓冲功能。创建BufferedInputStream时,我们会通过它的构造函数指定某个输入流为参数。BufferedInputStream会将该输入流数据分批读取,每次读取一部分到缓冲中;操作完缓冲中的这部分数据之后,再从输入流中读取下一部分的数据。(即对应read时发现缓冲区数据不够时调用fill函数,fill函数内部调用read函数读取输入流中的数据再填充buf缓冲区。)

    为什么需要缓冲呢?原因很简单,效率问题!缓冲中的数据实际上是保存在内存中,而原始数据可能是保存在硬盘或NandFlash等存储介质中;而我们知道,从内存中读取数据的速度比从硬盘读取数据的速度至少快10倍以上。
    那干嘛不干脆一次性将全部数据都读取到缓冲中呢?第一,读取全部的数据所需要的时间可能会很长。第二,内存价格很贵,容量不像硬盘那么大。

    该类最关键的函数及fill()方法,其他方法都很好理解。该方法负责读取输入流的数据来填充buf缓冲区。具体解释可参考http://www.cnblogs.com/skywang12345/p/io_12.html

    BufferedOutputStream

    看过BufferedInputStream源码之后BufferedOutputStream就看起来很简单了,和BufferedInputStream一样,BufferedOutputStream通过字节数组来缓冲数据(1024*8)。BufferedOutputStream当缓冲区满或者用户调用flush()函数时,它就会将缓冲区的数据写入到输出流中。

    总结

    • BufferedInputStream是缓冲输入流,作用是为另一个输入流添加一些功能,比如缓冲输入功能以及支持mark和reset方法的能力。
    • BufferedOutputStream是缓冲输出流,通过设置这种输出流,应用程序就可以将单个或字节数组缓冲的写入底层输出流中,而不必针对每次字节写入调用底层系统。

    DataInputStream,DataOutputStream

    DataInputStream

    • DataInputStream为数据输入流,它允许应用程序以与机器无关方式从底层输入流中读取基本Java数据类型。
    • DataOutputStream为数据输出流,它允许应用程序以适当方式将基本 Java数据类型写入输出流中。
    public final String readUTF() throws IOException {
        return readUTF(this);
    }
    
    public final static String readUTF(DataInput in) throws IOException {
        int utflen = in.readUnsignedShort();
        byte[] bytearr = null;
        char[] chararr = null;
        if (in instanceof DataInputStream) {
            DataInputStream dis = (DataInputStream)in;
            if (dis.bytearr.length < utflen){
                dis.bytearr = new byte[utflen*2];
                dis.chararr = new char[utflen*2];
            }
            chararr = dis.chararr;
            bytearr = dis.bytearr;
        } else {
            bytearr = new byte[utflen];
            chararr = new char[utflen];
        }
    
        int c, char2, char3;
        int count = 0;
        int chararr_count=0;
    
        in.readFully(bytearr, 0, utflen);
    
        // 由于UTF-8的单字节和ASCII相同,所以这里就将它们进行预处理,直接保存到“字符数组chararr”中。
        // 对于其它的UTF-8数据,则在后面进行处理。
        while (count < utflen) {
            c = (int) bytearr[count] & 0xff;
            if (c > 127) break;
            count++;
            chararr[chararr_count++]=(char)c;
        }
    
        while (count < utflen) {
            c = (int) bytearr[count] & 0xff;
            switch (c >> 4) {
                case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
                    /* 0xxxxxxx*/
                    count++;
                    chararr[chararr_count++]=(char)c;
                    break;
                case 12: case 13:
                    /* 110x xxxx   10xx xxxx*/
                    count += 2;
                    if (count > utflen)
                        throw new UTFDataFormatException(
                            "malformed input: partial character at end");
                    char2 = (int) bytearr[count-1];
                    if ((char2 & 0xC0) != 0x80)
                        throw new UTFDataFormatException(
                            "malformed input around byte " + count);
                    chararr[chararr_count++]=(char)(((c & 0x1F) << 6) |
                                                    (char2 & 0x3F));
                    break;
                case 14:
                    /* 1110 xxxx  10xx xxxx  10xx xxxx */
                    count += 3;
                    if (count > utflen)
                        throw new UTFDataFormatException(
                            "malformed input: partial character at end");
                    char2 = (int) bytearr[count-2];
                    char3 = (int) bytearr[count-1];
                    if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
                        throw new UTFDataFormatException(
                            "malformed input around byte " + (count-1));
                    chararr[chararr_count++]=(char)(((c     & 0x0F) << 12) |
                                                    ((char2 & 0x3F) << 6)  |
                                                    ((char3 & 0x3F) << 0));
                    break;
                default:
                    /* 10xx xxxx,  1111 xxxx */
                    throw new UTFDataFormatException(
                        "malformed input around byte " + count);
            }
        }
        // The number of chars produced may be less than utflen
        return new String(chararr, 0, chararr_count);
    }
    

    readUTF的执行流程相当于把UTF-8编码的输入流中的字节数据先读到了bytearr数组中,然后根据UTF-8编码的特殊性,判断数据是几个字节,根据情况又往chararr数组中转。保证了每个字符转化的正确率,最后所有的字符都正确的转化到了chararr数组中,然后返回string值。

    readUnsignedShort,readBoolean,readUnsignedByte

    // UTF-8输入流的前2个字节是数据的长度
    public final int readUnsignedShort() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (ch1 << 8) + (ch2 << 0);
    }
    
    // 此方法适用于读取用接口DataOutput的 writeBoolean方法写入的字节
    public final boolean readBoolean() throws IOException {
        //从输入流中读取一个字节
        int ch = in.read();
        //如果达到输入流末尾,抛出异常
        if (ch < 0)
            throw new EOFException();
        //如果读取的字节不是零,则返回true;如果是零,则返回false
        return (ch != 0);
    }
    
    // 读取一个无符号为byte输入字节,将它数值位左侧补零转变为int类型(覆盖符号位),并返回结果,所以结果的范围是0到255
    public final int readUnsignedByte() throws IOException {
        int ch = in.read();
        if (ch < 0)
            throw new EOFException();
        return ch;
    }
    

    最后我们落实到该类的特点,它允许应用程序以与机器无关方式从底层输入流中读取基本Java数据类型。

    其实就是通过构造时传入的InputStream对象来根据要读取的数据类型来读取对应的字节数,比如readByte就调用一次in.read。然后把读取出来的数据强转成(byte)。readChar的话就调用两次,然后因为两个字节连续起来表示一个字符,那么就将先读出来的字节适当的移位并将两个字节+起来。这样就相当于一次性读出来两个字节,然后对该数据强转即可得到最真实的数据。

    需要注意的是,因为我们从输入流中读出的内容返回的是int类型的,默认除了数据位,把我们原数据的符号位覆盖掉了。然后我们强转就可以就可以恢复(暂时先那么理解有符号位和无符号位的计算方式,方便记忆,虽然肯定不是这样,以后再研究。)

    DataOutputStream

    incCount,flush,size

    // 到目前为止写入到输出流中的字节数 最大值为Integer.MAX_VALUE
    protected int written;
    
    // 增加wirtten的值。最大值为Integer.MAX_VALUE
    private void incCount(int value) {
        int temp = written + value;
        //int允许的最大值为Integer.MAX_VALUE,即2147483647,2147483647+1即为负数
        if (temp < 0) {
            temp = Integer.MAX_VALUE;
        }
        written = temp;
    }
    
    //  清空此数据输出流。这迫使所有缓冲的输出字节被写出到流中。
    public void flush() throws IOException {
        out.flush();
    }
    
    // 返回written的当前值,即到目前为止写入此数据输出流的字节数。最大值为Integer.MAX_VALUE。
    public final int size() {
        return written;
    }
    

    writeShort,writeChar,writeFloat

    
    public final void writeChar(int v) throws IOException {
        out.write((v >>> 8) & 0xFF);
        out.write((v >>> 0) & 0xFF);
        incCount(2);
    }
    
    public final void writeChars(String s) throws IOException {
        int len = s.length();
        for (int i = 0 ; i < len ; i++) {
            int v = s.charAt(i);
            out.write((v >>> 8) & 0xFF);
            out.write((v >>> 0) & 0xFF);
        }
        incCount(len * 2);
    }
    
    // 使用Float类中的floatToIntBits方法将float参数转换为一个int值
    public final void writeFloat(float v) throws IOException {
        writeInt(Float.floatToIntBits(v));
    }
    
    

    可以看到DataOutputStream和DataInputStream的处理方式是一样的,要输入到输出流的数据有可能是1个字节或多个字节的,所以对应不同的数据定义了不同的函数,然后针对这些数值一个字节一个字节的进行输入就好。

    总结

    • DataInputStream提供了一系列从二进制流中读取字节,并根据所有Java基本类型数据进行重构的readXXXX方法。同时还提供根据UTF-8编码格式的数据写入输入流的方式,即readUTF方法。
    • DataOutputStream提供了一系列将数据从任意Java基本类型转换为一系列字节,并将这些字节写入二进制流的writeXXXX方法。同时还提供了一个将String转换成UTF-8修改版格式并写入所得到的系列字节的工具,即writeUTF方法。

    PrintStream

    由于学习的时候,我在学习字节流的时候跳过了PrintStream,先看的PrintWriter所以看过PrintWriter后再来看PrintStream就感觉很简单了,所以简单记录下。

    • PrintStream 是打印输出流,它继承于FilterOutputStream。
    • PrintStream 是用来装饰其它输出流。它能为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式。为底层输出流提供了缓存池(BufferedWriter)。
    • 与其他输出流不同,PrintStream 永远不会抛出 IOException;它产生的IOException会被自身的函数所捕获并设置错误标记, 用户可以通过 checkError() 返回错误标记,从而查看PrintStream内部是否产生了IOException。
    • 另外,PrintStream 提供了自动flush 和 字符集设置功能。所谓自动flush,就是往PrintStream写入的数据会立刻调用flush()函数。
    public class PrintStream extends FilterOutputStream
        implements Appendable, Closeable
    {
        // 自动flush
        // 所谓“自动flush”,就是每次执行print(), println(), write()函数,都会调用flush()函数;
        // 而“不自动flush”,则需要我们手动调用flush()接口。
        private final boolean autoFlush;
        // PrintStream是否右产生异常。当PrintStream有异常产生时,会被本身捕获,并设置trouble为true
        private boolean trouble = false;
        // 用于格式化的对象
        private Formatter formatter;
    
        // BufferedWriter对象,用于实现“PrintStream支持字符集”。
        // 因为PrintStream是OutputStream的子类,所以它本身不支持字符串;
        // 但是BufferedWriter支持字符集,因此可以通过OutputStreamWriter创建PrintStream对应的BufferedWriter对象,从而支持字符集。
        private BufferedWriter textOut;
        private OutputStreamWriter charOut;
    
        private static <T> T requireNonNull(T obj, String message) {
            if (obj == null)
                throw new NullPointerException(message);
            return obj;
        }
    }
    

    ==需要注意的是== :很明显,该类和PrintWriter还有个最大的区别。继承自FilterOutputStream,也就是它做的是装饰模式中的具体装饰类。至于Appendable,Closeable接口和PrintWriter则没区别,PrintWriter其父类Writer抽象也早实现了。

    public class PrintStream extends FilterOutputStream
        implements Appendable, Closeable
    {
    

    PrintStream和DataOutputStream异同点

    相同点:都是继承与FileOutputStream,用于包装其它输出流。

    不同点:

    1. PrintStream和DataOutputStream 都可以将数据格式化输出;但它们在“输出字符串”时的编码不同
      • PrintStream是输出时采用的是用户指定的编码(创建PrintStream时指定的),若没有指定,则采用系统默认的字符编码。而DataOutputStream则采用的是UTF-8。
    2. 它们的写入数据时的异常处理机制不同。
      • DataOutputStream在通过write()向“输出流”中写入数据时,若产生IOException,会抛出。
      • 而PrintStream在通过write()向“输出流”中写入数据时,若产生IOException,则会在write()中进行捕获处理;并设置trouble标记(用于表示产生了异常)为true。用户可以通过checkError()返回trouble值,从而检查输出流中是否产生了异常。
    3. 构造函数不同
      • DataOutputStream的构造函数只有一个:DataOutputStream(OutputStream out)。即它只支持以输出流out作为“DataOutputStream的输出流”。
      • 而PrintStream的构造函数有许多:和DataOutputStream一样,支持以输出流out作为“PrintStream输出流”的构造函数;还支持以“File对象”或者“String类型的文件名对象”的构造函数。
      • 而且,在PrintStream的构造函数中,能“指定字符集”和“是否支持自动flush()操作”。
    4. 目的不同
      • DataOutputStream的作用是装饰其它的输出流,它和DataInputStream配合使用:允许应用程序以与机器无关的方式从底层输入流中读写java数据类型
      • 而PrintStream的作用虽然也是装饰其他输出流,但是它的目的不是以与机器无关的方式从底层读写java数据类型;而是为其它输出流提供打印各种数据值表示形式,使其它输出流能方便的通过print(),println()或printf()等输出各种格式的数据。

    相关文章

      网友评论

        本文标题:Java I/O源码分析 - InputStream,Outpu

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