Java Videotraining Werbung

Videotraining Spring 3 Boot Werbung

1. Ein-/Ausgabeströme

Im vorherigen Kapitel, »Dateien und wahlfreier Zugriff auf Dateiinhalte«, sowie Kapitel »Exceptions«, haben wir uns schon mit dem grundlegenden Lesen und Schreiben von Dateiinhalten beschäftigt. Diese Kapitel legt den Fokus auf die Datenströme, also den kontinuierlichen Fluss von Daten; sie können in ein Ziel geschrieben oder aus einer Quelle gelesen werden und dabei durch mehrere Filter laufen. Die Verschachtelung von Javas Ein-/Ausgabeströmen ist ein gutes Beispiel für eine Abstraktion und Flexibilität, das auch bei der Modellierung von eigenen Filtern sehr hilft.

Voraussetzungen

  • Typhierarchie von Ein-/Ausgabeklassen kennen

  • zeichenorientierte und byteorientierte Klassen unterscheiden können

  • Dekoration von Strömen verstehen

  • Stromdaten durch Filter schicken können

  • Daten komprimieren können

Verwendete Datentypen in diesem Kapitel:

1.1. Direkte Datenströme

In Java werden vier Typen unterschieden: InputStream und OutputStream (byteorientiertes Lesen und Schreiben) sowie Reader und Writer (zeichenorientiertes Lesen und Schreiben). Wir beginnen mit den Aufgaben zunächst mit genau solchen Strömen, die direkt Daten in eine Ressource schreiben oder direkt aus einer Ressource lesen.

1.1.1. Anzahl unterschiedlicher Stellen ermitteln (Dateien lesen) ⭐

Captain CiaoCiao bekommt zwei Textdateien, und die sehen auf den ersten Blick sehr gleich aus. Aber er möchte genau wissen, ob die beiden Dateien exakt übereinstimmen oder ob es Unterschiede gibt.

Aufgabe:

  • Schreibe eine Methode long distance(Path file1, Path file2), die die Anzahl unterschiedlicher Stellen zurückgeliefert. In der Informatik wird das Hamming-Abstand (engl. Hamming distance) genannt.

  • Es ist davon auszugehen, dass die beiden Dateien exakt gleich lang sind.

Beispiel: Eine Datei enthält den String

To Err is Human. To Arr is Pirate.

und eine andere Datei den String

To Arr is Human. To Err is Pirate!

Die Distanz ist 3, weil 3 Symbole nicht übereinstimmen.

1.1.2. Python-Programm in Java konvertieren (Datei schreiben) ⭐

In Kapitel »Imperative Sprachkonzepte« haben wir verschiedene Aufgaben gelöst, die SVG-Ausgaben auf den Bildschirm schreiben — nun wollen wir diese Ausgaben direkt in HTML-Dateien schreiben. Zur Erinnerung: Folgendes HTML enthält eine SVG mit einem Rechteck mit einer Höhe und Breite von 1 und den x-/y-Koordinaten 10/10:

<!DOCTYPE html>
<html><body>
 <svg width="256" height="256">
  <rect x="10" y="10" width="1" height="1" style="fill:rgb(0,29,0);" />
 </svg>
</body></html>

In einem Buch über computergenerierte Kunst findet Captain CiaoCiao auf den ersten Seiten eine Abbildung. Das Muster wird von einem Python-Programm erzeugt:

import Image, ImageDraw

image = Image.new("RGB", (256, 256))
drawingTool = ImageDraw.Draw(image)

for x in range(256):
    for y in range(256):
        drawingTool.point((x, y), (0, x^y, 0))

del drawingTool
image.save("xorpic.png", "PNG")

Die Python-Funktion point(…​) bekommt die x-y-Koordinate und RGB-Farbinformationen, wobei die drei Argumente 0, x^y, 0 für die Anteile Rot, Grün, Blau stehen.

Aufgabe:

  • Da Captain CiaoCiao keine Schlangen mag, muss das Python-Programm in ein Java-Programm konvertiert werden.

  • Statt einer PNG-Datei soll am Ende eine HTML-Datei mit einem SVG-Block entstehen, in der jedes Pixel ein 1 × 1 großes SVG-Rechteck ist.

Bonus: Öffne am Ende die HTML-Datei mit dem Browser — hier hilft die Desktop-Klasse.

1.1.3. Zielcode generieren (Datei schreiben) ⭐

Von der Post bekommt Captain CiaoCiao immer öfter Briefe mit rosafarbenem Strichcode. Er denkt erst an kodierte Liebesbotschaften von Bonny Brain, muss dann aber feststellen, dass auf dem Briefumschlag ein sogenannter Zielcode steht, der die Postleitzahl kodiert.

Generate target code

Die Kodierung der Zahlen in Strichen ist wie folgt, wobei der Unterstrich _ den Abstand durch ein Leerzeichen symbolisiert:

Tabelle 1. Werte und Kodierungen
WertKodierung

0

| | | |

1

| | | _

2

| | _ |

3

| | _ _

4

| _ | |

5

| _ | _

6

| _ _ |

7

_ | _ |

8

_ | | |

9

_ | | _

Aufgabe:

  • Schreibe eine statische Methode writeZielcode(String, Writer), die einen String mit Ziffern in der genannten Kodierung in einen Writer schreibt.

  • Zwischen den vier Symbolen für eine Ziffer sollen zwei Leerzeichen stehen.

Beispiel:

  • Der String "023" wird als ||||  || |  ||   in die Datei geschrieben.

Besorge von Files einen Writer, um in Dateien schreiben zu können.

Für ergänzende Informationen zur Kodierung siehe auch https://de.wikipedia.org/wiki/Zielcode.

1.1.4. Dateiinhalt in Kleinbuchstaben konvertieren (Datei lesen und schreiben) ⭐

Textkonvertierungen von einem Format in ein anderes sind übliche Vorgänge.

Aufgabe:

  • Öffne eine Textdatei, lies jedes Zeichen ein, konvertiere es in Kleinbuchstaben, und schreibe es in eine neue Datei. Schreibe eine Methode, die das erledigt, und nenne sie convertFileToLowercase(Path inPath, Path outPath).

1.1.5. PPM-Grafikformat in ASCII-Graustufen anzeigen ⭐⭐⭐

Pixelgrafiken zu generieren ist wegen der verschiedenen Formate immer etwas aufwendiger. Es gibt allerdings mit PPM (Portable Pixel Map) ein sehr einfaches ASCII-basiertes Dateiformat. Die Spezifikation (http://netpbm.sourceforge.net/doc/ppm.html) ist einfach und ein Java-Programm kann PPM-Bilder leicht generieren. Ein Nachteil unter Windows ist jedoch, dass für die Darstellung Drittanbieterprogramme nötig sind, etwa die freie Software GIMP (https://www.gimp.org/).

Das folgende Beispiel zeigt den Grundaufbau einer PPM-Datei:

P3
3 2
255
255   0   0
  0 255   0
  0   0 255
255 255   0
255 255 255
  0   0   0

Es gibt diverse Tokens, die durch Weißraum getrennt sind. Wir stellen folgende Regeln auf:

  • Das erste Token ist die Kennung P3.

  • Es folgen die Breite und Höhe des Bildes.

  • Es folgt der maximale Farbwert, wir nehmen immer 255 an.

  • Es folgen Rot-, Grün-, Blauwerte für alle Pixel von oben links nach unten rechts.

  • Höhe und Breite und die Farbwerte sind immer positiv.

Aufgabe:

  • Lies eine PPM-Datei ein, und extrahiere alle Farbwerte.

  • Übertrage jeden Farbwert auf einen Graustufenwert.

  • Jeder Punkt der Grafik soll ein ASCII-Zeichen werden. Konvertiere jeden Graustufenwert von 0 bis 255 in ein ASCII-Zeichen.

  • Erlaube im Programm die Parametrisierung der Umrechnung der RGB-Werte in den Graustufenwert, damit der Algorithmus auswechselbar ist.

  • Erlaube die Parametrisierung der Umwandlung vom Graustufenwert in das ASCII-Zeichen.

Zur Konvertierung in einen Graustufenwert lässt sich auf folgende Schnittstelle und Konstante zurückgreifen:

public interface RgbToGray {
  RgbToGray DEFAULT = (r, g, b) -> (r + g + b) / 3;
  int toGray( int r, int g, int b );
}

Java bietet mit dem IntBinaryOperator eine Abbildung von (int, int) auf ein int, aber keine funktionale Schnittstelle mit drei Parametern.

Die Durchschnittsmethode ist performant, aber entspricht nicht der menschlichen Wahrnehmung. Realistischere Abbildungen berücksichtigen, dass Durchschnittsmenschen Farben unterschiedlich stark wahrnehmen. Die bekannte »Leuchtkraft-Methode« ist: 0.21 R + 0.72 G + 0.07 B.

Die Schnittstelle IntUnaryOperator kann gut für die Abbildung eines Graustufenwerts (int) auf ein ASCII-Zeichen (char, erweitert auf int) herangezogen werden. Ein Default-Konverter kann so aussehen:

public enum GrayToAscii implements IntUnaryOperator {
  DEFAULT;

  private final char[] ASCII_FOR_SHADE_OF_GRAY =
    // black = 0, white = 255
    "@MBENRWDFQASUbehGmLOYkqgnsozCuJcry1v7lit{}?j|()=~!-/<>\"^_';,:`. ".toCharArray();
  private final int CHARS_PER_RGB = 256 / ASCII_FOR_SHADE_OF_GRAY.length;
  @Override public int applyAsInt( int gray ) {
    return ASCII_FOR_SHADE_OF_GRAY[ gray / CHARS_PER_RGB ];
  }
}

Der vorgegebene String[1] ist 64 Zeichen lang. Im Prinzip heißt das: Schwarz wird @, und Weiß wird ein Leerzeichen.

Beispiel:

  • Das Ergebnis für oberes PPM ist:

    kkk
    ? @

1.1.6. Dateien portionieren (Dateien lesen und schreiben) ⭐⭐

Auf dem Anaa-Atoll läuft die Hafensoftware seit rund 40 Jahren auf einem Commodore PC-30. Bonny Brain hat den Rechner erfolgreich manipuliert, doch jetzt benötigt die Software ein Update, das über Disketten eingespielt werden muss. 3,5-Zoll-HD-Disketten können standardmäßig 1.474.560 Byte (1440 KiB) speichern. Das Software-Update passt nicht auf eine Diskette, daher ist eine Software nötig, die eine große Datei »diskettenkompatibel« in mehrere kleine Dateien zerlegt.

Aufgabe:

  • Schreibe ein Programm, das auf der Kommandozeile einen Dateinamen übergeben bekommt und dann diese Datei in mehrere kleinere Teile aufspaltet.

Beispiel:

  • Der Aufruf sieht folgendermaßen aus:

    $ java com.tutego.exercise.io.FileSplitter Hanjaab.bin

    Ist die Datei Hanjaab.bin 2440 KiB groß, dann wird das Java-Programm daraus die Dateien Hanjaab.bin.1 und Hanjaab.bin.2 mit den Größen 1440 KiB und 1000 KiB machen.

1.2. Ströme verschachteln

Ströme können wie russische Puppen verschachtelt werden; ein Strom ist im Kern die eigentliche Ressource, und andere Ströme sind wie eine Hülle darumgelegt. Die Operationen über die Hüllen gehen letztendlich in den Kern.

1.2.1. Zahlenfolgen mit dem GZIPOutputStream komprimieren ⭐

java.util.zip.GZIPOutputStream ist ein besonderer Ausgabestrom, der Daten verlustfrei komprimiert.

Aufgabe:

  • Erstelle eine komprimierte Datei mit Zahlen von 0 bis < N, die mit writeLong(…​) in einen GZIPOutputStream geschrieben werden.

  • Vergleiche die Dateigrößen für unterschiedliche N.

  • Ab welchem N lohnt sich eine Kompression?

1.2.2. Ausgaben aufspreizen mit einer Klasse TeeOutputStream ⭐⭐

Java bietet nur Klassen, die einen Ausgabestrom in genau einen anderen Ausgabestrom schreiben.

Aufgabe:

  • Schreibe eine neu Klasse TeeOutputStream, die gleichzeitig in zwei Ausgabeströme schreiben kann.

Beispiel:

  • Ausgaben sollen in zwei ByteArrayOutputStream geschrieben werden:

    ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
    ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
    
    try ( OutputStream tos = new TeeOutputStream( baos1, baos2 );
          Writer ost = new OutputStreamWriter( tos, StandardCharsets.UTF_8 );
          PrintWriter pw = new PrintWriter( ost ) ) {
      pw.print( "Hey" );
      pw.write( '\n' );
      pw.printf( "%d times %d equals %d", 2, 3, 4 );
    }
    
    System.out.println( baos1.toString( StandardCharsets.UTF_8 ) );
    System.out.println( baos2.toString( StandardCharsets.UTF_8 ) );

Hinweis: Berücksichtige, dass bei Ausnahmen auf dem einen Kanal die Operationen immer noch in den anderen Kanal geschrieben werden.

1.2.3. PrintWriter und weitere Dekoratoren zusammenbringen ⭐

In dieser Aufgabe soll eine ganze Kette an Strömen zusammengebracht werden, der → gibt an, wer wohin schreibt:

PrintWriterOutputStreamWriterBufferedOutputStreamCheckedOutputStream → Datei-OutputStream

Bei Verkettungen dieser Art fängt man immer bei der eigentlichen Ressource an.

Aufgabe:

  • Lege eine Datei mit einem OutputStream an, der von Files.new*(...) kommt.

  • Nutze einen CheckedOutputStream, um mit einer Adler32-Checksum-Implementierung eine Prüfsumme berechnen zu können.

  • Setze um den CheckedOutputStream einen java.io.BufferedOutputStream zur Pufferung.

  • Um von den Bytes eines OutputStream in die Unicode-Welt zu kommen wollen wir eine OutputStreamWriter einsetzen. Konfiguriere ihn mit UTF-8.

  • Erzeuge anschließend einen java.io.PrintWriter und schreibe einige Zeilen Text hinein.

  • Am Ende soll die Prüfsumme ausgegeben werden.

Achte darauf die Ressourcen mit try-mit-Ressourcen zu schließen.

1.2.4. Häufigkeiten mit einem FilterReader mitzählen ⭐⭐

Aus der Häufigkeit der Buchstaben lässt sich oft die Sprache ableiten. Gesucht ist ein Java-Filter, der sich als Writer zwischen andere Writer hängen lässt und intern die Anzahl unterschiedlichen Zeichen mitzählt.

Aufgabe:

  • Nenne die Klasse CharacterFrequencyReader, leite sie von java.io.FilterReader ab.

  • Die Methode toString() soll eine Tabelle mit Buchstabenhäufigkeiten ausgeben.

Beispiel: Gegeben ist die Zeichenfolge in einer Ressource.

3.14% of sailors are Pi Rates.

Wenn alle Zeichen durch den CharacterFrequencyReader strömen, und das Programm später toString() aufruft, sollte ein String mit folgendem Ergebnis auf dem Bildschirm erscheinen:

(0A) |   1 |  3,23
     |   5 |  16,13
   % |   1 |  3,23
   . |   2 |  6,45
   1 |   1 |  3,23
   3 |   1 |  3,23
   4 |   1 |  3,23
   P |   1 |  3,23
   R |   1 |  3,23
   a |   3 |  9,68
   e |   2 |  6,45
   f |   1 |  3,23
   i |   2 |  6,45
   l |   1 |  3,23
   o |   2 |  6,45
   r |   2 |  6,45
   s |   3 |  9,68
   t |   1 |  3,23

Kann dadurch auf die Sprache des Textes geschlossen werden?

1.3. Serialisierung

Java ermöglicht mit der Serialisierung, Objektzustände in einen Datenstrom zu schreiben und dann später das Objekt aus einem Datenstrom wiederzubelegen; diesen Vorgang nennt man Deserialisierung.

Zur Umwandlung von Java-Objekten in einen Binärstrom und umgekehrt werden die Klassen ObjectOutputStream und ObjectInputStream verwendet; alle zu serialisierenden Objekttypen müssen Serializable sein. Wir wollen die Typen in den nächsten Aufgaben einsetzen und praktische Beispiele für die Serialisierung kennenlernen.

Beide Klassen sind typische Dekoratoren: Beim Serialisieren ermittelt der ObjectOutputStream die Daten und schreibt die seriell anfallenden Bytefolgen in den OutputStream, der im Konstruktor übergeben wurde — beim Einlesen ist es umgekehrt, hier liest der ObjectInputStream aus einem übergeben InputStream.

1.3.1. Daten für den Chat (de)serialisieren und in Text umwandeln ⭐⭐

Ein Chat-Programm soll zur Übermittlung von Java-Objekten genutzt werden. Das Chat-Programm kann allerdings nur ASCII-Zeichen übermitteln. Daher müssen die Objekte nicht nur (de)serialisiert werden, sondern auch in das bzw. aus dem Textformat konvertiert werden.

Aufgabe:

  • Schreibe eine Methode String objectToBase64(Object), die ein Objekt serialisiert, anschließend mit einem DeflaterOutputStream komprimiert und dann Base64 kodiert zurückliefert.

  • Schreibe eine Methode deserializeObjectFromBase64(String), die einen Base64-kodierten String in einen Bytestrom bringt, diesen mit dem InflaterInputStream entpackt und als Quelle für die Deserialisierung nutzt.

Zur Konvertierung von Binärdaten in einen String und umgekehrt helfen der Base64.Encoder und Base64.Decoder und insbesondere die wrap(…​)-Methode.

1.3.2. Quiz: Voraussetzung für das Serialisieren ⭐

Wenn wir von der folgenden Klasse Inputs ein Objekt bilden, können wir es dann über den ObjectOutputStream serialisieren? Oder welche Voraussetzungen sind vielleicht nicht gegeben?

class Inputs {
  public static class Input {
    String input;
  }
  public List<Input> inputs = new ArrayList<>();
}

1.3.3. Letzte Eingaben sichern ⭐⭐

Bonny Brain benutzt regelmäßig die STRING2UPPERCASE-Anwendung, die im Kern so aussieht:

for ( String line; (line = new Scanner( System.in ).nextLine()) != null; )
  System.out.println( line.toUpperCase() );

Doch jetzt soll jede Benutzereingabe im Dateisystem gespeichert werden, sodass die Anwendung beim Start die gemachten Eingaben anzeigt.

Aufgabe:

  • Setze den folgenden Container für alle Eingaben in das Projekt:

    class Inputs implements Serializable {
      public static class Input implements Serializable {
        String input;
      }
      public List<Input> inputs = new ArrayList<>();
    }
  • Immer dann, wenn eine Benutzereingabe gemacht wurde, soll sie in ein Inputs-Objekt aufgenommen werden.

  • Nach jeder Eingabe soll Inputs in eine Datei serialisiert werden.

  • Wenn die Anwendung neu startet, sollen alle serialisierten Werte am Anfang auf dem Bildschirm angezeigt werden. Ausnahmen durch nichtexistierende Dateien oder falsche Dateiformate können geloggt, sollen aber ignoriert werden.

  • Ändere in Input den Datentyp String der Objektvariablen input in den Datentyp CharSequence. Starte das Programm neu. Was passiert bei der Deserialisierung von Inputs? Gibt es Probleme?

  • Setze in Inputs und in Input die Zeile

    @Serial private static final long serialVersionUID = 1;
  • Starte das Programm erneut, und serialisiere neue Daten.

  • Ergänze in Input die Zeile

    LocalDateTime localDateTime = LocalDateTime.now();

    für eine zusätzliche Objektvariable. Starte das Programm neu: was passiert bzw. passiert nicht?


1. Der String ist eine Vereinfachung von https://www.pouet.net/topic.php?which=8056&page=1.