Serialize data into one byte array using ByteBuffer

Posted on

Problem

I have a class in which I am passing certain parameters through the constructor and then using those parameters to make one final byte array with a proper format (header + data):

public final class Frame {
  private final byte addressedCenter;
  private final byte version;
  private final Map<byte[], byte[]> keyDataHolder;
  private final long location;
  private final long locationFrom;
  private final long locationOrigin;
  private final byte partition;
  private final byte copy;

  public Frame(byte addressedCenter, byte version,
      Map<byte[], byte[]> keyDataHolder, long location, long locationFrom,
      long locationOrigin, byte partition, byte copy) {
    this.addressedCenter = addressedCenter;
    this.version = version;
    this.keyDataHolder = keyDataHolder;
    this.location = location;
    this.locationFrom = locationFrom;
    this.locationOrigin = locationOrigin;
    this.partition = partition;
    this.copy = copy;
  }

  public byte[] serialize() {
    // All of the data is embedded in a binary array with fixed maximum size 70000
    ByteBuffer byteBuffer = ByteBuffer.allocate(70000);
    byteBuffer.order(ByteOrder.BIG_ENDIAN);

    int numOfRecords = keyDataHolder.size();
    int bufferUsed = getBufferUsed(keyDataHolder); // 36 + dataSize + 1 + 1 + keyLength + 8 + 2;

    // header layout
    byteBuffer.put(addressedCenter); // byte
    byteBuffer.put(version); // byte
    byteBuffer.putInt(numOfRecords); // int
    byteBuffer.putInt(bufferUsed); // int
    byteBuffer.putLong(location); // long
    byteBuffer.putLong(locationFrom); // long
    byteBuffer.putLong(locationOrigin); // long
    byteBuffer.put(partition); // byte
    byteBuffer.put(copy); // byte

    // now the data layout
    for (Map.Entry<byte[], byte[]> entry : keyDataHolder.entrySet()) {
      byte keyType = 0;
      byte keyLength = (byte) entry.getKey().length;
      byte[] key = entry.getKey();
      byte[] data = entry.getValue();
      short dataSize = (short) data.length;

      ByteBuffer dataBuffer = ByteBuffer.wrap(data);
      long timestamp = 0;

      if (dataSize > 10) {
        timestamp = dataBuffer.getLong(2);              
      }       

      byteBuffer.put(keyType);
      byteBuffer.put(keyLength);
      byteBuffer.put(key);
      byteBuffer.putLong(timestamp);
      byteBuffer.putShort(dataSize);
      byteBuffer.put(data);
    }
    return byteBuffer.array();
  }

  private int getBufferUsed(final Map<byte[], byte[]> keyDataHolder) {
    int size = 36;
    for (Map.Entry<byte[], byte[]> entry : keyDataHolder.entrySet()) {
      size += 1 + 1 + 8 + 2;
      size += entry.getKey().length;
      size += entry.getValue().length;
    }
    return size;
  }  
}

I would like to know if it can be improved in any way. As you can see right now, I am allocating ByteBuffer with predefined size of 70000. Is there a better way by which I can allocate the size I am using while making ByteBuffer instead of using a hardcoded 70000?

Solution

A minor aesthetics advice

You can save some typing by using the method chains over ByteBuffer:

private int getBufferCapacity(final Map<byte[], byte[]> keyDataHolder) {
    int size = 36;
    for (Map.Entry<byte[], byte[]> entry : keyDataHolder.entrySet()) {
        size += 1 + 1 + 8 + 2;
        size += entry.getKey().length;
        size += entry.getValue().length;
    }
    return size;
}

public byte[] serialize2() {
    ByteBuffer byteBuffer = ByteBuffer.allocate(getBufferCapacity(keyDataHolder))
            .order(ByteOrder.BIG_ENDIAN);
    // Use chaining:
    byteBuffer.put(addressedCenter)
            .put(version)
            .putInt(keyDataHolder.size())
            .putInt(getBufferUsed(keyDataHolder))
            .putLong(location)
            .putLong(locationFrom)
            .putLong(locationOrigin)
            .put(partition)
            .put(copy);

    for (Map.Entry<byte[], byte[]> entry : keyDataHolder.entrySet()) {
        byte keyType = 0;
        byte[] key = entry.getKey();
        byte[] value = entry.getValue(); // A map mapping is often called a
        // key/value -pair.
        byte keyLength = (byte) key.length;
        short valueLength = (short) value.length;

        ByteBuffer dataBuffer = ByteBuffer.wrap(value);
        // Short cut:
        long timestamp = valueLength > 10 ? dataBuffer.getLong(2) : 0;

        // Chaining:
        byteBuffer.put(keyType)
                .put(keyLength)
                .put(key)
                .putLong(timestamp)
                .putShort(valueLength)
                .put(value);
    }

    return byteBuffer.array();
}

Also, when you deal with maps, the conventional terminology for a mapping is a key/value-pair. For this reason, I would rewrite

byte[] key = entry.getKey();
byte[] data = entry.getValue();

to

byte[] key = entry.getKey();
byte[] value = entry.getValue();

Then, later on in the code, I would change

byte keyLength = (byte) entry.getKey().length;

with

byte keyLength = key.length;

Instead of

long timestamp = 0;

if (dataSize > 10) {
    timestamp = dataBuffer.getLong(2);              
}   

you can write an one-liner:

long timestamp = valueLength > 10 ? dataBuffer.getLong(2) : 0;

Finally, since you know your file format, you should be able to come up with a function that computes the exact size of the file for particular set of data; call it foo. Now, you would normally do:

ByteBuffer byteBuffer = ByteBuffer.allocate(foo(keyDataHolder));

Hope that helps.

Leave a Reply

Your email address will not be published. Required fields are marked *