Reader & Writer
Streams
Streams sind Datenströme, die verwendet werden, um Daten in ein Projekt ein- oder aus einem Projekt auszulesen. Quell- und/oder Zielort könnte dabei z.B. eine Datei sein.
Alle wichtigen Ein- und Ausgabeklassen sind im Java-Paket java.io (Input/Output) definiert.
Streams arbeiten in Java entweder byte- oder zeichenorientiert. Zeichenorientierte Streams sind sinnvoll, wenn Zeichen, Wörter und Texte verarbeitet werden sollen. Byteorientiert Streams können für alle anderen Datenformate genutzt werden, z.B. auch für Objekte.
Dieses Beispielprogramm (siehe Beispielprojekt IO) nutzt In- und OutputStreams, um eine Datenquelle zu kopieren. Hier wird zum einen eine lokale Datei, zum anderen der Inhalt einer Webseite kopiert.
Streams müssen grundsätzlich geschlossen werden, wenn sie nicht mehr benutzt werden. Andernfalls kann es zu Datenverlust kommen - bspw. dann, wenn ein BufferedOutputStream nicht geschlossen wird (und noch Daten im Puffer enthalten sind, die eigentlich geschrieben werden müssten). Zudem blockieren einige Betriebssysteme (Windows 95 - Vista) den Zugriff auf Dateien, die von nicht geschlossen Streams referenziert werden.
Reader
Etwas komfortabler als InputStreams sind Reader - diese bieten teilweise einfachere Funktionen, die z.B. das Konvertieren eines Streams in einen String übernehmen.
Vor allem sorgen Reader dafür, dass Texte korrekt eingelesen werden können, wenn ein Encoding verwendet wird, in dem ein Zeichen durch mehr als ein Byte dargestellt wird.
Da ein Reader davon ausgeht, dass in einer Textdatei das Standard-Encoding des Betriebssystems verwendet wird (ISO-8859-1 unter Windows, UTF-8 unter einigen Linux-Distributionen usw.), dieses Programm aber auf jeder Plattform korrekt arbeiten soll, muss das Encoding explizit gesetzt werden. Dies geschieht im Konstruktor des InputStreamReaders.
Beispiel für einen Reader:
Das zeichenweise Lesen einer Datei ist relativ langsam, denn für sehr wenig Daten (1 Zeichen) muss hoher Aufwand (Position auf Festplatte suchen o.ä.) betrieben werden. Um die Geschwindigkeit der Lese- und Schreibzugriffe zu optimieren, werden gepufferte Streams (buffered Streams) verwendet. Diese sorgen dafür, dass Daten nicht zeichenweise, sondern "am Stück" eingelesen bzw. geschrieben werden.
Beim ersten Aufruf von read() wird nicht nur ein Zeichen gelesen, sondern eine Reihe von Zeichen, die in einem Zwischen-Speicher abgelegt werden. Bei weiteren Aufrufen von read() werden Daten aus dem Zwischenspeicher zurückgegeben, bis dieser leer ist und erneut eine Reihe von Zeichen aus der eigentlichen Quelle gelesen werden kann.
Reader ist die (abstrakte) Superklasse von BufferedReader und FileReader - d.h. ein BufferedReader IST ein Reader (FileReader ebenfalls), und so können Instanzen beider Klassen an diese Methode übergeben werden. Wenn spezifische Methoden einer Klasse nicht benötigt werden (wie z.B. der Konstruktor der Klasse FileReader, der ein File-Objekt benötigt und der von der Klasse BufferedReader nicht angeboten wird), ist es auch nicht nötig, in einer Methodendeklaration diese Klasse zu verlangen. Wird stattdessen eine Superklasse gewählt, so wird eine Methode automatisch flexibler, gleichzeitig entfällt die Notwendigkeit, ähnliche Methoden parallel zu warten. Dieses Prinzip nennt sich Substitutionsprinzip und wurde bereits bei der Einführung der Vererbung angesprochen - hier finden Sie nun einmal ein konkretes Beispiel (Beispiel aus IOTime) (siehe auch: http://de.wikipedia.org/wiki/Liskovsches_Substitutionsprinzip).
Serialisierung von Objekten
Die Standardserialisierung in Java bietet die Möglichkeit, Objekte mit ihrer Struktur und all ihren Zuständen zu persistieren. Sie können über einen byteorientierte Stream z.B. in eine Datei geschrieben werden, aus der sie anschließend vollständig wieder hergestellt (deserialisiert) werden können.
Objekte, die serialisiert werden sollen, müssen das Interface Serializable implementieren, sonst wird eine NotSerializableException geworfen. Dieses Interface ist nur ein Marker, der angibt, dass dieses Objekt persistiert werden kann, es enthält also keine Klassen, die implementiert werden müssen. Es gibt Java-Klassen, die schon serialisierbar sind (z.B. java.util.Date), um unsere eigenen Projekte persistieren zu können, muss das Interface jedoch implementiert werden.