– Cześć, Amigo! Dzisiaj opowiem Ci kilka ciekawych rzeczy o klasie BufferedInputStream, ale zacznijmy od «wrapperów» i «torebki cukru».

– Co masz na myśli mówiąc «wrapper» i «torebka cukru»?"

– To metafory. Posłuchaj. Tak więc…

Wzorzec projektowy «wrapper» (lub «dekorator») jest dość prostym i wygodnym narzędziem służącym do rozszerzania funkcjonalności obiektu bez użycia dziedziczenia.

BufferedInputStream - 1

Załóżmy, że mamy klasę Cat z dwiema metodami: getName i setName:

Kod Java Opis
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;
 }
}
Klasa Cat posiada dwie metody: getName i setName
public static void main(String[] args)
{
 Cat cat = new Cat("Oskar");

 printName(cat);
}

public static void printName(Cat cat)
{
 System.out.println(cat.getName());
}
Przykład, jak można go wykorzystać.

«Oskar» zostanie wyświetlony w konsoli.

Załóżmy, że musimy przejąć wywołanie metody na obiekcie cat i być może wprowadzić jakieś drobne zmiany. W tym celu musimy opakować go w jego własną klasę opakowującą.

Jeśli chcemy „opakować” nasz własny kod wokół wywołań metody na jakimś obiekcie, to musimy:

1) Utworzyć własną klasę opakowującą i dziedziczyć tą samą klasę/interfejs jak obiekt, który ma być opakowany.

2) Przekazać obiekt, który ma być opakowany do konstruktora naszej klasy.

3) Nadpisać wszystkie metody w naszej nowej klasie. Wywołać metody opakowanego obiektu wewnątrz każdej z nadpisanych metod.

4) Dokonać dowolnych zmian: zmienić to, co powodują wywołania metod, zmienić ich parametry, i/lub zrobić coś innego.

W poniższym przykładzie przejmujemy wywołania do metody getName obiektu Cat i nieznacznie zmieniamy jej zwracaną wartość.

Kod Java Opis
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;
 }
}
Klasa Cat zawiera dwie metody: getName i setName.
class CatWrapper extends Cat
{
 private Cat original;
 public CatWrapper (Cat cat)
 {
  super(cat.getName());
  this.original = cat;
 }

 public String getName()
 {
  return "Kot o imieniu " + original.getName();
 }

 public void setName(String name)
 {
  original.setName(name);
 }
}
Klasa wrapper. Klasa ta nie przechowuje żadnych danych poza wywołaniem do oryginalnego obiektu.
Klasa ta jest w stanie „wyrzucać” wywołania do oryginalnego obiektu (setName) przekazanego do konstruktora. Może również „łapać” te wywołania i modyfikować ich parametry i/lub wyniki.
public static void main(String[] args)
{
 Cat cat = new Cat("Oskar");
 Cat catWrap = new CatWrapper (cat);
 printName(catWrap);
}

public static void printName(Cat named)
{
 System.out.println(named.getName());
}
Przykład, jak można go wykorzystać.

«Kot o imieniu Oskar»
zostanie wyświetlone w konsoli

Innymi słowy po cichu wymieniamy każdy oryginalny obiekt na obiekt opakowujący, który otrzymuje odnośnik do oryginalnego obiektu. Wszystkie wywołania metody na wrapperze są przekazywane do oryginalnego obiektu, co sprawia, że wszystko działa jak w zegarku.

– I to mi się podoba. Rozwiązanie jest proste i funkcjonalne.

– Opowiem ci też o «torebce cukru». To jest metafora, a nie wzorzec projektowy. Metafora dla bufora i buforowania. Co to jest buforowanie i dlaczego go potrzebujemy?

BufferedInputStream - 2

Powiedzmy, że dzisiaj Raszi gotuje, a Ty mu pomagasz. Jeszcze go nie ma, ale ja chcę napić się herbaty. Proszę Cię o przyniesienie mi łyżeczki cukru. Idziesz do piwnicy i znajdujesz torebkę cukru. Możesz przynieść mi całą torebkę, ale ja nie potrzebuję aż tyle. Potrzebuję tylko jedną łyżeczkę. Więc, jak na dobrego robota przystało, bierzesz jedną łyżeczkę i mi ją przynosisz. Dodaję ją do herbaty, ale nadal nie jest wystarczająco słodka. I proszę Cię o przyniesienie mi jeszcze jednej. Znowu idziesz do piwnicy i przynosisz kolejną łyżeczkę. Potem przychodzi Basia i proszę, żebyś przyniósł cukier też dla niej... To wszystko trwa zbyt długo i jest nieefektywne.

Przychodzi Raszi, widzi to wszystko i prosi, żebyś przyniósł mu miskę z cukrem. Wtedy Basia i ja zaczynamy prosić Rasziego o cukier. On po prostu podaje go nam z miski z cukrem i to wszystko.

To, co stało się po pojawieniu się Rasziego, nazywa się buforowaniem: miska cukru jest buforem. Dzięki buforowaniu „klienci” mogą odczytywać dane z bufora w małych porcjach, podczas gdy bufor, aby zaoszczędzić czas i wysiłek, odczytuje je ze źródła w dużych porcjach.

– To świetny przykład, Kim. Doskonale to teraz rozumiem. Prośba o łyżeczkę cukru jest jak czytanie jednego bajta ze strumienia.

– Dokładnie. Klasa BufferedInputStream to klasyczny przykład buforowanego wrappera. Stanowi opakowanie dla klasy InputStream. Wczytuje ona dane z oryginalnego InputStream w dużych blokach do bufora, a następnie, gdy z niego odczytujemy, wyciąga je z bufora kawałek po kawałku.

– Bardzo dobrze. Wszystko jasne. Czy istnieją bufory służące do zapisywania?

– Och, pewnie.

– Może dasz jakiś przykład?

– Wyobraź sobie kosz na śmieci. Zamiast wychodzić na zewnątrz i za każdym razem wrzucać śmieci do spalarni, po prostu wrzucasz je do kosza. Następnie Twój brat raz na dwa tygodnie wynosi go na zewnątrz. To klasyczny przykład bufora.

– Ciekawe! A przy okazji, o wiele jaśniejsze niż przykład z torebką cukru.

– A metoda flush() jest jak natychmiastowe wynoszenie śmieci. Można jej użyć przed przybyciem gości.