Java中长度未知的字节数组:第二部分

6
与Java中的"未知长度的字节数组"类似,我需要能够将来自数据源的未知数量的字节写入byte[]数组中。 但是,为了进行压缩算法,我需要读取之前存储的字节,因此ByteArrayOutputStream对我不起作用。
现在,我有一个方案,其中我分配固定大小N的ByteBuffer,在达到N、2N、3N字节等时添加一个新的ByteBuffer。在数据用尽后,我将所有缓冲区转储到现在已知大小的数组中。
有更好的方法吗?使用固定大小的缓冲区会降低压缩算法的灵活性。
7个回答

5


4
为什么不对ByteArrayOutputStream进行子类化?这样您的子类就可以访问受保护的bufcount字段,并且您可以添加方法来直接操作它们。

2

如Chris所回答的那样,使用CircularByteBuffer API是正确的方法。幸运的是,它现在已经在中央maven仓库中了。引用这个链接中的一段代码,如下所示:

循环缓冲区的单线程示例

// buffer all data in a circular buffer of infinite size
CircularByteBuffer cbb = new CircularByteBuffer(CircularByteBuffer.INFINITE_SIZE);
class1.putDataOnOutputStream(cbb.getOutputStream());
class2.processDataFromInputStream(cbb.getInputStream());

优点包括:

  • 只需要一个CircularBuffer类,而不是两个pipe类。
  • 在“缓冲所有数据”和“额外线程”方法之间进行转换更加容易。
  • 您可以更改缓冲区大小,而不是依赖于管道中硬编码的1k缓冲区。

最后,我们摆脱了内存问题和管道API。


2
ByteArrayOutputStream的开销在于调整底层数组的大小。您的固定块处理程序消除了其中大部分。如果调整大小不太昂贵(即在您的测试中ByteArrayOutputStream“足够快”且不会产生反向内存压力),则可以尝试像vanza建议的那样子类化ByteArrayOutputStream。

我不知道您的压缩算法,所以无法说出为什么块列表会使其不够灵活,甚至无法说出压缩算法为什么会知道这些块。但由于块可以是动态的,因此您可能能够根据需要调整块大小,以更好地支持您正在使用的压缩算法的各种变化。

如果压缩算法可以处理“流”(即固定大小的数据块),则块大小应该很重要,因为您可以将所有这些细节隐藏在实现中。完美的情况是,如果压缩算法希望其数据与您分配的块大小匹配,那么您就不必复制数据来提供给压缩器。


2
虽然您可以使用ArrayList来实现此功能,但是您将面临4-8倍的内存开销 - 假设字节不是新分配的,而是共享一个全局实例(因为这对于整数是正确的,我认为对于Bytes也是有效的) - 并且您会失去所有缓存局部性。
因此,尽管您可以子类化ByteArrayOutputStream,但即使在那里,您也会得到不必要的开销(方法是同步的)。所以我个人会自己编写一个类,在写入时动态增长。虽然比您当前的方法效率低,但简单易懂,我们都知道摊销成本的部分 - 否则,您当然也可以使用您的解决方案。只要您将解决方案包装在干净的接口中,就可以隐藏复杂性并获得良好的性能。
或者换句话说:否,您几乎无法比您已经执行的操作更有效,并且每个内置的Java集合都应该由于某种原因表现更差。

我不确定我理解ByteArrayOutputStream中的“方法是同步的”的含义。你能详细说明一下吗? - trashgod
1
无论出于什么原因,类中的方法都被定义为同步的 - 尽管我认为您可以用普通方法覆盖同步方法?不确定。如果不能,那么同步开销对于您的情况来说是相当无用的。 - Voo
啊,我现在明白了;谢谢。 - trashgod

0

为了简单起见,您可以考虑使用java.util.ArrayList

ArrayList<Byte> a = new ArrayList<Byte>();
a.add(value1);
a.add(value2);
...
byte value = a.get(0);

Java 1.5及更高版本将在byteByte类型之间提供自动装箱和拆箱。性能可能会比ByteArrayOutputStream略逊,但易于阅读和理解。

2
它需要4-8倍的内存,而且你会失去缓存局部性。更糟糕的是,你消除了JIT向量化代码的任何机会 - 或者至少只能一次处理更大的值(即你不能处理字大小的值并进行一些位操作)。"稍微"更糟可能有点乐观。 - Voo
确实如此,感谢提供信息。如果性能是一个问题,这将是最糟糕的解决方案之一。 - Calvin
这也是我的第一个想法,但对于相当小的文件会导致“OutOfMemoryError”。我不建议使用这种解决方案。 - Michael Kern

0
我最终编写了自己的方法,它使用一个临时的固定缓冲区数组,并在填满固定缓冲区后每次将其附加到最终字节数组中。它将继续覆盖固定缓冲区数组并附加到最终数组,直到所有字节都被读取和复制。最后,如果temporaryArray没有填满,它将把从该数组中读取的字节复制到最终数组中。我的代码是为Android编写的,因此您可能需要使用类似于ArrayUtils.concatByteArrays (com.google.gms.common.util.ArrayUtils)的方法。
我的代码将临时数组大小设置为100 (growBufferSize),但最好将其设置为500或甚至1000,具体取决于您使用的环境的性能。最终结果将存储在bytesfinal数组中。
这种方法应该减少内存使用,以防止OutOfMemoryError。由于它主要使用基元,因此应该减少内存使用。
final int growBufferSize = 100;
byte[] fixedBuffer = new byte[growBufferSize];
byte[] bytesfinal = new byte[0];

int fixedBufferIndex=0;
while (zin.available()>0){
    fixedBuffer[fixedBufferIndex] = (byte)zin.read();
    if (fixedBufferIndex == growBufferSize-1){
        bytesfinal = ArrayUtils.concatByteArrays(bytesfinal,fixedBuffer);
        fixedBufferIndex = -1;
    }

    fixedBufferIndex++;
}

if (fixedBufferIndex!=0){
    byte[] lastBytes = new byte[fixedBufferIndex];
    //copy last read bytes to new array
    for (int i = 0; i<fixedBufferIndex; i++){
        lastBytes[i]=fixedBuffer[i];
    }

    //add last bits of data
    bytesfinal = ArrayUtils.concatByteArrays(bytesfinal,lastBytes);
    lastBytes = null;
    fixedBuffer = null;
}

public class ArrayUtils {

    static byte[] concatByteArrays(byte[] first, byte[] second) {
        byte[] result = Arrays.copyOf(first, first.length + second.length);
        System.arraycopy(second, 0, result, first.length, second.length);
        return result;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接