Bin heute zufällig über folgende Methode gestolpert:
String readToString1(InputStream in) throws IOException
{
byte[]buf = new byte[256];
StringBuilder sb = new StringBuilder();
int n;
do {
n = in.read(buf, 0, 256);
if (n > 0) {
String s = new String(buf, 0, n, "UTF-8");
sb.append(s);
}
} while(n > 0);
return sb.toString();
}
Diese Funktion soll einen InputStream dessen Zeichen UTF-8 codiert sind in einen String lesen. Problem (mal abgesehen von der unnötigen Verwendung des if und den temporär angelegten String Objekten) bei dieser Funktion ist allerdings, dass der 256-byte Puffer in einen String umgewandelt wird, denn dabei werden ein oder mehrere Bytes durch den Zeichenkonverter gelesen. Falls der Zeichenkonverter dabei am ende des Puffers anlangt, so ist das Zeichen unvollständig. Das führt dann dazu dass ein Ersetzungszeichen (das Fragezeichen) am Ende des String steht. Man sollte solche starren bytepuffer also vermeiden, wenn man diese in Zeichen umwandeln will.
Besser ist folgendes Vorgehen:
String readToString2(InputStream in) throws IOException
{
char[] buf = new char[128];
StringBuilder sb = new StringBuilder();
Reader r = new InputStreamReader(in, "UTF-8");
int n;
while((n = r.read(buf, 0, 128)) != -1)
{
sb.append(buf, 0, n);
}
return sb.toString();
}
Um das unterschiedliche Verhalten zu testen, erzeuge ich einen InputStream der an ungeraden Byte Positionen Umlaute hat (also zuerst ein 1 byte Zeichen und dann lauter 2 byte Umlaute) und übergebe beiden Testmethoden diese Streams. Zusätzlich benutze ich noch Mockito, um zu ermitteln welche Methoden wie aufgerufen wurden:
import static org.mockito.Mockito.*;
public static void main(String[] args) throws IOException
{
byte[] umlaut = "ö".getBytes("UTF-8");
byte[] inbuf = new byte[513];
inbuf[0] = 'X'; for(int i = 0;i<256;i++)
{inbuf[i*2+1] = umlaut[0]; inbuf[i*2+2] = umlaut[1]; }
InputStream in1 = spy(new ByteArrayInputStream(inbuf));
System.out.println(" readToString1()=" + readToString1(in1));
// make sure the inputstream was used with efficient block reads
verify(in1, times(4)).read(any(byte[].class), eq(0), eq(256));
verifyNoMoreInteractions(in1);
InputStream in2 = spy(new ByteArrayInputStream(inbuf));
System.out.println(" readToString2()=" + readToString2(in2));
// make sure the inputstream was used with efficient block reads
verify(in2, atMost(2)).read(any(byte[].class), eq(0), eq(8192));
verify(in2, atMost(1)).available();
verifyNoMoreInteractions(in2);
}
Und hier das Ergebnis (gekürtzt):
readToString1()=Xööö...ööö??ööö...ööö??
readToString2()=Xööö...öööööööö...ööööö
Am Ende jeden Puffers zerschneidet die erste Methode die 2 Bytes eines Umlauts, und deswegen erscheinen an diesen Stellen das Füllzeichen des Character Konverters. Bei UTF-8 streams ist es unwahrscheinlich dass ein multi-byte Zeichen ausgerechnet genau auf eine Blockgrenze fällt - umso unwahrscheinlicher ist es, dass ein Problem damit beim Testen auffällt.
Übrigens ist es nicht notwendig hier einen BufferedInputStream oder BufferedReader zu verwenden. Der Reader wird ja bereits mit einem char array buffer (und nicht einzelnen Zeichen) gelesen. Zudem liest der InputSreamReader() aus dem darunterliegenden InputStream mit einem StreamDecoder der einen eigenen Lesepuffer (bei den Sun Klassen ist das ein 8kb Puffer) hat.