添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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

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

::: hljs-center

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;
                    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() 方法没有任何实现,即使调用该方法,也没有任何作用。

    Java io流怎么读取文件内容 java文件流读取文件

    流是一个很重要的概念。这里的话不阐述,就简单说说文件的读。这里的话我们尽可能的和python的文件的读进行比较,写出一个类似的功能。 在Java中对于文件的操作有对应的两种流,一种是字节流,另一种是字符流。前者适合读取二进制文件比如图片,后者适合读取文本文件。