Ich habe ein Java-Klasse für Euch

J

jpv

Aktives Mitglied
Thread Starter
Dabei seit
09.06.2006
Beiträge
159
Reaktionspunkte
11
Moin allerseits,

ich hoffe, ich bin im richtigen Forum ... Ich war mal wieder in der Situation, einen Writer für einen OutputStream vorzumachen. Es gibt dafür eine Implementation, die in zahlreichen Foren mit dem Hinweis des schlechten Stils zu finden ist, die für jeden write()-Aufruf die Bytes mit new String(byte[], String charset) konvertiert.
In den meisten Fällen würde ich einen anderen Weg suchen, der den Einsatz eines InputStreamReaders erlaubt. Aber es gibt Fälle, in denen ein Object eine Methode hat, seinen Inhalt auf einen OutputStream zu schreiben. Wenn man nun weiß, dass es sich um einen UTF-8 Character-Stream handelt, möchte man möglicherweise direkt auf einen Writer schreiben.
Ich habe nun diese Klasse mit einem java.nio.CharsetDecoder umgesetzt.

Was haltet Ihr davon?

Gruß, jpv.

Code:
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

public class WriterOutputStream extends OutputStream
{
	private final ByteBuffer		bbuf;
	private final CharBuffer		cbuf;
	private final CharsetDecoder	csd;
	private final Writer			out;

	public WriterOutputStream(String charset, Writer pOut)
	{
		this(Charset.forName(charset).newDecoder(), pOut);
	}
	public WriterOutputStream(CharsetDecoder pCsd, Writer pOut)
	{
		csd = pCsd;
		out = pOut;
		bbuf = ByteBuffer.allocate(8);
		bbuf.limit(0);
		cbuf = CharBuffer.allocate(8);
	}

	@Override
	public void write(int b) throws IOException
	{
		if (bbuf.remaining() > 0)
			bbuf.compact();
		else
			bbuf.clear();
		bbuf.put((byte) b);
		bbuf.limit(bbuf.position());
		bbuf.position(0);
		cbuf.clear();
		csd.decode(bbuf, cbuf, false);
		if (cbuf.position() > 0)
			out.write(cbuf.array(), cbuf.arrayOffset(), cbuf.position());
	}
	@Override
	public void write(byte[] b, int off, int len) throws IOException
	{
		while (len > 0) {
			if (bbuf.remaining() > 0)
				bbuf.compact();
			else
				bbuf.clear();
			int wlen = len > bbuf.remaining() ? bbuf.remaining() : len;
			bbuf.put(b, off, wlen);
			len -= wlen;
			off += wlen;
			bbuf.limit(bbuf.position());
			bbuf.position(0);
			cbuf.clear();
			csd.decode(bbuf, cbuf, false);
			if (cbuf.position() > 0)
				out.write(cbuf.array(), cbuf.arrayOffset(), cbuf.position());
		}
	}

	@Override
	public void flush() throws IOException
	{
		out.flush();
	}
	@Override
	public void close() throws IOException
	{
		try {
			cbuf.clear();
			csd.decode(bbuf, cbuf, true);
			csd.flush(cbuf);
			if (cbuf.position() > 0)
				out.write(cbuf.array(), cbuf.arrayOffset(), cbuf.position());
			out.flush();
		} catch (Exception e) {
			IOUtil.throwIOE(e);
		} finally {
			out.close();
		}
	}
}
 
Das das nutzen von final Buffern statt stetig neuer Objekte die bessere Variante ist, ist klar. Schont es doch den Heap und sorgt für bessere Performanz. Ansonsten sind undokumentierte Klassen wenig hilfreich für Newbies. Die werden Dich kaum verstehen.

Viele Java-Programmierer haben wir hier nicht, wunder Dich also nicht wenn es geringes feedback gibt.
 
Ja, Doku wäre nicht schlecht. Man muss sich nämlich erst den Quellcode angucken, damit man versteht ob charset jetzt der Input- oder Outputzeichensatz ist.
 
Moin,
ja das mit der Doku ist so eine Sache ... sie stört mich meistens ;)
Aber Ihr habt natürlich vollkommen recht, dass diese 'Veröffentlichung' ohne Doku ziemlich zweckfrei war - Entschuldigung Forum! Ich habe hier auf die Schnelle etwas eingefügt.
Die Motivation für diese Klasse war ja, dass in der im Netz zu findenden Implementation ein Haken steckt. Wenn die Bytes einzeln mit write(int) eingeliefert werden, werden UTF-8-Multibyte-Zeichen auseinandergerissen. Dies kann auch passieren, wenn sie auf byte[]-Grenzen liegen. Wenn nun daraus ein String gebildet wird, bricht die Konvertierung mit einer Exception ab, weil die UTF8-Sequenz nicht mehr vollständig ist.
Bei dieser Klasse tritt nun bisher eine leider etwas unverständliche BufferOverflowException auf, wenn man versucht Latin1-kodierte Strings mit einem UTF8-Dekodierer zu bearbeiten.


Code:
package stanko.util.io;

import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

/**
 * Diese Klasse konvertiert eingehende Bytes mit Hilfe eines {@link
 * CharsetDecoder}. Mit <code>new WriterOutputStream("<i>charset</i>", <i>{@link
 * Writer}</i>)</code> erhaelt man einen {@link OutputStream}, der den
 * eingehenden Bytestrom auf einen Writer ausgibt.<br>
 * Wenn der Bytestrom korrupt ist - z.B. beim Schreiben von Latin1-Kodierten
 * Strings - wir eine {@link BufferOverflowException} geworfen!
 * @author jpv
 */
public class WriterOutputStream extends OutputStream
{
	// Puffer fuer eingehende Bytes
	private final ByteBuffer		bbuf;
	// Puffer fuer ausgehende Character
	private final CharBuffer		cbuf;
	// Zeichensatz-Dekodierer
	private final CharsetDecoder	csd;
	// Ausgabe
	private final Writer			out;

	/**
	 * Es wird {@link Charset#defaultCharset()}.newDecoder() verwendet.
	 * @param pOut - der Writer, auf den geschrieben werden soll.
	 */
	public WriterOutputStream(Writer pOut)
	{
		this(Charset.defaultCharset().newDecoder(), pOut);
	}
	/**
	 * @param charset - Name des Zeichensatzes
	 * @param pOut - {@link Writer} auf den geschrieben werden soll.
	 */
	public WriterOutputStream(String charset, Writer pOut)
	{
		this(Charset.forName(charset).newDecoder(), pOut);
	}
	/**
	 * @param pCsd - der Zeichensatz-Dekodierer
	 * @param pOut - {@link Writer} auf den geschrieben werden soll.
	 */
	public WriterOutputStream(CharsetDecoder pCsd, Writer pOut)
	{
		csd = pCsd;
		out = pOut;
		bbuf = ByteBuffer.allocate(8);
		bbuf.limit(0);
		cbuf = CharBuffer.allocate(8);
	}

	@Override
	public void write(int b) throws IOException
	{
		if (bbuf.remaining() > 0)
			bbuf.compact();
		else
			bbuf.clear();
		bbuf.put((byte) b);
		bbuf.limit(bbuf.position());
		bbuf.position(0);
		cbuf.clear();
		csd.decode(bbuf, cbuf, false);
		if (cbuf.position() > 0)
			out.write(cbuf.array(), cbuf.arrayOffset(), cbuf.position());
	}
	@Override
	public void write(byte[] b, int off, int len) throws IOException
	{
		while (len > 0) {
			if (bbuf.remaining() > 0)
				bbuf.compact();
			else
				bbuf.clear();
			int wlen = len > bbuf.remaining() ? bbuf.remaining() : len;
			bbuf.put(b, off, wlen);
			len -= wlen;
			off += wlen;
			bbuf.limit(bbuf.position());
			bbuf.position(0);
			cbuf.clear();
			csd.decode(bbuf, cbuf, false);
			if (cbuf.position() > 0)
				out.write(cbuf.array(), cbuf.arrayOffset(), cbuf.position());
		}
	}

	@Override
	public void flush() throws IOException
	{
		out.flush();
	}
	@Override
	public void close() throws IOException
	{
		try {
			cbuf.clear();
			csd.decode(bbuf, cbuf, true);
			csd.flush(cbuf);
			if (cbuf.position() > 0)
				out.write(cbuf.array(), cbuf.arrayOffset(), cbuf.position());
			out.flush();
		} catch (Exception e) {
			IOUtil.throwIOE(e);
		} finally {
			out.close();
		}
	}
}
 
Es gibt dafür eine Implementation, die in zahlreichen Foren mit dem Hinweis des schlechten Stils zu finden ist, die für jeden write()-Aufruf die Bytes mit new String(byte[], String charset) konvertiert.

Hi jpv,

ich frag mich wieso new String(byte[], String charset) schlechter Stil sein soll.

Ich find's so nämlich extrem geil. Man kann in einem einzigen Statement die rohen Byte-Daten einer Textdatei in einen String wandeln.

Besser geht's nicht.

Oder verstehe ich Dich falsch?


Schönen Gruß
Fabian
 
Moin Fabian,

wenn man alle Bytes sowieso gerade im Speicher hält, ist das natürlich die Methode der Wahl. Wenn man aber nicht weiß, ob einem gerade die Bibel zugesandt wird oder was einen sonst so erwartet und man die Konvertierung deshalb 'on the fly' vornehmen möchte, muss man eine andere Methode suchen. Diese 'On-The-Fly'-Konvertierung zerlegt das Ausgangswerk in einzelne Blöcke. Wenn diese Teilung ein Multibyte-Zeichen (z.B. UTF-8) trennt, schlägt die Konvertierung fehl, weil für diese Puffer new String(byte[], String charset) ungültig ist. Dafür setze ich in dieser Klasse die NIO-CharsetDecoder ein, die sich so ein 'halbes' UTF8-Zeichen merken, bis die fehlenden Bytes hereinkommen.

... verständlich Schreiben muss ich mal wieder üben ...

Gruß und schönes Wochenende noch ;-)
 
Na dieser Textblock gehört in die Klassendoku ;)
 
Zurück
Oben Unten