1.概述

ByteArrayInputStream即字节数组输入流,ByteArrayOutputStream即字节数组输出流,这俩对象在创建时,需要提供一个byte[]作为输入来源、输出目的地,所有的方法都是围绕这个byte[]进行操作。byte[]保存在内存的一块空间,也就是说所有操作完全不涉及磁盘文件,一般用于流数据的中转。

2.ByteArrayInputStream

2.1 构造器

ByteArrayInputStream提供了俩个构造器,核心是要传入一个字节数组,其他的下面成员变量会详解:

1
2
3
4
5
6
7
8
9
10
11
12
public ByteArrayInputStream(byte buf[]) {
this.buf = buf;
this.pos = 0;
this.count = buf.length;
}

public ByteArrayInputStream(byte buf[], int offset, int length) {
this.buf = buf;
this.pos = offset;
this.count = Math.min(offset + length, buf.length);
this.mark = offset;
}

2.2 成员变量

1
2
3
4
5
6
7
8
9
10
11
// 核心属性,字节数组
protected byte buf[];

// 当前要读取的数组坐标,可以在构造器指定,默认为0
protected int pos;

// 标记的位置,可以在构造器指定,默认为0
protected int mark = 0;

// 流中字节的数目,也可以理解为下次读取的起始坐标
protected int count;

2.3 内部方法

读取单个字节数据:
1
2
3
4
public synchronized int read() {
// 位与0xFF的作用就是将buf[pos]值的二进制变成32位,并且前位24补零,然后转化为int
return (pos < count) ? (buf[pos++] & 0xff) : -1;
}

批量读取字节数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public synchronized int read(byte b[], int off, int len) {

// 参数校验
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
}

// 如果将要读取的数组坐标,已经超出最大长度,代表读取到流末尾,返回-1
if (pos >= count) {
return -1;
}

// 计算出可读取的长度
int avail = count - pos;

// 如果想要读取的长度大于可读取长度,那么根据可读取的长度,进行读取
if (len > avail) {
len = avail;
}

// 再次校验是否已经达到流末尾
if (len <= 0) {
return 0;
}

// 拷贝数据,从buf的pos坐标开始,拷贝len个元素到b,b从off坐标开始接收拷贝过来的数据
System.arraycopy(buf, pos, b, off, len);

// 下次读取的起始坐标递增
pos += len;

// 返回读取的字节数量
return len;
}

将下次要读取的坐标往后推移n位,但是不能超过最大坐标值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public synchronized long skip(long n) {

// 计算出可以后推的坐标数量
long k = count - pos;

// 如果想要后推的数量小于可以后推的数量
if (n < k) {
k = n < 0 ? 0 : n;
}

// 进行后推并返回后推的值
pos += k;
return k;
}

其他方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 如果无效返回0,其他值代表有效
public synchronized int available() {
return count - pos;
}

// 是否支持标记
public boolean markSupported() {
return true;
}

// 逻辑固定,将下次要读取的坐标进行标记
public void mark(int readAheadLimit) {
mark = pos;
}

// 将下次读取的坐标设置为标记值
public synchronized void reset() {
pos = mark;
}

// 关闭流,由于不涉及操作系统文件描述符的释放,这里设计为模版模式,如果有特殊要求可以继承并重写此类
public void close() throws IOException {

}

3.ByteArrayOutputStream

ByteArrayInputStream类关注读取坐标(pos)的管理,而ByteArrayOutputStream类关注字节数组容量管理,因为某个实例可能会被无限写入,构造器初始化的容量很大几率不够使用,因此需要动态扩容。

ByteArrayInputStream类还有一个特点,可以获取写入结果,或者将写入结果写入其他输出流,这是其他输出流没有的。

3.1 构造器

ByteArrayOutputStream也提供了俩个构造器,核心时创建一个字节数据,用于后续的写入:

1
2
3
4
5
6
7
8
9
10
public ByteArrayOutputStream() {
this(32);
}

public ByteArrayOutputStream(int size) {
if (size < 0) {
throw new IllegalArgumentException("Negative initial size: " + size);
}
buf = new byte[size];
}

3.2 成员变量

1
2
3
4
5
6
7
8
// 核心属性,字节数组
protected byte buf[];

// 字节数组的长度
protected int count;

// 字节数据最大的容量,有些虚拟机会数组中保留一些头字,所以要在最大int值上减8
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

3.3 内部方法

传入一个假定容量,如果大于当前字节数组长度,进行扩容:
1
2
3
4
private void ensureCapacity(int minCapacity) {
if (minCapacity - buf.length > 0)
grow(minCapacity);
}

字节数组扩容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void grow(int minCapacity) {

// 记录原始容量
int oldCapacity = buf.length;

// 将原始容量左移一位,也就是乘以2,得到新容量
int newCapacity = oldCapacity << 1;

// 如果计算出来的新容量大于最小容量,取最小容量(保证数组正好装满,不会有空余)
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;

// 如果最重要扩容的容量大于最大允许容量,调用hugeCapacity方法再处理
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);

// 执行扩容
buf = Arrays.copyOf(buf, newCapacity);
}

字节数组容量最大值处理:
1
2
3
4
5
6
7
8
private static int hugeCapacity(int minCapacity) {
// 参数校验
if (minCapacity < 0)
throw new OutOfMemoryError();

// 大于MAX_ARRAY_SIZE就取int最大值,否则用MAX_ARRAY_SIZE
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

单字节写入:
1
2
3
4
5
6
7
8
9
10
11
public synchronized void write(int b) {

// 写入前先将写入成功后的字节数组长度值,拿去检查是否需要扩容
ensureCapacity(count + 1);

// 将int值转化为byte并赋值到字节数组的坐标中
buf[count] = (byte) b;

// 容量+1,也代表下次写入的坐标
count += 1;
}

多字节写入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

其他方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 将写入结果写入另一个输出流
public synchronized void writeTo(OutputStream out) throws IOException {
out.write(buf, 0, count);
}

// 重置,但只是将容量标记归零,数组中的字节元素仍然存在
public synchronized void reset() {
count = 0;
}

// 将字节数组拷贝一份并返回
public synchronized byte toByteArray()[] {
return Arrays.copyOf(buf, count);
}

// 返回字节数组长度(容量)
public synchronized int size() {
return count;
}

// 将写入结果转化为字符串返回
public synchronized String toString() {
return new String(buf, 0, count);
}

// 将写入的结果转化为字符串返回
public synchronized String toString(String charsetName) throws UnsupportedEncodingException {
return new String(buf, 0, count, charsetName);
}

// 将写入的结果转化为字符串返回,已废弃
@Deprecated
public synchronized String toString(int hibyte) {
return new String(buf, hibyte, 0, count);
}

// 关闭流,仍然为空方法
public void close() throws IOException {
}

4.使用场景

4.1 MultipartFile

有些文件的来源不一定是磁盘,还有可能来自网络,比如SpringMVC框架接收上传文件的MultipartFile类,服务端接收到文件后是没有路径这个概念的,对外提供的getInputStream()方法返回的是个ByteArrayInputStream,这种设计使文件不知道来源的情况下,也能融入到IO流体系中。

4.2 多次使用

InputStream

4.3 避免创建临时文件

之前有个需求,简单说就是某个远程服务器有一些excel文件,但是表头都是数据库字段名,前端无法通过URL直接导出(都是英文,用户也看不懂),因此需要后端做一层处理,将这些excel表头修改为中文描述再呈现给用户。正常情况下,读取到远程excel的InputStream后,创建一个Workbook对象并进行修改,并将结果写入HttpServletResponse即可。

但是公司因为一些原因不能通过流的形式将文件下载到浏览器,必须将文件放入云服务器然后返回路径,前端只能通过路径去下载。上传到云服务器必须提供文件的InputStream,但是Workbook只能将结果写入OutputStream。一般情况下你可能会想到先写到本地,然后创建本地的InputStream再上传到云服务器,最后删除本地文件。

现在我们可以利用ByteArrayInputStream、ByteArrayOutputStream来避免这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static void main(String[] args) throws Exception{

// 远程文件路径
String excelPath = "excel path";

// 通过java.net.URL类,远程获取excel的InputStream
InputStream inputStream = getInputStreamByUrl(excelPath);

// 创建Workbook
XSSFWorkbook xwb = new XSSFWorkbook(inputStream);

// sheet固定为第一个,表头固定为第一行
XSSFSheet sheetAt = xwb.getSheetAt(0);
XSSFRow headRow = sheetAt.getRow(0);

// 修改表头内容
XSSFCell cell1 = headRow.getCell(0);
cell1.setCellValue("A");
XSSFCell cell2 = headRow.getCell(1);
cell2.setCellValue("B");

// 将excel修改结果,写入临时输出流
ByteArrayOutputStream tempOutputStream = new ByteArrayOutputStream();
xwb.write(tempOutputStream);

// 将临时输出流 转化为临时输入流
InputStream tempInputStream = new ByteArrayInputStream(tempOutputStream.toByteArray());

// 上传到云服务器
upload(tempInputStream);
}

评论