I am currently working on an English translation. If you like to help to proofread please contact me: ullenboom ät g m a i l dot c o m.

Java Videotraining Werbung

1. Dateien und wahlfreier Zugriff auf Dateiinhalte

Auch wenn vieles in die Cloud und Datenbank wandert: Das Dateisystem ist immer noch ein wichtiger Speicher und Ort zur Organisation von Dokumenten. Auch Bonny Brain und Captain CiaoCiao legen noch vieles lokal ab — es gibt genug, was nicht in die Öffentlichkeit geraten darf.

Voraussetzungen

  • File-Klasse, Path-Schnittstelle und Files-Klasse in den Grundzügen kennen

  • temporäre Dateien anlegen können

  • Metadateien von Dateien und Verzeichnissen erfragen können

  • Verzeichnisinhalte auflisten und filtern können

  • vollständige Dateien lesen und schreiben können

  • RandomAccessFile kennen

Verwendete Datentypen in diesem Kapitel:

1.1. Path und Files

Wie bei so vielen Sachen in Java gibt es auch bei der Dateiverarbeitung den »alten« und den »neuen« Weg. In vielen Beispielen sieht man noch immer Code mit den Typen java.io.File, FileInputStream, FileOutputStream, FileReader und FileWriter, doch diese Typen sind nicht mehr zeitgemäß, weshalb wir uns in diesem Kapitel ausschließlich mit Path und Files befassen wollen, denn diese Typen ermöglichen den Einsatz von virtuellen Dateisystemen, wie einem ZIP-Archiv. File ist nur noch dann zwingend, wenn es tatsächlich um Dateien oder Verzeichnisse des lokalen Dateisystems geht; Beispiele wären das Öffnen von Dateien mit dem vom Betriebssystem assoziierten Programmen oder das Umlenken von Datenströmen von extern gestarteten Programmen.

1.1.1. Spruch des Tages anzeigen ⭐

Hin und wieder kann sich Captain CiaoCiao nicht so richtig motivieren. Ein Motivations- oder Sinnspruch für den Tag bringt den Murrkopf auf neue Gedanken. Es soll eine Anwendung programmiert werden, die eine HTML-Datei mit einem Spruch erzeugt und dann den Browser öffnet, um diesen Text anzuzeigen. Die Aufgabe lässt sich mit zwei Methoden von java.nio.files.Files lösen.

Aufgabe:

  • Lege mit einer passenden Files-Methode eine temporäre Datei an, die mit dem Datei-Suffix .html endet.

  • Schreibe in die neue temporäre Datei HTML-Code, etwa den folgenden:

    <!DOCTYPE html><html><body>
    ›Die Dinge, die wir stehlen, sagen uns, wer wir sind.‹
    - Thomas von Tew
    </body></html>
  • Suche aus der Klasse java.awt.Desktop eine Methode, die den Standard-Browser öffnet und somit die HTML-Datei anzeigt.

1.1.2. Verstecke zusammenführen ⭐

Mit gewissen Files-Methoden lässt sich in einem Rutsch eine ganze Datei zeilenweise einlesen und wieder schreiben.

Captain CiaoCiao sammelt in einer großen Textdatei potenzielle Verstecke. Doch oft fallen ihm spontan weitere Verstecke ein, und er schreibt sie schnell in eine neue Datei. Nun nimmt er sich die Zeit und räumt auf und fasst alles zusammen; die kleinen Textdateien sollen mit der großen Datei zusammengelegt werden. Wichtig ist, dass die Reihenfolge der Einträge der großen Datei nicht geändert wird und nur die Einträge aus den kleinen Dateien aufgenommen werden, wenn sie nicht in der großen Datei vorkommen, denn es kann sein, dass in der Hauptdatei schon längst die Verstecke stehen.

Aufgabe:

  • Schreibe eine Methode mergeFiles(Path main, Path... temp), die die Master-Datei öffnet, alle temporären Inhalte ergänzt, und dann die Master-Datei zurückschreibt.

1.1.3. Kopien einer Datei erstellen ⭐⭐

Wenn man zum Beispiel im Windows-Explorer eine Datei in den gleichen Ordner kopiert, wird eine Kopie angelegt. Diese Kopie bekommt automatisch einen neuen Namen. Gesucht ist ein Java-Programm, das dieses Verhalten nachbildet.

Aufgabe:

  • Schreibe eine Java-Methode cloneFile(Path path), die Kopien von Dateien erzeugt und dabei die Dateinamen systematisch generiert. Nehmen wir an, <Name> symbolisiert den Dateinamen, dann wird die erste Kopie Copy of <Name> heißen und anschließend sollen die Dateinamen Copy (<Zahl>) of <Name> lauten.

  • Ruft man die Methoden auf Verzeichnissen auf oder gibt es andere Fehler, kann die Methode eine IOException auslösen.

Beispiel:

  • Nehmen wir an, eine Datei heißt Top Secret UFO Files.txt. Dann sollen die neuen Dateinamen so aussehen:

    • Copy of Top Secret UFO Files.txt

    • Copy (2) of von Top Secret UFO Files.txt

    • Copy (3) of von Top Secret UFO Files.txt

    • usw.

1.1.4. Verzeichnislisting generieren ⭐

Auf der Kommandozeile kann sich der Benutzer die Verzeichnisinhalte und Metadaten anzeigen lassen, genauso wie auch ein Dateiauswahldialog dem Benutzer Dateien anzeigt.

Aufgabe:

  • Schreibe mit Files und der Methode newDirectoryStream(…​) ein Programm, das den Verzeichnisinhalt für das aktuelle Verzeichnis auflistet.

  • Rufe unter DOS das Programm dir auf. Bilde die Ausgabe des Verzeichnislistings vollständig nach. Der Kopf und Fuß sind nicht nötig.

1.1.5. Nach einer großen GIF-Datei suchen ⭐

Auf der Festplatte von Bonny Brain herrscht Chaos, auch weil sie alle Bilder in genau einem Verzeichnis speichert. Nun sind die Bilder von der letzten Schatzsuche unauffindbar! Sie weiß nur noch, dass die Bilder im GIF-Format gespeichert waren und sie über 1024 Pixel breit waren.

Aufgabe:

  • Gegeben ist ein beliebiges Verzeichnis. Suche in diesem Verzeichnis (nicht rekursiv!) nach allen Bildern, die vom Typ GIF sind und eine Mindestbreite von 1024 Pixeln aufweisen.

Greife zum Auslesen der Breiten und zur GIF-Prüfung auf folgenden Code zurück:

private static final byte[] GIF87aGIF89a = "GIF87aGIF89a".getBytes();
private static boolean isGifAndWidthGreaterThan1024( Path entry ) {
  if ( ! Files.isRegularFile( entry ) || ! Files.isReadable( entry ) )
    return false;

  try ( RandomAccessFile raf = new RandomAccessFile( entry.toFile(), "r" ) ) {
    byte[] bytes = new byte[ 8 ];
    raf.read( bytes );

    if ( ! Arrays.equals( bytes, 0, 6, GIF87aGIF89a, 0, 6 ) &&
         ! Arrays.equals( bytes, 0, 6, GIF87aGIF89a, 6, 12 ) )
      return false;

    int width = bytes[ 6 ] + (bytes[ 7 ] << 8);
    return width > 1024;
  }
  catch ( IOException e ) {
    throw new UncheckedIOException( e );
  }
}

Die Methode liest die ersten Bytes ein und schaut nach, ob die ersten 6 Bytes entweder dem String GIF87a oder GIF89a entsprechen. Prinzipiell lässt sich dieser Test auch mit ! new String(bytes, 0, 6).matches("GIF87a|GIF89a") umsetzen, doch das würde einiges an temporären Objekten im Speicher nach sich ziehen.

Nach der Prüfung liest das Programm 2 Bytes für die Breite aus und konvertiert die Bytes in eine 16-Bit-Ganzzahl.

1.1.6. Verzeichnisse rekursiv absteigen und leere Textdateien finden ⭐

Auf der Festplatte von Bonny Brain ist immer noch ein großes Durcheinander. Aus unerklärlichen Gründen hat sie viele Textdateien mit 0 Byte.

Aufgabe:

  • Laufe mit einem FileVisitor rekursiv ab einem gewählten Startverzeichnis alle Unterverzeichnisse ab, und suche nach leeren Textdateien.

  • Textdateien sind Dateien, die die Dateiendung .txt tragen (unabhängig von der Groß-/Kleinschreibung).

  • Bei einem Fund zeige den absoluten Pfad der Datei auf der Konsole.

1.1.7. Eigene Utility-Bibliothek für Dateifilter entwickeln ⭐⭐⭐

Die Files-Klasse bietet drei statische Methoden zum Erfragen aller Einträge in einem Verzeichnis:

  • newDirectoryStream(Path dir)

  • newDirectoryStream(Path dir, String glob)

  • newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter)

Das Ergebnis ist immer ein DirectoryStream<Path>. Die erste Methode filtert die Ergebnisse nicht, die zweite Methode erlaubt einen Glob-String wie etwa *.txt, und die dritte Methode erlaubt einen beliebigen Filter.

java.nio.file.DirectoryStream.Filter<T> ist eine Schnittstelle, die von Filtern implementiert werden muss. Die Methode lautet boolean accept(T entry) und ist wie ein Prädikat.

Die Java-Bibliothek deklariert zwar die Schnittstelle aber keine Implementierung.

Aufgabe:

  • Schreibe diverse Implementierungen von DirectoryStream.Filter, die Dateien prüfen können auf

    • Attribute (wie lesbar, schreibbar)

    • die Länge

    • die Dateiendungen

    • den Dateinamen über reguläre Ausdrücke

    • magische Anfangskennungen

Im Idealfall erlaubt die API eine Verkettung aller Filter, etwa so:

DirectoryStream.Filter<Path> filter =
    regularFile.and( readable )
               .and( largerThan( 100_000 ) )
               .and( magicNumber( 0x89, 'P', 'N', 'G' ) )
               .and( globMatches( "*.png" ) )
               .and( regexContains( "[-]" ) );

try ( DirectoryStream<Path> entries =Files.newDirectoryStream( dir, filter ) ) {
  entries.forEach( System.out::println );
}

1.2. Wahlfreier Zugriff auf Dateiinhalte

Für Dateien lässt sich ein Ein-/Ausgabestrom besorgen und von vorne bis hinten auslesen oder schreiben. Eine andere API erlaubt den wahlfreien Zugriff, also einen Positionszeiger.

1.2.1. Datenbanken mit fixen Datensätze verwalten ⭐⭐

Wenn man sich anschaut wie Datenbankmanagementsysteme die Daten physikalisch ablegen, wird man immer wieder auf Dateien mit fester Satzlänge kommen, engl. fixed-length format. Dabei bestehen die Dateien aus Blöcken fester Länge, die ohne Trennung sequenziell hintereinander liegen. Die Einheiten nennt man Records.

Beispiel: Jedes Record ist 6 Symbole lang:

AAAAAABBBBBBCCCCCCDDDDDDAAAAAA

Bei so einem Format lässt sich leicht zu einem Datensatz springen — man multipliziert den gewünschten Datensatz mit der Länge, hier 6, und positioniert den Zeiger und liest 6 Zeichen aus.

Aufgabe:

  • Schreibe eine Klasse RecordDatabase, die Operationen anbietet zum Lesen, Anhängen und Aktualisieren von Records. Überlege, wie man Records löschen könnte.

  • Das Dateiformat ist wie folgt:

    • In den ersten vier Bytes steht eine Kennung, CCDB (CiaoCiao-Database)

    • Es folgen 6 Bytes mit einem Datumsstempel, wann die Datenbank zuletzt verändert wurde, das Format ist YYMMDD.

    • Es folgen 4 Byte (ein int) mit der Anzahl Records in der Datenbank.

    • Es folgen 4 Byte (ein int) mit der Breite jedes Records; die Breite ist in Byte.

    • Es folgen die Records.

1.2.2. Letzte Zeile einer Textdatei ausgeben ⭐⭐

Die Crew-Mitglieder schreiben alle Aktionen in ein elektronisches Logbuch, wobei die neuen Einträge hinten angehängt werden. Kein Eintrag ist länger als 100 Zeichen, die Texte sind in UTF-8 geschrieben.

Nun interessiert sich Captain CiaoCiao für den letzten Eintrag. Wie sieht ein Java-Programm aus, wenn aus einer Datei nur die letzte Zeile ausgelesen werden soll? Da schon sehr viele Einträge im Logbuch stehen, ist es nicht möglich, die Datei komplett einzulesen.

Aufgabe:

  • Schreibe ein Programm, das die letzte Zeile einer Textdatei liefert.

  • Finde eine Lösung, die nicht unnötig viel Speicher benötigt.

Überlege, ob sich ([^\r\n]*)$ sinnvoll einsetzen lässt.