mark 和 reset
在说明问题之前先根据源码说明下 mark 和 reset 方法的用处:
mark
public synchronized void mark(int readlimit) {
marklimit = readlimit;
markpos = pos;
}
@param readlimit 在标记失效前允许读取的最大字节数,readlimit 的含义也就是在 mark 位置之后,可以读取 readlimit 长度的字节数,之后标记点将不再生效,配合 reset 方法查看更清晰。
reset
public synchronized void reset() throws IOException {
getBufIfOpen();
if (markpos < 0)
throw new IOException("Resetting to invalid mark");
pos = markpos;
}
当 marpos(在 mark 方法中赋值,即最近的标记位置) 是空时(未设置标记或者标记已经失效),直接抛出异常。否则,将该流读取的起始位置设置为之前的标记位置。
总结
其实综合两个方法来看,mark 在输入流中标记当前位置,方便 reset 之后能够从标记点位置开始重新获取到相同的信息。
问题描述
在使用 BufferdInputStream 用到 mark( int readlimit) 和 reset() 方法, 但是 mark 中的参数无论设置多少,无论在设置标记之后读取多少字节,标记位置都不会失效,调用 reset 之后都将从最近的一次标记位置读取。即:我设置了 readlimit 为 0,然后读取了1024 个字节,markpos 仍然有效, reset 之后仍然可以实现从标记点重新读取信息。
实例及源码解析
问题复现
待码
String textTxt = "ABCDEFG";
InputStream iStream = new ByteArrayInputStream(textTxt.getBytes(StandardCharsets.UTF_8));
BufferedInputStream bis = new BufferedInputStream(iStream);
bis.mark(6);
int rd = bis.read();
while(rd!=-1){
System.out.println("First Time: "+ (char)rd);
rd = bis.read();
}
bis.reset();
int rd2 = bis.read();
System.out.println("Second Time:"+(char)rd2);
打印结果
First Time: A First Time: B First Time: C First Time: D First Time: E First Time: F First Time: G Second Time:A
源码分析
从最后打印出来的结果可以看出,reset 之后,输入流从位置 0 重新读取信息,表示 mark 标记仍然有效。 但是在上面 mark 和 reset 的源码中除了对标记属性和当前位置的赋值外,没有看到任何其他对标记位置处理的逻辑。 所以我们要寻找其他的”可疑“代码,我们在代码中只用到了 mark 、reset 和 read,所以”嫌疑“理所当然的落在了 read 头上,查看 read 的代码
read()源码
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
明确问题点
我们查看代码中的 fill 方法
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos < 0)
pos = 0;
else if (pos >= buffer.length)
if (markpos > 0) {
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
markpos = -1;
pos = 0;
} else if (buffer.length >= MAX_BUFFER_SIZE) {
throw new OutOfMemoryError("Required array size too large");
} else {
int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
pos * 2 : MAX_BUFFER_SIZE;
if (nsz > marklimit)
nsz = marklimit;
byte nbuf[] = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
throw new IOException("Stream closed");
}
buffer = nbuf;
}
count = pos;
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}
在 fill 方法中处理到了 markpos,那应该就是它没跑了,接下来就看一下它是怎么处理的。
普通的输入流读取方式
我们先看一下涉及 markpos 的逻辑片段,其实就是输入流读取字节的逻辑
byte[] buffer = getBufIfOpen();
if (markpos < 0)
pos = 0;
count = pos;
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
后续在 read 逻辑中,如果当前位置没有超过 buffer 中字节总数的大小,将直接从 buffer 中读取字节
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
涉及 markpos 的处理逻辑
else if (pos >= buffer.length)
if (markpos > 0) {
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
markpos = -1;
pos = 0;
} else if (buffer.length >= MAX_BUFFER_SIZE) {
throw new OutOfMemoryError("Required array size too large");
} else {
int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
pos * 2 : MAX_BUFFER_SIZE;
if (nsz > marklimit)
nsz = marklimit;
byte nbuf[] = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
throw new IOException("Stream closed");
}
buffer = nbuf;
}
问题处理
根据上面的源码分析可以看到,对于标记失效的关键逻辑在于:
else if (pos >= buffer.length)
...
else if (buffer.length >= marklimit) {
markpos = -1;
pos = 0;
}...
标记失效实现的条件是:
- 当前输入流读取的位置大于等于 buffer 的长度
- buffer 的长度大于等于 marklimit
我们在初始化时并没有给 BufferdInputStream 设置 buffer 的大小,所以将使用默认大小
private static int DEFAULT_BUFFER_SIZE = 8192;
所以我们此时之只能在输入流读取了 8192 个字节之后,才会进入到失效逻辑中,或者我们设置 reallimit 大于8192 并读取超过 reallimit 个字节,标记也会失效。 然而,另一方面,我们可以修改 buffer 长度,修改代码
String textTxt = "ABCDEFG";
InputStream iStream = new ByteArrayInputStream(textTxt.getBytes(StandardCharsets.UTF_8));
BufferedInputStream bis = new BufferedInputStream(iStream,5);
bis.mark(6);
int j =0;
int rd = bis.read();
while(rd!=-1 && j<6){
System.out.println("First Time: "+ (char)rd);
rd = bis.read();
j++;
}
bis.reset();
int rd2 = bis.read();
System.out.println("Second Time:"+(char)rd2);
此时在 reset 方法执行时将会抛出异常,标记失效:
First Time: A
First Time: B
First Time: C
First Time: D
First Time: E
First Time: F
First Time: G
Exception in thread "main" java.io.IOException: Resetting to invalid mark
at java.io.BufferedInputStream.reset(BufferedInputStream.java:448)
at com.dm.java.demo.Test.main(Test.java:35)
|