中北软院创新实验室

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

2018-09-29  本文已影响25人  HikariCP

说明

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

另一篇本人总结的IO系列

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

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

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

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 {
}

总结

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类中,这样状态都由一个类来控制才能做到,某个线程通过管道输出流向管道中写入数据,另一端管道输入流能立马从管道中取出对应存储到的数据。

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的作用就是为它装饰的输入流提供缓冲功能。

至此:

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全部方法,并有一些赋予了自己的处理逻辑。

总结

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()函数时,它就会将缓冲区的数据写入到输出流中。

总结

DataInputStream,DataOutputStream

DataInputStream

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个字节或多个字节的,所以对应不同的数据定义了不同的函数,然后针对这些数值一个字节一个字节的进行输入就好。

总结

PrintStream

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

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()等输出各种格式的数据。
上一篇下一篇

猜你喜欢

热点阅读