Eugene的回答解释了为什么在大量数组中观察到内存消耗增加的原因。那么标题中的问题,"如何高效地存储Java中的小字节数组?" 的答案可能是:完全不需要。
1
然而,可能有方法可以实现您的目标。像往常一样,这里的“最佳”解决方案取决于这些数据将如何被使用。一个非常实用的方法是:为您的数据结构定义一个接口。
在最简单的情况下,此接口可能只需是:
interface ByteArray2D
{
int getNumRows();
int getNumColumns();
byte get(int r, int c);
void set(int r, int c, byte b);
}
提供一个“2D字节数组”的基本抽象。根据应用情况,可能有必要在此处提供其他方法。这里可以使用的模式通常与处理“2D矩阵”(通常是float
值)的矩阵库相关,并且它们经常提供以下方法:
interface Matrix {
Vector getRow(int row);
Vector getColumn(int column);
...
}
然而,当主要目的是处理一组byte[]
数组时,访问每个数组(即2D数组的每一行)的方法可能已经足够:
ByteBuffer getRow(int row);
有了这个接口,创建不同的实现就非常简单。例如,您可以创建一个简单的实现,只是在内部存储一个 2D 的 byte[][]
数组:
class SimpleByteArray2D implements ByteArray2D
{
private final byte array[][];
...
}
或者,您可以创建一个存储1Dbyte[]
数组或类似的ByteBuffer
的实现来内部存储:
class CompactByteArray2D implements ByteArray2D
{
private final ByteBuffer buffer;
...
}
这个实现只需要在调用访问二维数组的某一行/列的方法时计算(1D)索引即可。
下面是一个
MCVE,展示了这个接口和两个实现,介绍了接口的基本用法,并使用JOL进行内存占用分析。
该程序的输出为:
For 10 rows and 1000 columns:
Total size for SimpleByteArray2D : 10240
Total size for CompactByteArray2D: 10088
For 100 rows and 100 columns:
Total size for SimpleByteArray2D : 12440
Total size for CompactByteArray2D: 10088
For 1000 rows and 10 columns:
Total size for SimpleByteArray2D : 36040
Total size for CompactByteArray2D: 10088
展示:
整个程序:
package stackoverflow;
import java.nio.ByteBuffer;
import org.openjdk.jol.info.GraphLayout;
public class EfficientByteArrayStorage
{
public static void main(String[] args)
{
showExampleUsage();
anaylyzeMemoryFootprint();
}
private static void anaylyzeMemoryFootprint()
{
testMemoryFootprint(10, 1000);
testMemoryFootprint(100, 100);
testMemoryFootprint(1000, 10);
}
private static void testMemoryFootprint(int rows, int cols)
{
System.out.println("For " + rows + " rows and " + cols + " columns:");
ByteArray2D b0 = new SimpleByteArray2D(rows, cols);
GraphLayout g0 = GraphLayout.parseInstance(b0);
System.out.println("Total size for SimpleByteArray2D : " + g0.totalSize());
ByteArray2D b1 = new CompactByteArray2D(rows, cols);
GraphLayout g1 = GraphLayout.parseInstance(b1);
System.out.println("Total size for CompactByteArray2D: " + g1.totalSize());
}
private static void showExampleUsage()
{
System.out.println("Using a SimpleByteArray2D");
ByteArray2D b0 = new SimpleByteArray2D(10, 10);
exampleUsage(b0);
System.out.println("Using a CompactByteArray2D");
ByteArray2D b1 = new CompactByteArray2D(10, 10);
exampleUsage(b1);
}
private static void exampleUsage(ByteArray2D byteArray2D)
{
System.out.println(byteArray2D.get(2, 4));
byteArray2D.set(2, 4, (byte)123);
System.out.println(byteArray2D.get(2, 4));
ByteBuffer row = byteArray2D.getRow(2);
for (int c = 0; c < row.capacity(); c++)
{
System.out.println(row.get(c));
}
}
}
interface ByteArray2D
{
int getNumRows();
int getNumColumns();
byte get(int r, int c);
void set(int r, int c, byte b);
ByteBuffer getRow(int row);
}
class SimpleByteArray2D implements ByteArray2D
{
private final int rows;
private final int cols;
private final byte array[][];
public SimpleByteArray2D(int rows, int cols)
{
this.rows = rows;
this.cols = cols;
this.array = new byte[rows][cols];
}
@Override
public int getNumRows()
{
return rows;
}
@Override
public int getNumColumns()
{
return cols;
}
@Override
public byte get(int r, int c)
{
return array[r][c];
}
@Override
public void set(int r, int c, byte b)
{
array[r][c] = b;
}
@Override
public ByteBuffer getRow(int row)
{
return ByteBuffer.wrap(array[row]);
}
}
class CompactByteArray2D implements ByteArray2D
{
private final int rows;
private final int cols;
private final ByteBuffer buffer;
public CompactByteArray2D(int rows, int cols)
{
this.rows = rows;
this.cols = cols;
this.buffer = ByteBuffer.allocate(rows * cols);
}
@Override
public int getNumRows()
{
return rows;
}
@Override
public int getNumColumns()
{
return cols;
}
@Override
public byte get(int r, int c)
{
return buffer.get(r * cols + c);
}
@Override
public void set(int r, int c, byte b)
{
buffer.put(r * cols + c, b);
}
@Override
public ByteBuffer getRow(int row)
{
ByteBuffer r = buffer.slice();
r.position(row * cols);
r.limit(row * cols + cols);
return r.slice();
}
}
再次强调,这主要是一个草图,展示一种可能的方法。界面的细节将取决于预期的应用程序模式。
1 一个附注:
内存开销的问题在其他语言中也很相似。例如,在C/C++中,最接近“2D Java数组”的结构将是手动分配指针的数组:
char** array;
array = new (char*)[numRows];
array[0] = new char[numCols];
...
在这种情况下,您还有一个与行数成比例的开销——通常为每行一个(通常为4个字节)指针。
length
的public final
字段,它包含数组的元素数量,length
可以是正数或零。每个数组最小的开销为4字节(加上从java.lang.Object
继承的任何开销),这对于10字节的数组来说是最小的40%的惩罚(每个数组)。你究竟想要实现什么目标? - Elliott Frischinterface Data { byte get(int x, int y); void set(int x, int y, byte b)}
。然后,您可以将所有内容存储在单个数组中。如果更方便,您还可以以ByteBuffer
的形式返回此大型数组的“切片”(使用ByteBuffer#slice
方法)。 - Marco13