„Hallo Amigo! Heute werde ich dir ein paar interessante Dinge über die Klasse BufferedInputStream erzählen, aber beginnen wir mit ‚Wrappern‘ und einem ‚Beutel Zucker‘.“

„Was meinst du mit ‚Wrapper‘ und ‚Beutel Zucker‘?

„Das sind Metaphern. Hör zu. Also...“

Das „Wrapper“- (oder „Decorator“-)Designmuster ist ein ziemlich einfacher und praktischer Mechanismus zur Erweiterung der Objektfunktionalität ohne Verwendung von Vererbung.

BufferedInputStream - 1

Nehmen wir an, wir haben eine Cat-Klasse mit zwei Methoden: getName und setName:

Java-Code Beschreibung
class Cat
{
 private String name;
 public Cat(String name)
 {
  this.name = name;
 }
 public String getName()
 {
  return this.name;
 }
 public void setName(String name)
 {
  this.name = name;
 }
}
Die Klasse Cat hat zwei Methoden: getName und setName
public static void main(String[] args)
{
 Cat cat = new Cat("Oscar");

 printName(cat);
}

public static void printName(Cat cat)
{
 System.out.println(cat.getName());
}
So könnte sie verwendet werden.

„Oscar“ wird auf der Konsole angezeigt.

Angenommen, wir müssen einen Methodenaufruf mit einem Cat-Objekt abfangen und vielleicht einige kleine Änderungen vornehmen. Dazu müssen wir ihn in seine eigene Wrapper-Klasse einhüllen (wrap).

Wenn wir unseren eigenen Code um die Methodenaufrufe mit einem Objekt „hüllen“ wollen, dann müssen wir folgendes tun:

1) Unsere eigene Wrapper-Klasse erstellen und von derselben Klasse/Interface wie das einzuhüllende Objekt erben.

2) Das einzuhüllende Objekt an den Konstruktor unserer Klasse übergeben.

3) Alle Methoden in unserer neuen Klasse überschreiben. Rufe die Methoden des eingehüllten Objekts innerhalb jeder der überschriebenen Methoden auf.

4) Gewünschte Änderungen vornehmen: ändern, was die Methodenaufrufe tun, Parameter ändern oder etwas anderes.

Im folgenden Beispiel fangen wir Aufrufe der getName-Methode eines Cat-Objektes ab und ändern den Rückgabewert geringfügig.

Java-Code Beschreibung
class Cat
{
 private String name;
 public Cat(String name)
 {
  this.name = name;
 }
 public String getName()
 {
  return this.name;
 }
 public void setName(String name)
 {
  this.name = name;
 }
}
Die Klasse Cat enthält zwei Methoden: getName und setName
class CatWrapper extends Cat
{
 private Cat original;
 public CatWrapper (Cat cat)
 {
  super(cat.getName());
  this.original = cat;
 }

 public String getName()
 {
  return "A cat named " + original.getName();
 }

 public void setName(String name)
 {
  original.setName(name);
 }
}
Die Wrapper-Klasse. Die Klasse speichert keine Daten außer einer Referenz auf das Originalobjekt.
Die Klasse ist in der Lage, Aufrufe an das Originalobjekt (setName), das an den Konstruktor übergeben wurde, „auszulösen“. Sie kann diese Aufrufe auch „abfangen“ und ihre Parameter und/oder Ergebnisse verändern.
public static void main(String[] args)
{
 Cat cat = new Cat("Oscar");
 Cat catWrap = new CatWrapper (cat);
 printName(catWrap);
}

public static void printName(Cat named)
{
 System.out.println(named.getName());
}
So könnte sie verwendet werden.

„Eine Katze namens Oscar“
wird auf der Konsole angezeigt

Mit anderen Worten: Wir ersetzen jedes Originalobjekt stillschweigend durch ein Wrapper-Objekt, das eine Verknüpfung zum Originalobjekt erhält. Alle Methodenaufrufe mit dem Wrapper werden an das Originalobjekt weitergeleitet, und alles läuft wie ein Uhrwerk.

„Das gefällt mir. Die Lösung ist einfach und funktionell.“

„Ich erzähle dir auch von einem ‚Beutel Zucker‘. Das ist eher eine Metapher als ein Entwurfsmuster. Eine Metapher für das Wort Puffer und Pufferung. Was ist Pufferung und wofür brauchen wir das?“

BufferedInputStream - 2

Nehmen wir einmal an, heute ist Ritschie mit dem Kochen an der Reihe und du hilfst ihm dabei. Ritschie ist noch nicht hier, aber ich möchte Tee trinken. Ich bitte dich, mir einen Löffel Zucker zu bringen. Du gehst in den Keller und findest einen Beutel Zucker. Du kannst mir den ganzen Beutel bringen, aber ich brauche nicht den ganzen Beutel. Ich brauche nur einen Löffel voll. Dann nimmst du, wie ein guter Roboter, einen Löffel voll und bringst ihn mir. Ich gebe den Zucker in den Tee, aber er ist immer noch nicht süß genug. Also bitte ich dich, mir noch einen weiteren Löffel Zucker zu bringen. Du gehst wieder in den Kelle rund holst noch einen Löffel Zucker. Dann kommt Ellie, und ich bitte dich, auch für sie Zucker mitzubringen... Das alles dauert viel zu lange und ist ineffizient.

Ritschie kommt, sieht das alles und bittet dich, ihm eine Zuckerdose voller Zucker zu bringen. Dann bitten Ellie und ich Ritschie um Zucker. Er stellt uns einfach die Zuckerdose hin und fertig.

Was nach dem Auftauchen von Ritschie passiert ist, nennt man Pufferung: Die Zuckerdose ist ein Puffer. Dank der Pufferung können „Clients“ Daten in kleinen Portionen aus einem Puffer lesen, während der Puffe sie in großen Portionen von der Quelle liest, um Zeit und Arbeit zu sparen.

„Das ist ein cooles Beispiel, Kim. Das verstehe ich absolut. Die Bitte um einen Löffel Zucker ist wie das Lesen eines Bytes aus einem Stream.“

„Exakt. Die Klasse BufferedInputStream ist ein klassisches Beispiel für einen gepufferten Wrapper. Sie umhüllt die InputStream-Klasse. Sie liest Daten aus dem ursprünglichen InputStream in großen Blöcken in einen Puffer und ruft sie dann Stück für Stück aus dem Puffer ab, während wir daraus lesen.“

„Sehr schön. Alles klar. Gibt es Puffer auch für Schreibvorgänge?“

„Na sicher.“

„Hast du vielleicht ein Beispiel?“

„Stell dir eine Mülltonne vor. Anstatt jedes Mal raus zu gehen und den Müll in die Müllverbrennungsanlage zu werfen, wirft man ihn einfach in die Mülltonne. Und die stellt man dann einfach alle zwei Wochen an die Straße. Ein klassischer Puffer.“

„Sehr interessant! Und übrigens viel einleuchtender als ein Beutel Zucker.“

„Und die flush()-Methode ist so, als würde man den Müll sofort rausbringen. Du kannst sie benutzen, bevor die Gäste kommen.“