在处理字节级别的数据流的时候,常常需要一个“流”的数据结构,而且需要这么6个基本功能:
方法原型 | 功能描述 |
---|---|
void put(byte aByte) | 向流末尾塞入一个字节 |
byte unput() | 从流末尾取出一个字节(撤销一次put动作) |
byte take() | 从流的开头取出一个字节 |
void untake(byte aByte) | 向流的开头塞入一个字节(撤销一次take动作) |
byte peek(int index) | 查看从流开头开始第index个字节的值(但不取出) |
int getLength() | 查看流的总长度 |
有两种直观的方法可以实现这么一个字节流:
(1)直接使用链表LinkedList<Byte>实现;
(2)使用数组构建一个循环队列;
这两个方法有各自的优缺点。
首先说使用链表LinkedList<Byte>实现的方式,其基本特征是:
(1)put()、untake()的效率较高(但不是很高,因为会频繁地有节点的动态分配);
(2)unput()、take()和getLength()的的效率很高;
(3)peek()的效率很低,因为要顺着链表遍历;
再说说基于数组的循环队列,其基本特征是:
(1)put()、unput()、take()、untake()、getLength()和peek()的效率很高;
(2)但是一旦数组长度不够,扩容的时候效率很低。
我想到了一种把链表和数组结合起来的方法。它的基本数据结构是LinkedList<byte[]>,也就是说,高层是一个链表,而链表的每一个元素则是一个长度确定的数组,我称之为数据块。此外,还有一个读指针、写指针、当前长度和回收链表。太抽象了,直接看代码:
package zjs.util; import java.util.LinkedList; /** * 字节流 */ public class ByteStream { //每一个数据块的大小 private static final int BUFSIZE=1024; //空闲数据块 private LinkedList<byte[]> freeBuffers; //正在使用的数据块的链表 private LinkedList<byte[]> usedBuffers; //下一个读取位置(相对于第一个数据块开头的偏移) private int readPos; //下一个写入位置(相对于最后一个数据块开头的偏移) private int writePos; //流的长度 private int length; /** * 构造一个字节流 */ public ByteStream() { freeBuffers=new LinkedList<byte[]>(); usedBuffers=new LinkedList<byte[]>(); usedBuffers.add(allocateBuffer()); readPos=0; writePos=0; length=0; } /** * 获取字节流当前长度 * @return 包含的长度 */ public int getLength() { return length; } /** * 从流的开头取出一个字节 * @return 开头的字节,如果当前长度为0,则返回0 */ public byte take() { if(length==0) return 0; //获取第一个数据块 byte[] buffer=usedBuffers.getFirst(); //读取流头部的字节 byte aByte=buffer[readPos]; //下一个读取位置后移 readPos++; //如果超出了第一个数据块 if(readPos>=BUFSIZE) { //把第一个数据块移除 usedBuffers.removeFirst(); //把第一个数据块放入空闲队列中,回收以备用 freeBuffers.add(buffer); //下一读取位置为新的数据块的开头 readPos=0; } length--; return aByte; } /** * 把一个字节塞回流的开头 * @param aByte 一个字节 */ public void untake(byte aByte) { //先回退读取位置 readPos--; //如果掉出了开头 if(readPos<0) { //分配一块空闲数据块,加到链表开头 usedBuffers.addFirst(allocateBuffer()); //读取位置就是新的数据块的末尾 readPos=BUFSIZE-1; } //获取当前第一个数据块 byte[] buffer=usedBuffers.getFirst(); //写入数据 buffer[readPos]=aByte; length++; } /** * 向流的末尾放入一个字节 * @param aByte 字节 */ public void put(byte aByte) { //获取链表最后一个数据块 byte[] buffer=usedBuffers.getLast(); //写入字节 buffer[writePos]=aByte; //写入位置后移 writePos++; //如果写入位置超出数据块长度 if(writePos>=BUFSIZE) { //分配一个新的数据块,加到链表末尾 usedBuffers.addLast(allocateBuffer()); //下一个写入位置指向新数据块的开头 writePos=0; } length++; } /** * 从字节流末尾取出一个字节 * @return 流尾的字节 */ public byte unput() { if(length==0) return 0; //回退写入位置 writePos--; //如果掉出了数据块开头 if(writePos<0) { //从链表末尾删除数据块,并回收以备用 freeBuffers.add(usedBuffers.removeLast()); //写入位置指向新的数据块的末尾 writePos=BUFSIZE-1; } //取出当前最后一个数据块 byte[] buffer=usedBuffers.getLast(); //读取数据 byte aByte=buffer[writePos]; length--; return aByte; } /** * 查看流中某个字节的值 * @param index 编号 * @return 值 */ public byte peek(int index) { if(index<0||index>=length) return 0; //如果把整个链表中的数据块连在一起看作一个长长的数组 //那么该位置所在的索引 int logicPos=readPos+index; //计算处于第几个数据块 int bufferIndex=logicPos/BUFSIZE; //计算相对那块数据块开头的偏移 int indexInBuffer=logicPos%BUFSIZE; //取出数据块 byte[] buffer; //适当的优化,如果是第0块,那么直接使用getFirst(),如果是最后一块,直接使用getLast() if(bufferIndex==0) buffer=usedBuffers.getFirst(); else if(bufferIndex==usedBuffers.size()-1) buffer=usedBuffers.getLast(); else buffer=usedBuffers.get(bufferIndex); //读取数据 byte aByte=buffer[indexInBuffer]; return aByte; } /** * 分配一个新的数据块 * @return 空闲数据块 */ private byte[] allocateBuffer() { if(freeBuffers.size()>0) return freeBuffers.remove(); return new byte[BUFSIZE]; } }
写了这么一个Demo进行性能对比:
package zjs.wirelessSample; import java.util.LinkedList; import zjs.util.ByteStream; public class Test6 { public static void main(String[] args) { ByteStream s=new ByteStream(); long startTime=System.currentTimeMillis(); for(int j=0;j<10000;j++) { for(int i=0;i<100000;i++) s.put((byte)0); for(int i=0;i<100000;i++) s.take(); } long endTime=System.currentTimeMillis(); System.out.println(endTime-startTime); LinkedList<Byte> l=new LinkedList<Byte>(); startTime=System.currentTimeMillis(); for(int j=0;j<10000;j++) { for(int i=0;i<100000;i++) l.add((byte)0); for(int i=0;i<100000;i++) l.remove(); } endTime=System.currentTimeMillis(); System.out.println(endTime-startTime); } }
我的ByteStream用了6507毫秒,LinkedList用了15408毫秒,两倍多的性能~