Samstag, 4. Juli 2009

Stream toByteArray()

InputStreams sind eine tolle Sache. Kann man mit Ihnen standardisiert Daten auslesen - ganz gleich woher, der Code zum Einlesen braucht auch dann nicht verändert zu werden, wenn die Daten von einer anderen Quelle kommen.


Kann man die Datehn aus dem Stream jedoch nicht linear verarbeiten, bietet es sich an, die Daten erst einmal in den Speicher zu kippen. Üblicher Weise liest man dafür zunächst die Bytes ein, um anschließend irgend eine verarbeitende Klasse damit zu füttern. Alternativ kann man sich auch mittels mark und reset auf dem Stream bewegen, Das ist aber nur dann wirklich sinnvoll, wenn die erwartete Datenmenge ein vernünftiges Maß überschreitet. Neben der Unhandlichkeit ist es eben auch nicht besonders performant auf diesem Wege zu arbeiten.


Um zum Punkt zu kommen, gibt es eine einfache und gängige Lösung, Daten aus einem InputStream in ein Bytearray zu lesen. Das Beispiel kopiert die Daten aus einem InputStream in einen ByteArrayOutputStream, dem man anschließend das Bytearray entnehmen kann.


  ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}

Diese Systematik sieht man sehr häufig. Im Grunde ist das eine einfache Kopierfunktion. Schlecht daran ist, dass die Daten zunächst in einen Puffer gelesen werden, dessen Inhalt anschließend weiterkopiert wird. Besser und schneller währe es, würde man die Daten direkt in das Ziel lesen. Leider ist die Gesamtmenge der Daten vor dem Auslesen nicht sicher zu ermitteln, weshalb die Größe des Zielbytearrays unbekannt ist. Für diesen Zweck habe ich die Klasse ByteArrayOutputStream erweitert und eine append Methode hinzugefügt, die Daten aus dem InputStream direkt in den Puffer des ByteArrayOutputStreams liest. Ist außerdem die Menge der Daten, die man der available Methode entnehmen kann, dicht an der Gesamtmenge oder enstpricht dieser sogar, läßt sich die Geschwindigkeit zum Einlesen um fast die Hälfte erhöhen. Bei einem FileInputStream erhält man via available() recht zuverlässig die Gesamtmenge der zu erwartenden Bytes, was unnötiges Zielpuffer vergrößern erspart.


    public class ByteBufferByteArrayOutputStream extends ByteArrayOutputStream {
ByteBufferByteArrayOutputStream() {
super(0);
}

public void attachBytesFromStream(InputStream in) throws IOException {
final int bufferSize = 2048;
final int available = in.available();
final int capacity = available > 0 ? available : bufferSize;

this.enlargeCapacityBy(capacity + bufferSize);
int read = 0;
while(read >= 0) {
super.count += read;
if(super.buf.length - count < bufferSize) {
this.enlargeCapacityBy(bufferSize);
}
read = in.read(super.buf, super.count, bufferSize);
}
}

private void enlargeCapacityBy(int size) {
byte newbuf[] = new byte[Math.max(buf.length << 1, super.buf.length + size)];
System.arraycopy(buf, 0, newbuf, 0, count);
super.buf = newbuf;
}

public ByteBuffer getByteBuffer() {
return ByteBuffer.wrap(super.buf, 0, super.count);
}
}

Weil das Einlesen immer in 2048 byte Schritten geschieht, ist das zugrundeliegende Bytearray am Ende etwas größer. Um trotzdem sinnvoll damit zu arbeiten wird das Bytearray in einen ByteBuffer gemapped, um anschließend komfortabel auf den Inhalt zugreifen zu können, ohne die überflüssigen 0 bytes am Ende berücksichtigen zu müssen. Benötigt man jedoch ein primitives Bytearray, kommt man um das Umkopieren leider nicht herum. Damit währe dann auch der Zeitgewinn hinüber.


Eine weitere flotte Möglichkeit währe da noch direkt auf den FileChannel zu gehen und die Datei via read(ByteBuffer) zu lesen. Dann kann man aber nicht mehr mit jeden x beliebigen InputStream verarbeiten.