其他文章:解读Lucene.Net 阅读索引
InputStream这个类在Java基本类库里就有,但是Lucene选择了自己来实现,翻译到dotnet版本,名称保持没变。InputStream实现了ICloneable接口,就是能支持拷贝出新对象。
代码2-1
public virtual System.Object Clone()
{
InputStream clone = null;
try
{
clone = (InputStream) this.MemberwiseClone();
}
catch (System.Exception e)
{
throw new Exception("Can't clone InputStream.", e);
}
if (buffer != null)
{
clone.buffer = new byte[BUFFER_SIZE];
Array.Copy(buffer, 0, clone.buffer, 0, bufferLength);
}
clone.chars = null;
return clone;
}
从2-1代码可以看出,显示定义了一个InputStream 变量,然后用MemberwiseClone方法拷贝一个浅表副本,然后,判断如果buffer 内有数据,就把数据拷贝到浅副本里。而这个过程呢,实际上就是深拷贝。从中可以看出,浅拷贝就是克隆出新对象,但是不带数据,深拷贝呢就是浅副本带上数据。
InputStream 类有一个静态构造函数,给BUFFER_SIZE赋了一下值,这个值是默认buffer的大小。由OutputStream类提供,初始值是1024大小。下来在看看每个方法是干嘛的。
代码 2-2
private void Refill()
{
long start = bufferStart + bufferPosition;
long end = start + BUFFER_SIZE;
if (end > length)
// don't read past EOF
end = length;
bufferLength = (int) (end - start);
if (bufferLength == 0)
throw new System.IO.IOException("read past EOF");
if (buffer == null)
buffer = new byte[BUFFER_SIZE]; // allocate buffer lazily
ReadInternal(buffer, 0, bufferLength);
bufferStart = start;
bufferPosition = 0;
}
要理解Refill方法一定要看看另外三个变量。
private long bufferStart = 0; // position in file of buffer
private int bufferLength = 0; // end of valid bytes
private int bufferPosition = 0; // next byte to read
从注释上可以看到,bufferStart 代表文件缓冲区的位置,bufferLength 代表文件缓冲区的结束点,bufferPosition 表示当前读取到文件缓冲区的位置。而Refill方法也只在ReadByte方法中被使用了。
代码2-3
public byte ReadByte()
{
if (bufferPosition >= bufferLength)
Refill();
return buffer[bufferPosition++];
}
可以看出,是当读取的指针达到了文件缓冲区的最大长度,才调用了Refill方法。大体上就可以猜测,Refill是用来把下一段数据载入缓冲区的。Refill为什么要在第一个语句中计算
long start = bufferStart + bufferPosition;
这个start 变量到底代表什么呢?InputStream类有一个自身的缓冲区域,private byte[] buffer;在静态构造函数中设置了这个区域的大小。而用这个类去读取文件,文件的大小一般都是大于1024字节,所以,InputStream每次读取最多1024个字节的话,在InputStream读取文件的方式就是不连续读取的,是一段一段的载入的,这个start就代表了当前读取到了文件流的位置。long end = start + BUFFER_SIZE;语句则是计算出缓冲区域相对于文件流的位置。然后判断当前缓冲区相对于文件流位置如果大于length,那么就做一个调整。在InputStream的子类RAMInputStream中,可以看到length代表了文件的总长度。这个判断就是为了限制不会超出。
bufferLength = (int) (end - start);计算出了当前缓冲区域的长度。因为读入的字节如果不满1024字节的话,实际上在任何地方都没有对buffer进行清空,因此,只能用这种方式来处理在读取的字节数小于缓冲区大小的时候,防止读取超出文件实际长度。接着用到一个ReadInternal方法,这个方法在InputStream类中是抽象方法。等讲到子类的时候再来看。
看完了Refill方法,继续关注一下ReadByte,可以看到ReadByte方法相对于是做遍历操作的,只要调用ReadByte方法,就是获取下一个字节。
代码2-4
public void ReadBytes(byte[] b, int offset, int len)
{
if (len < BUFFER_SIZE)
{
for (int i = 0; i < len; i++)
// read byte-by-byte
b[i + offset] = (byte) ReadByte();
}
else
{
// read all-at-once
long start = GetFilePointer();
SeekInternal(start);
ReadInternal(b, offset, len);
bufferStart = start + len; // adjust stream variables
bufferPosition = 0;
bufferLength = 0; // trigger refill() on read
}
}
而ReadBytes方法则是一次读取指定数量的字节。如果读取的字节小于缓冲区域,则按字节读取,而如果超出了,则会先计算当前缓冲区相对文件缓冲区的位置。
代码 2-5
public long GetFilePointer()
{
return bufferStart + bufferPosition;
}
然后SeekInternal方法也是一个抽象方法,是把读取位置跳转到指定的位置。然后用抽象方法ReadInternal实际读取。读取出的b变量因为是引用类型,所以值直接就发生了变化。
bufferLength = 0; // trigger refill() on read
把bufferLength 设置为0,则ReadByte方法一定会触发refill方法,这个注释也是这么写的。
代码2-6
public int ReadInt()
{
return ((ReadByte() & 0xFF) << 24) | ((ReadByte() & 0xFF) << 16) | ((ReadByte() & 0xFF) << 8) | (ReadByte() & 0xFF);
}
ReadInt方法,用读出的每个字节和0xFF做与操作。这个用法前面也介绍到过,在.net中实际上是无必要的,这是Java和dotnet的差异造成的,翻译过来的时候译者可能并未深究。事实上在Java中也只需要 0x7F就够了,不需要0xFF。
假如ReadByte得出的值是 byte b = 244 那么 做了与操作之后还是244。左移24位,244的2进制是1111 1010,就是在后面加 24个0,位数就变成了32位。后面的操作和这个类似,做了或操作,相当于是把ReadByte读取到的4个byte值,排列得到一个数字。因为数字是32位,所以只要这个byte值是大于127的,都会产生负数。因为int类型的最高位是代表符号的,127的2进制是0111 1111,左移24位,最高位是0,而如果是128,就会变成
1111 1111
1000 0000,自然就变成了负数了。这个方法读取了4个字节。
代码2-7
public int ReadVInt()
{
byte b = ReadByte();
int i = b & 0x7F;
for (int shift = 7; (b & 0x80) != 0; shift += 7)
{
b = ReadByte();
i |= (b & 0x7F) << shift;
}
return i;
}
和ReadInt相比,运算的方式和ReadInt相似,但也有差别,每次位移是7位。
代码2-8
public long ReadLong()
{
return (((long) ReadInt()) << 32) | (ReadInt() & 0xFFFFFFFFL);
}
ReadLong方法读取了8个字节。对比看看ReadLong方法和ReadVInt方法有什么区别。ReadVInt方法每次位移的是7位的整数倍,那是因为
int i = b & 0x7F;操作得到的值不会大于127,而这个值得2进制是0111 1111,把最高位的0省略,那么就是7个1。这样第一次没有位移,而第二次位移7位,相当于在第一次读取字节二进制的高位加上了第二读取的字节。一直循环N次,加上第一次,就是N+1次。而ReadLong则是和ReadInt方法一样的,把后面得到的字节放在低位。ReadVInt方法相当于把ReadLong方法的高位和低位调换了,这在系统中比较常见,Socket就是这么做的,做过游戏外挂的肯定都知道这个的。
代码 2- 9
public long ReadVLong()
{
byte b = ReadByte();
long i = b & 0x7F;
for (int shift = 7; (b & 0x80) != 0; shift += 7)
{
b = ReadByte();
i |= (b & 0x7FL) << shift;
}
return i;
}
对比以上的理解就很容易理解ReadVLong方法了,和ReadVInt类似。ReadVLong,ReadVInt循环中的条件是(b & 0x80) != 0,0x80就是 1000 0000,所以一旦读取到b是1000 0000以上的数值,这个循环就终止了。也就是当b >= 128 。当然在java版本中和这个有差异,因为java的byte是 -128 - 127,所以,byte的最高位是符号,那么这个地方就是小于0的意思。
代码 2- 10
public System.String ReadString()
{
int length = ReadVInt();
if (chars == null || length > chars.Length)
chars = new char[length];
ReadChars(chars, 0, length);
return new System.String(chars, 0, length);
}
ReadString方法,在第一次运行时候,会设置为整个文件的长度,然后读取出了整个文件的字符。
代码 2-11
public void ReadChars(char[] buffer, int start, int length)
{
int end = start + length;
for (int i = start; i < end; i++)
{
byte b = ReadByte();
if ((b & 0x80) == 0)
buffer[i] = (char) (b & 0x7F);
else if ((b & 0xE0) != 0xE0)
{
buffer[i] = (char) (((b & 0x1F) << 6) | (ReadByte() & 0x3F));
}
else
buffer[i] = (char) (((b & 0x0F) << 12) | ((ReadByte() & 0x3F) << 6) | (ReadByte() & 0x3F));
}
}
ReadChars方法在ReadString方法中就是用来读取文件的全部字符的。
其他方法都比较简单,就不用细看了。至于这里为什么要这样来读取,那就要看OutputStream类了。呵呵。