这是《水煮 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[]
、 count
和 MAX_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
:将字节数组输出流中的全部数据写入到输出流参数中,调用的是OutputStream
的write()
方法
这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() 方法,没有任何本质作用
}
需要注意的是 ByteArrayOutputStream
的 close()
方法没有任何实现,即使调用该方法,也没有任何作用。