Java I/O源码分析 - InputStream,OutputStream系列

说明

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

另一篇本人总结的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()等输出各种格式的数据。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348