这是《水煮 JDK 源码》系列 的第2篇文章,计划撰写100篇关于JDK源码相关的文章

ByteArrayOutputStream 类位于 java.io 包下,继承于 OutputStream 类,从字面上可以看出,它表示的是一个字节数组输出流。它的实现方式是先在内存中创建一个字节数组缓冲区 byte buf[],然后把所有发送到输出流的数据保存于字节数组缓冲区中,其中字节数组缓冲区会随着数据的增加而自动调整大小,其UML 类图如下:

::: hljs-center

image20220728144910896.png

:::

1、构造函数

ByteArrayOutputStream 类提供两个构造方法,分别如下:

public ByteArrayOutputStream() {
    this(32);
}

public ByteArrayOutputStream(int size) {
    // 传入的 size 不能小于 0
    if (size < 0) {
        throw new IllegalArgumentException("Negative initial size: "
                                           + size);
    }
    buf = new byte[size];
}

无参构造方法默认创建一个32字节的缓冲区,而另一个构造方法则是创建指定大小为 size 的缓冲区。

ByteArrayOutputStream 类中有3个成员变量 buf[]countMAX_ARRAY_SIZE,其定义如下:

/**
 * 字节数组.
 */
protected byte buf[];

/**
 * 字节数组大小.
 */
protected int count;
/** 最大数组大小 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

在成功创建字节数组输出流后,就可以调用相应的方法进行操作,操作方法主要分为下面几类:

  • 写入字节数据方法

  • 字节数组扩容方法

  • 将字节数组转换为字符串方法

  • 其他方法

2、写入字节数据方法

ByteArrayOutputStream 提供了2个写入方法,1个写入到其他输出流方法,分别如下:

  • public synchronized void write(int b):将指定的字节写入字节数组输出流;
  • public synchronized void write(byte b[], int off, int len):将指定字节数组中从偏移量 off 开始的 len长度的字节写入字节数组输出流中
  • public synchronized void writeTo(OutputStream out) throws IOException:将字节数组输出流中的全部数据写入到输出流参数中,调用的是 OutputStreamwrite() 方法

这3个写入方法都使用 synchronized 关键字,即为同步方法。

public synchronized void write(int b) {
    // 首先检查字节数组大小,由于写入了 b,所以新的数组容量至少为 count + 1
    // count 代表之前写入的数据大小
    ensureCapacity(count + 1);
    // 写入的新数据存放在数组的最后
    buf[count] = (byte) b;
    count += 1;
}

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();
    }
    // 检查字节数组大小,由于要新写入len长度的字节数据,所以最小容量为 count + len
    ensureCapacity(count + len);
    // 复制数组 b[] 中的数据到 buf[] 中
    System.arraycopy(b, off, buf, count, len);
    count += len;
}

public synchronized void writeTo(OutputStream out) throws IOException {
    // 将字节数组中所有的数据全部写入到输出流参数中
    out.write(buf, 0, count);
}

在向字节数组中写入新数据时,首先要做的就是检查当前数组的容量,如果容量不足,则需要先对数组进行扩容,然后再保存数据;如果同时写入多个字节数据,将会使用 System.arraycopy() 方法进行数组拷贝,它是一个 native 方法,其定义如下:

public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

3、字节数组扩容方法

ByteArrayOutputStream 可以实现自动对字节数组进行扩容,当数组大小无法存放新的字节内容时,就会自动进行扩容,数组的扩容主要涉及到下面的三个方法:

  • private void ensureCapacity(int minCapacity):检查字节数组容量大小
  • private void grow(int minCapacity):字节数组扩容
  • private static int hugeCapacity(int minCapacity):检查是否超过最大容量,最大容量为 Integer.MAX_VALUE - 8

write() 方法中可以看到,当向字节数组中写入数据时,会先调用 ensureCapacity() 方法检查数组的容量,如果容量不足,则会进行扩容,其实现如下:

private void ensureCapacity(int minCapacity) {
    // overflow-conscious code
    // 如果数组容量不足,则进行扩容操作
    if (minCapacity - buf.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    // 原字节数组的长度
    int oldCapacity = buf.length;
    // 将原数组长度左移1位(相当于乘以2),得到新字节数组的长度
    // 扩容后的数组长度为扩容前的2倍
    int newCapacity = oldCapacity << 1;
    // 如果扩容后的数组长度小于最小所需长度的话,则新的长度等于最小所需长度
    // 比如原数组长度为32,保存了32个字节数据,现在如果新增1个字节,那么此时
    // 最小所需长度为33,而 newCapacity 此时为64
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 判断默认扩容后的数组大小是否超过了数组容量最大值
    // 如果默认扩容后的大小超过了最大值,则直接判断最小所需大小是否超过最大值
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 对原数组进行复制扩容
    buf = Arrays.copyOf(buf, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    // 如果所需最小容量小于0,直接抛出OOM错误
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    // 比较所需最小容量与最大数组大小,如果所需最小容量更大,则扩容后的数组长度为 Integer.MAX_VALUE
    // 否则扩容后长度为最大数组长度,即为 Integer.MAX_VALUE - 8
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

在对数据进行扩容操作时,默认的扩容策略是直接将数组大小增加1倍,如果扩容后仍然不够,则等于所需最小容量大小,然后在与数组最大值进行比较,判断扩容后的数组是否超过允许的最大值。

数据扩容时,调用的是 Arrays.copyOf() ,该方法是位于 java.util 包下 Arrays 类的方法, 其定义如下:

public static byte[] copyOf(byte[] original, int newLength) {
    // 创建一个长度为 newLength 的新字节数组
    byte[] copy = new byte[newLength];
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

Arrays.copyOf()方法的实现方式是先创建一个长度为 newLength 的新字节数组,然后使用 System.arraycopy() 方法进行数组数据的拷贝,最后返回新的数组,也就是说 ByteArrayOutputStream 进行数组扩容时,都会执行数据拷贝的动作。

4、将字节数组转换为字符串方法

ByteArrayOutputStream 提供了3个将字节数组转换为字符串的方法,其中有1个已经被废弃,具体如下:

  • public synchronized String toString():根据默认字符编码将缓冲区的字节内容转换为字符串
  • public synchronized String toString(String charsetName):根据指定字符编码将缓冲区的字节内容转换为字符串
  • public synchronized String toString(int hibyte) :该方法已废弃

3个 toString() 方法都是使用了关键词 synchronized,即为同步方法。

public synchronized String toString() {
    return new String(buf, 0, count);
}

public synchronized String toString(String charsetName)
    throws UnsupportedEncodingException
{
    // 根据指定字符编码将缓冲区的字节内容转换为字符串
    // charsetName 不能为 null
    return new String(buf, 0, count, charsetName);
}

@Deprecated
public synchronized String toString(int hibyte) {
   	// 已废弃
    return new String(buf, hibyte, 0, count);
}

将字节数组转换为字符串的实现都比较简单,都是直接 new String() 创建一个新的字符串,在转换的时候可以使用系统默认的字符编码,也可以使用指定的字符编码,关于上面两个 new String() 方法实现如下:

public String(byte bytes[], int offset, int length) {
    // 检查数组边界,避免数组越界取值
    checkBounds(bytes, offset, length);
    // 调用 StringCoding.decode 方法将字节数组转换为字符串,使用默认字符编码
    // 默认字符编码获取方式为 Charset.defaultCharset().name();
    this.value = StringCoding.decode(bytes, offset, length);
}

public String(byte bytes[], int offset, int length, String charsetName)
    throws UnsupportedEncodingException {
    // charsetName 不能为 null,否则会抛出空指针异常
    if (charsetName == null)
        throw new NullPointerException("charsetName");
    // 检查数组边界,避免数组越界取值
    checkBounds(bytes, offset, length);
    // 调用 StringCoding.decode 方法将字节数组转换为字符串
    this.value = StringCoding.decode(charsetName, bytes, offset, length);
}

关于系统默认的字符编码格式获取方法为:Charset.defaultCharset().name(),其具体实现如下:

public static Charset defaultCharset() {
    if (defaultCharset == null) {
        synchronized (Charset.class) {
            String csn = AccessController.doPrivileged(
                new GetPropertyAction("file.encoding"));
            Charset cs = lookup(csn);
            if (cs != null)
                defaultCharset = cs;
            else
                defaultCharset = forName("UTF-8");
        }
    }
    return defaultCharset;
}

从上面的实现可以看出,如果默认编码不存在,则使用 UTF-8 编码格式。

5、其他方法

ByteArrayOutputStream 提供的其他方法主要有以下这些:

  • public synchronized void reset():将字节数组输出流的 count 字段重新置为0,丢弃所有已累积的数据输出;
  • public synchronized byte toByteArray()[]:拷贝并创建一个新的字节数组,数组大小和内容与当前输出流一致;
  • public synchronized int size():获取字节数组的大小;
  • public void close() throws IOException:关闭字节数组输出流

下面分别看看这些方法的具体实现。

public synchronized void reset() {
    // reset 方法是直接将 count 置为0,count代表的是字节数组中有效的字节数
    count = 0;
}

public synchronized byte toByteArray()[] {
    // 使用 Arrays.copyof() 方法拷贝并创建一个新的字节数组
    // Arrays.copyof() 方法在上面已经分析过,具体是使用 System.arraycopy() 实现
    return Arrays.copyOf(buf, count);
}

public synchronized int size() {
    // 返回字节数组中有效字节的大小,即数组中保存了多少字节数据
    return count;
}

public void close() throws IOException {
    // 该方法没有任何实现,即调用 close() 方法,没有任何本质作用
}

需要注意的是 ByteArrayOutputStreamclose() 方法没有任何实现,即使调用该方法,也没有任何作用。