Sonntag, 25. August 2019

Properties-Dateien lesen und schreiben

Was sind properties-Dateien?

Properties-Dateien in Java sind eine sehr nützliche Angelegenheit. Es sind einfache Textdateien, die einem bestimmten Aufbau folgen. Und zwar bestehen sie aus Schlüssel-Wert-Paaren. Es gibt also Strings, die sogenannten Schlüssel, denen andere Strings, die Werte, zugeordnet sind. So könnte eine properties-Datei aussehen:

language=de
resolution=850x1024
startfile=/usr/local/anyFile


Das Beispiel zeigt auch, wofür properties-Dateien gut eingesetzt werden können: zum Speichern von Konfigurationsdaten. Sie werden aber auch bspw. beim Internationalisierungsmechanismus von Java eingesetzt. Wenn ich wissen möchte, mit welcher Auflösung das Programm angezeigt werden soll, lese ich den Wert des Schlüssels resolution. Wenn der Benutzer die Auflösung verändert, ändert mein Programm den Wert des Schlüssels resolution.

Mehr Zauberei sind properties-Dateien nicht. Da stellt sich jetzt eine wichtige Frage: Wie lese und schreibe ich die Dinger? Natürlich können Sie die Mechanismen verwenden, mit denen Sie jede Textdatei lesen und schreiben. Aber Java bietet gerade für properties-Dateien eine Abkürzung.

Lesen von properties-Dateien


Der folgende Code zeigt wie eine properties-Datei gelesen wird.

    public static String findValueOfKey(String key) {
        Properties properties = new Properties();
        FileInputStream in = null;
        try {
            in = new FileInputStream("/usr/local/propertiesFile.properties");
            properties.load(in);
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            try {
                in.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }


        String value = properties.getProperty(key);
        return value;
    }


Das wichtigste Konzept ist die Klasse Properties. Diese stellt die Schnittstelle zu den properties-Dateien dar. Wir lesen die Datei in einen FileInputStream, laden diesen in das Properties-Objekt und lassen uns dann den Wert zu dem übergebenen Schlüssel geben. Das war es auch schon. Wie immer bei Dateiverarbeitung mit Java ist leider einiges an Exception-Handling erforderlich.

Schreiben von properties-Dateien

Wenn wir Werte in die Schlüssel der properties-Datei schreiben möchten, ist das auch nicht mehr Aufwand. Wir programmieren eigentlich nur den umgekehrten Weg: von dem Properties-Objekt in die Datei.

public static void writeValueOfKey(String key, String value) {
    Properties properties = new Properties();
    properties.setProperty(key, value);  
    FileOutputStream out;
    try {
        out = new FileOutputStream("/usr/local/propertiesFile.properties");
        properties.store(out, null);
    } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}


Der Code sieht wieder sehr ähnlich aus zum Lesen. Wir arbeiten wie gehabt mit dem Properties-Objekt und setzen den Wert des Schlüssels. Dann speichern wir die ganze Geschichte mit store() in der Datei.

Sie sehen, dass die Bearbeitung von properties-Dateien ziemlich einfach ist. Und deswegen sind properties-Dateien ein so nützliches Werkzeug, um Informationen schnell auf die Festplatte zu kriegen, vorausgesetzt natürlich, man kann sie in Schlüssel-Wert-Paare pressen.

Freitag, 23. August 2019

Textdateien Zeile für Zeile lesen

Einfache Textdateien mögen auf den ersten Blick etwas mittealterlich wirken, aber sie werden dennoch häufig bei der Programmentwicklung verwendet. Es ist eben leicht, einfachen Text in diesen Dateien unterzukriegen und deswegen sollte man sie nicht unterschätzen. Eine besondere Form, die properties-Dateien, stelle ich in einem anderen Beitrag vor.

Die Frage ist jetzt wie wir in eine Textdatei schreiben und wieder aus ihr lesen. In diesem Beitrag beschäftigen wir uns mit dem Lesen.

Die ganze Datei in den Arbeitsspeicher laden

Für kleine Dateien ist dies eine ganz gute Methode: Wir klatschen einfach den Inhalt der kompletten Textdatei in den Arbeitsspeicher. Das sieht dann so aus wie im folgenden Beispiel zu sehen:

public static void printTextFileOnScreen(String fileName) {
    Path path = Paths.get(fileName);
    List<String> lines = Files.readAllLines(path);
    for (String line : lines) {
       System.out.println(line);
    }

}

Dieser Code lädt die gesamte Textdatei in den Arbeitsspeicher und die Zeilen in ein String-Array. Dann wird das Array durchlaufen und jede Zeile der Textdatei wird ausgegeben. Der Code ist einfach und knackig. Der Nachteil liegt auch auf der Hand und wurde schon erwähnt: Für größere Dateien ist diese Vorgehensweise absolut ungeeignet, da wir die komplette Datei in den Arbeitsspeicher laden.

BufferedInputReader


Obwohl die vorherige Methode für kleine Dateien durchaus anwendbar ist, möchte ich dennoch empfehlen, den im folgenden vorgestellten BufferedInputReader immer zu verwenden. Denn wie genau definiert sich eine Datei als "klein"? Aber sehen wir uns erst einmal ein bisschen Code an.

public static void printFileOnScreen(String filename) {
    BufferedReader bufferedReader = null;
   
    try {
        bufferedReader = new BufferedReader(new FileReader(filename));
        String line = null;
       
        while ((line = bufferedReader.readLine()) != null) {
            System.out.println(line);
        }
    } catch (FileNotFoundException ex) {
        ex.printStackTrace();
    } catch (IOException ex) {
        ex.printStackTrace();
    } finally {
        if (bufferedReader != null) {
            try {
                bufferedReader.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}


Als erstes fällt auf, dass wir erheblich mehr schreiben müssen. In einem anderen Beitrag werde ich noch eine Möglichkeit vorstellen, wie man diese Menge an Boilerplate-Code etwas reduzieren kann. Was geschieht hier? Wir erzeugen als erstes ein FileReader-Objekt, mit dem wir auf die Datei zugreifen. FileReader stellt nur die absoluten Basisfunktionen zur Verfügung und aus diesem Grund stecken wir ihn in einen BufferedReader, dessen Interface umfangreicher ist. Dann laufen wir in einer Schleife durch die Datei. In jedem Schleifendurchlauf holen wir uns mit readLine() die aktuelle Zeile.

Im Gegensatz zu der vorherigen Lösung haben wir also immer nur die aktuelle Zeile im Arbeitsspeicher. Die Schleife läuft, solange wir noch Zeilen lesen. Wenn wir die letzte Zeile erreicht haben, gibt readLine() null zurück und die Schleife wird beendet. Vergessen Sie nicht, das BufferedReader-Objekt am Ende wieder brav zu schließen. 

Die geheimnisvolle SerialVersionUUID

Sie kennen das bestimmt: Sie programmieren mit einer IDE wie z.B. Eclipse und schreiben eine Klasse, die das Interface Serializable implementiert. Und auf einmal erhalten Sie von der IDE eine Warnung, die gerne eine SerialVersionUUID hätte. Was hat es denn damit auf sich?

Beim Serialisieren und Deserialisieren kann es zu folgendem Problem kommen: Sie serialisieren ein Objekt z.B. in eine Datei. Dann verändern Sie das Programm und möchten das Objekt wieder deserialisieren. Aber der aktuelle Stand des deserialisierten Objekts passt nicht mehr zu dem neuen Stand der Klasse in dem Programm, weil sie bspw. ein Attribut entfernt haben. Das Problem ist, dass beliebig viel Zeit zwischen dem Serialisieren und dem Deserialisieren liegen kann.

Die SerialVersionUUID ist ein Sicherheitsmechanismus, mit dem solche Probleme vermieden werden sollen. Beim Schreiben, also beim Serialisieren, wird die UUID mit gespeichert. Und beim Lesen, also beim Deserialisieren, überprüft Java, ob die gelesene UUID zu der aktuellen passt. Falls nicht, wird die Deserialisierung verweigert.

Standardmäßig berechnet sich diese UUID als Hashwert aus dem Inhalt der Klasse. Also wenn Sie das Interface Serializable implementieren und dann nichts weiter tun, wird Java einen Hashwert verwenden. Nun ist ein Hashwert nicht ohne Probleme. Manchmal gibt es Änderungen, die sich im Hashwert und somit in der UUID widerspiegeln, aber auf die Serialisierung keine Auswirkung haben. Dennoch würde gelesene Wert nicht mehr zum berechneten passen und Java würde die Deserialisierung verweigern.

Ein weiteres Problem ist, dass die Berechnung der UUID abhängig ist vom Compiler. Und somit könnte sie sich von Umgebung zu Umgebung unterscheiden, obwohl es eine Äderungen an der betroffenen Klasse gegeben hat. 

Und an dieser Stelle kommt die Warnung der IDE ins Spiel: Sie macht darauf aufmerksam, dass man am besten eine eigene SerialVersionUUID definiert. Dazu genügt es, einen privaten long-Wert zu definieren mit einem beliebigen Versions-Wert. Die meisten IDEs bieten an, einen Wert zu generieren. Diese Möglichkeit sollte man dann auch nutzen.

Also zusammenfassend lässt sich sagen: Sie müssen keine eigene SerialVersionUUID definieren, aber Sie sind sicherer und frustfreier unterwegs, wenn Sie es tun. Nutzen Sie dazu einfach die Möglichkeiten, die Ihnen die IDE anbietet und Sie haben damit keinerlei Arbeit mehr.

Serialisieren und Deserialisieren

Ein wichtiges Konzept in Java ist die Serialisierung und Deserialisierung. Dabei werden Objekte so umgewandelt, dass sie in eine Datei auf die Festplatte geschrieben werden können. Ein Objekt speichern ist ja im ersten Moment gar nicht so trivial wie es klingt: Die Daten des Objekts müssen irgendwie in eine Folge aus Bits umgewandelt werden. Und das ist eigentlich die korrektere Aufgabe der Serialisierung: Umwandlung von Objekten in Bits.

Das braucht man natürlich, um Objekte in Dateien zu speichern, aber die Serialisierung wird auch beim Senden von Objekten über ein Netzwerk benötigt (auch als Bit-Strom) oder in Datenbanken.

Das Schöne an Java ist: Zwar ist die Serialisierung als solche nicht trivial, aber die Benutzung des Serialisierungsmechanismus in Java ist sehr einfach. Es genügt, wenn eine Klasse das Interface java.io.Serializable implementiert wie im nächsten Beispiel zu sehen.

public class MySerializableClass implements Serializable {
    private int aValue;
    private Person person;
}



Serializable ist ein reines Marker-Interface, d.h. es markiert einfach eine Klasse für eine bestimmte Aufgabe. Wir müssen also keine Methoden implementieren. Java kümmert sich um den Rest. Ihre Klassen müssen auch keine weiteren Voraussetzungen erfüllen. Eine leichtere Benutzung ist kaum denkbar.

Im Beispiel oben fällt Ihnen auf, dass die Klasse eine Referenz auf ein Objekt enthält. Aber auch das ist kein Problem. Solange diese Klasse das Interface Serializable implementiert, kann Java damit umgehen.

Transiente Attribute

Manchmal gibt es Attribute, die nicht mit serialisiert werden sollen. Das kann verschiedee Gründe haben. Wenn wir über Netzwerk Daten übertragen, möchten wir z.B. Bandbreite sparen.  Oder wenn wir das Objekt in einer Datei speichern möchten, möchten wir vielleicht bestimmte Informationen nicht speichern. Aus welchen Gründen auch immer: Es gibt eine einfache Möglichkeit, einzelne Attribute von der Serialisierung auszuschließen. Das nächste Beispiel zeigt den Code.

public class MySerializableClass implements Serializable {
    private int aValue;
    private transient Person person;
}


Das Attribut person ist mit dem Schlüsselwort trasient versehen worden. Das heißt, dass es von der Serialisierung ausgeschlossen wird.

Überprüfungen auf null mit requiresNonNull

Ein bekanntes Phänomen in Java-Programmen sind die unseligen Überprüfungen auf null:

if (myObject == null) {
    throw new MyException();

}

Diese Überprüfungen sind nicht nur unpraktisch. Sie blähen den Code auch sehr auf. So hat man dann schnell mal mehrere if-Anweisungen hinteinander oder stark ineinander verschachtelte ifs. Beides trägt nicht unbedingt zur Lesbarkeit des Codes bei. Natürlich ist es auch keine Alternative, diese Überprüfungen auf null-Werte wegzulassen.

Nicht viele Programmierer wissen, dass Java noch einen weiteren Weg bietet mithilfe der Klasse Objects und deren statischer Methode requireNonNull(). Diese Methode existiert in drei Varianten, die wir uns jetzt alle einmal kurz ansehen möchten.

Die Grundform ist in dem folgenden Listing zu sehen:

String myString = null;       
Objects.requireNonNull(myString);


In diesem Fall wird eine NullPointerException geworfen. Es gibt keinen Unterschied zu einem Methoden-Zugriff auf ein null-Objekt.

Eine Alternative ist im folgenden Beispiel zu sehen:

String myString = null;       
Objects.requireNonNull(myString, "String ist null");


Wir können der Methode requireNonNull() einen String als zweiten Parameter mitgeben. Dieser enthält eine Fehlermeldung, die der NullPointerException mitgegeben wird.

Die dritte Möglichkeit sehen Sie im folgenden Code-Ausschnitt:

String myString = null;
Objects.requireNonNull(myString, () -> "Sie haben null mitgegeben");


Statt einem String kann der Methode auch ein Supplier-Objekt mittels eines Lambda-Ausdrucks mitgegeben werden. Dieser Supplier gibt die Fehlermeldung zurück, die zusammen mit der Exception ausgegeben werden soll. Diese letzte Variante kann etwas schneller sein als die vorherige, da die Fehlermeldung nur erzeugt wird, wenn sie tatsächlich benötigt wird. Wobei der Geschwindigkeitsunterschied in den meisten Fällen wohl kaum bemerkbar sein dürfte.

Letztendlich ermöglichen es diese drei Methoden, die drei Zeilen umfassende if-Anweisung von oben auf eine Zeile zu reduzieren. Etwas unschön ist, dass sie NullPointerExceptions wirft und man deswegen unter Umständen auch NullPointerExceptions abfangen muss, was ja nicht unbedingt als schöner Stil gilt. Es bleibt Ihnen überlassen wie Sie auf null prüfen, aber Sie sollten die hier vorgestellte Methode auf jeden Fall kennen, weil sie eingesetzt wird, z.B. finden Sie sie häufig im Code des JDK.

Veränderliche und unveränderliche Objekte

Die meisten Objekte in Java sind veränderlich (eng. mutable). Damit ist gemeint, dass der Status der Objekte, also die Werte ihrer Attribute, verändert werden kann. Veränderliche Objekte haben den Vorteil, dass wir beliebig mit ihnen verfahren und arbeiten können, weil sie veändert werden können. Sie haben allerdings den Nachteil, dass wir beliebig mit ihnen verfahren und arbeiten können, weil sie veändert werden können. Sie sehen hier, dass ein Vorteil auch zu einem Nachteil werden kann. Warum ist das so?

Manchmal möchte man keine Objekte haben, deren Status sich verändern kann. Die Werte der Attribute sollen immer konstant bleiben. Ein erster naiver Ansatz, um dies zu erreichen, könnte der folgende sein:

final Person meinePerson = new Person("Ali", "Baba");

Mit final können Konstanten definiert werden. Also liegt irgendwo der Gedanke nahe, dass nun auch das Objekt konstant ist und damit auch seine Attribute. Dieser Eindruck täuscht. Was hier konstant ist, ist die Referenz. Der folgende Code wird also vom Compiler nicht akzeptiert werden:

final Person meinePerson = new Person("Ali", "Baba");
final Person meineAnderePerson = new Person("Elle", "Fant");
meinePerson = meineAnderePerson;


Wir verändern in der dritten Zeile die Referenz und genau das wird durch das Schlüsselwort final verhindert. Was aber weiterhin funktionieren wird und was wir eigentlich verhindern wollten, zeigt der folgende Code-Ausschnitt:

final Person meinePerson = new Person("Ali", "Baba");
meinePerson.setVorname("Elle");
meinePerson.setNachname("Fant");


Also dieses schöne Schlüsselwort final führt uns nicht in die richtige Richtung. Schade. Um ein Objekt wirklich unveränderlich hinzubekommen, müssen wir uns etwas mehr Arbeit gönnen. Ein Objekt ist unveränderlich, wenn die Werte seiner Attribute nicht verändert werden können. Die Klasse muss also die folgenden Regeln erfüllen:

  1. Die Klasse darf keine Methoden anbieten, die schreibende Zugriffe auf die Attribute ermöglichen wie z.B. Setter.
  2. Alle Attribute sind privat und final.
  3. Wenn die Klasse Referenzen auf andere Objekte enthält, müssen diese Objekte natürlich auch unveränderlich sein.
  4. Wenn die Klasse eine Collection enthält, darf es keine Methoden geben, die das Verändern der Elemente dieser Collection ermöglichen, also keine Methoden zum Löschen, Hinzufügen usw.
  5. Die Klasse selbst ist als final deklariert. Damit können keine Unterklassen gebildet werden, die eventuell unerwünschtes Verhalten in die Objekte einschleusen.

Beispiele für unveränderliche Objekte im JDK sind die Wrapperklassen wie Integer. Sie bieten keinerlei Möglichkeit an, ihre Werte noch einmal zu verändern, nachdem die Objekte einmal erzeugt wurden.

Welche Voteile haben diese unveränderlichen Objekte jetzt?

  1. Ihre Werte können nicht aus Versehen verändert werden. Da in Java Objekte nur als Referenzen durch das Programm geschoben werden, ist manchmal nicht leicht zu sehen, dass man tatsächlich ein Objekt verändert. Dazu gleich noch mehr.
  2. Sie sind leichter zu entwickeln, da Klassen-Invarianten nur einmal überprüft werden müssen. Wenn man im Konstruktor prüft, ob die übergebenen Attributwerte korrekt sind, muss man keine weitere Überprüfungen mehr einbauen.
  3. Der interne Status bleibt konsistent, auch nachdem eine Exception geworfen wurde. Dies ist nicht bei allen veränderlichen Objekten der Fall.
  4. Unveränderliche Objekte sind automatisch thread-sicher, so dass man sich um Synchronisierung keine Gedanken machen muss.

Was meine ich nun damit, dass Programmierfehler vermieden werden können? Sehen Sie sich mal das nächste Code-Beispiel an.

public Person getMyPerson() {
    // Hier wird eine Person erzeugt und irgendwas tolles wird mit der Person getan.
    return person;
}

public void tueEtwasMitPerson(Person person) {
    person.setVorname("Elle");
    person.setNachame("Fant");

}

In Java wird oft übersehen, dass Objekte immer Referenzen sind. Die Methode getMyPerson() gibt also eine Referenz auf das Originalobjekt zurück. Die Methode tueEtwasMitPerson() erhält eine Referenz auf ein Objekt und wir verändern das Originalobjekt. Das kann erwünscht sein; das kann aber auch unerwünscht sein. Und genau an dieser Stelle greifen die unveränderlichen Objekte. Wenn Sie sicherstellen möchten, dass es keine unerwünschten Veränderungen geben kann, verwenden Sie unveränderliche Objekte.

Nun haben die unveränderlichen Objekte in Java einen immensen Nachteil: Klassen müssen von vornherein so konzipiert werden, dass ihre Objekte unveränderlich sind. Es ist nicht möglich, ein Objekt a der Klasse A veränderlich zu definieren und ein Objekt b der Klasse A veränderlich. Sie müssen die Klasse mit den oben beschriebenen Maßnahmen unveränderlich konzipieren. Daher kann es in manchen Fällen sinnvoll sein, zwei Klassen in seinem Programm zu haben wie z.B. Person und PersonImmutable.

Dieser Nachteil löst sich aber auch wieder in einen Vorteil auf: Sie können in Java natürlich auch nur teil-veränderliche Klassen entwickeln, was man sowieso tut, wenn man Attribute anständig kapselt.

Mit diesem Beitrag wollte ich nicht sagen, dass Objekte unveränderlich sein sollten. In vielen Fällen möchte man Attribute ja verändern können. Dennoch sollten Sie die Tatsache, was veränderlich bedeutet, unbedingt im Kopf behalten. Da sind schon besseren Programmieren mit Java schwere Fehler unterlaufen. Die Problematik ist wichtig und manchmal kann es wirklich ganz sinnvoll sein, vollkommen unveränderliche Objekte zu definieren.

Nicht-leere Verzeichnisse löschen

In Java ist es prinizipiell sehr einfach, ein Verzeichnis zu entfernen. Dazu bedient man sich einfach der Methode delete() der Klasse File. Das Problem an dieser Methode ist: Das Verzeichnis muss unbedingt leer sein! Bei einem nicht-leeren Verzeichnis endet diese Methode mit einer Exception. Aber was kann ich tun, um nicht-leere Verzeichnisse zu entfernen? Genau das möchte ich Ihnen in diesem Beitrag erklären.

Rekursives Löschen

Sie können ein Verzeichnis rekursiv löschen. Rekursion bedeutet, dass eine Methode sich selbst immer wieder aufruft. Das nächste Beispiel zeigt eine Methode, die rekursiv ein Verzeichnis und alle enthaltenen Dateien entfernt.

public static void deleteDirectoryRecursion(File file) throws IOException {
  if (file.isDirectory()) {
    File[] entries = file.listFiles();
    if (entries != null) {
      for (File entry : entries) {
        deleteDirectoryRecursion(entry);
      }
    }
  }
  if (!file.delete()) {
    throw new IOException("Failed to delete " + file);
  }
}

Sie sehen in Zeile 6, dass die Methode sich selbst noch einmal aufruft. Was soll das? Stellen Sie sich mal die folgende Ordnerstruktur vor:

- folder1
    -folder2
        -file1
        -file2
    -file3
   
Und jetzt spielen wir den Löschvorgang einmal in Gedanken durch. Wir starten mit folder1 als Übergabeparameter. Wir sehen in dem ersten if, dass es sich um ein Verzeichnis handelt und schnappen uns alle enthaltenen Dateien. Dann laufen wir durch alle enthaltenen Dateien und rufen die Methode deleteDirectoryRecursion() noch einmal mit jeder enthaltenen Datei auf. Die erste Datei ist folder2. Wir machen das gleiche Spiel noch einmal: Wir gehen mit folder2 in diese Methode, erkenen, dass es sich um ein Verzeichnis handelt und rufen für jede Datei einmal die Methode deleteDirectoryRecursion() auf. Wir gehen also mit file1 als Übergabewert in die Methode. file1 ist kein Verzeichnis und wir löschen die Datei. Genauso verfahren wir mit file2. Wir sind am Ende der Aufrufkette angelangt und laufen wieder zurück bis zu dem Methodenaufruf, der fodler2 als Übergabe hatten. Jetzt ist folder2 leer und wir können ihn ganz einfach mit delete() löschen.

Wenn Sie die Methode mal in Gedanken durchspielen, haben Sie das Prinzip schnell verstanden. Bis Java 6 einschließlich gab es keine andere Möglichkeit.

Rekursion mit NIO

Mit Java 7 wurde NIO eingeführt, eine verbesserte API für Dateizugriffe. Mit NIO sieht die ganze Geschichte aus wie folgt:

public static void deleteDirectoryRecursionNio(Path path) throws IOException {
  if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
    try (DirectoryStream<Path> entries = Files.newDirectoryStream(path)) {
      for (Path entry : entries) {
        deleteDirectoryRecursionNio(entry);
      }
    }
  }
  Files.delete(path);

}

Im Prinzip passiert das gleiche wie in dem anderen Rekursionsbeispiel.

Apache Commons IO

Es gibt noch einige weitere Möglichkeit, aber die möchte ich an dieser Stelle mal überspringen und sofort zu der einfachsten Möglichkeit kommen, die allerdings verlangt, dass Sie eine Dritt-API einbinden - nämlich Apache Commons IO. Sie können diese API als jar-Datei herunterladen und in Ihr Projekt einbinden oder bspw. mit Maven arbeiten. Um Apache Commons IO mit Maven einzubinden, fügen Sie einfach den folgenden Code in Ihre pom.xml ein:

<dependency>
   <groupId>commons-io</groupId>
   <artifactId>commons-io</artifactId>
   <version>2.6</version>
</dependency>


Egal wie Sie die Bibliothek in Ihr Programm einbinden, Sie können dann mit dem folgenden ganz einfachen Aufruf ein nicht-leeres Verzeichnis löschen:

FileUtils.delteDirectory(file);

Diese letzte Methode ist natürlich die einfachste Möglichkeit, denn Sie brauchen die unter Umständen fehleranfällige Rekursion nicht mehr selbst zu programmieren und - natürlich - zu testen. Daher empfehle ich diese Möglichkeit, wenn man Dritt-APIs einbinden kann. Die anderen Möglichkeiten sind jedoch ganz schön, um ein wenig mit Rekursion herumzuspielen und sie zu verstehen.

Freitag, 9. August 2019

Das größte und kleinste Element in einer Liste finden - ohne Schleife

Es kommt gar nicht so selten vor, dass man in einer Liste das größte oder das kleinste Element finden möchte. Stellen wir uns mal diese Liste vor: 4, 3, 6, 99, -44, 5. Wenn wir nun 99 als das größte Element bestimmen wollten, müssten wir mit einer for-Schleife durch diese Liste laufen und einen kleinen Algorithmus bauen, um das Maximum zu bestimmen. Aber es geht auch einfacher, dank der Klasse Collections.

Diese Werkzeugklasse hat nämlich zwei tolle Methoden: min() und max(). Betrachten wir mal ein kleines Beispiel.

List<Integer> myNumbers = Arrays.asList(4, 3, 6, 99, -44, 5);
int min = Collections.min(myNumbers);
int max = Collections.max(myNumbers);
System.out.println("Kleinste Zahl: " + min);
System.out.println("Größte Zahl: " + max);



Sie sehen, dass ich keine Schleifen-Konstrukte mehr programmiert habe. Diese beiden Methoden sind zwei nützliche kleine Methoden der Werkzeugklasse Collections. Beide Methoden existieren jetzt aber noch einmal in einer überladenen Version. Betrachten wir auch dafür mal ein Beispiel:

List<String> cities = Arrays.asList("Augsburg", "Saarbrücken", "Koblenz", "berlin");
String minCity = Collections.min(cities);
String maxCity = Collections.max(cities);
System.out.println("Min Stadt: " + minCity);
System.out.println("Max Stadt: " + maxCity);



Wenn wir das Beispiel ausführen, erhalten wir ein interessantes und vielleicht auch unerwartetes Ergebnis, denn berlin wird als Maximum der Städte ausgegeben uns nicht Saarbrücken, was wir bei einer lexikographischen Ordnung hätten erwarten können, denn "S" kommt nun einmal nach "B". Hier ist es wichtig, dass wir Strings sortieren. Und bei Strings werden zuerst die Großbuchstaben miteinander verglichen, dann die Kleinbuchstaben. Würde in der Liste Berlin statt berlin stehen, wäre tatsächlich Saarbrücken das Ergebnis gewesen.

Aber was, wenn wir nun das "richtige" Minimum oder Maximum haben möchten, also unabhängig von der Groß- und Kleinschreibung? Oder wenn wir von noch komplexeren Objekten das Minimum oder Maximum bestimmen möchten? Und genau an dieser Stelle kommen die überladenen Methoden ins Spiel.

Denn diese erwarten als zweiten Parameter einen Comparator. Das nächste Beispiel zeigt nun die Minimum- und Maximum-Suche ohne Beachtung der Groß- und Kleinschreibung.

List<String> cities = Arrays.asList("augsburg", "Saarbrücken", "Koblenz", "berlin");
String minCity = Collections.min(cities, (b,a) ->
    b.toLowerCase().compareTo(a.toLowerCase()));
String maxCity = Collections.max(cities, (a,b) ->
    a.toLowerCase().compareTo(b.toLowerCase()));
System.out.println("Min Stadt: " + minCity);
System.out.println("Max Stadt: " + maxCity);


In diesem Beispiel habe ich die Comparatoren mit Lambda-Ausdrücken umgesetzt. Sie sehen, dass Sie auf diese Art beliebig komplexe Vergleichsausdrücke mit beliebig komplexen Objekten ausführen können.

JavaFX: NoSuchMethodException init

Manchmal wird man bei JavaFX mit der folgenden Fehlermeldung gequält:

java.lang.NoSuchMethodException: xx.yy.zz.MyController<init>()

Da kommt die Frage auf: Welche init-Methode? Bevor Sie sie jetzt damit beschäftigen, irgendwelche Methoden namens init() zu schreiben, sollten Sie es mal mit der einfachen Lösung versuchen.

JavaFX benötigt einen Standardkonstruktor. Wenn Sie diese Fehlermeldung erhalten, haben Sie den Standardkonstruktor in Ihrem Controller wahrscheinlich überschrieben. Sobald Sie ihn hinzugefügt haben, sollte die unselige Fehlermeldung verschwunden sein.

Generell sollten Sie bei JavaFX den Controllern keinen Konstruktor geben, denn das Framework ruft ihn sowieso nicht auf und im Konstruktor sind notwendige Referenzen noch nicht definiert.

Verwenden Sie immer eine Methode initialize(), die Sie mit @FXML markieren:

@FXML
public void initialize() {
    System.out.println("Ich bin bei JavaFX besser  als der Konstruktor!");
}

Objkete null-sicher vergleichen

Wenn wir Objekte auf Gleichheit vergleichen möchten, benötigen wir natürlich die equals()-Methode. Die implementieren wir in der jeweiligen Klasse und programmieren sie so, dass sie zwei Objekte miteinander vergleichen kann. Schnappen wir uns mal eine einfache Personen-Klasse:

public class Person {
    private String firstName;
    private String lastName;
 }



Jetzt führen wir einmal die folgenden Vergleiche durch. Sie werden alle wunderbar funktionieren.

Person person1 = new Person("Phil", "Harmonie");
Person person2 = new Person("Elle", "Fant");
Person person3 = new Person("Phil", "Harmonie");

System.out.println("Person1 equals Person2: " + person1.equals(person2));
System.out.println("Person2 equals Person3: " + person2.equals(person3));
System.out.println("Person3 equals Person2: " + person3.equals(person3));
System.out.println("Person1 equals null: " + person1.equals(null));



Sie werden einwandfrei funktionieren, unter der Voraussetzung, dass wir daran gedacht haben, in der equals()-Methode einen null-Wert abzufragen. Haben wir das vergessen, wird uns der letzte Vergleich mit einer NullPointerException um die Ohren fliegen.

Daher gibt es die Hilfsmethode Objects.equals(). Sie erhält zwei Übergabeparameter und tut nichts weiter als deren equals()-Methode aufzurufen. Sie verspricht dabei null-sicher zu sein, d.h. sie kann mit null-Werten umgehen. Wir brauchen also die Überprüfung auf null in die equals()-Methode nicht mehr einzubauen:

System.out.println("Person1 equals Person2: " + Objects.equals(person1, person2));
System.out.println("Person2 equals Person3: " + Objects.equals(person2, person3));
System.out.println("Person3 equals Person1: " + Objects.equals(person3, person1));
System.out.println("null equals Person1: " + Objects.equals(null, person1));
System.out.println("null equals Person1: " + Objects.equals(person1, null));
System.out.println("Person1 equals null: " + Objects.equals(null, null)); 



Wenn einer der beiden Parameter null ist, gibt Objects.equals() false aus. Sind beide Parameter gleich, wird true ausgegeben. Wenn beide Parameter null sind, also der letzte Fall im Beispiel, sind beide Parameter gleich und somit ist die Rückgabe konsequenterweise true.

Nun ist mir beim Experimentieren ein Problem aufgefallen. Sie müssen unbedingt in die equals()-Methode einen Test auf die richtige Klasse mit instanceof einbauen, denn sonst gibt es auf jeden Fall eine NullPointerException, wenn die zweite Übergabe null ist. Warum das so ist, zeige ich gleich. Erst einmal ein bisschen Code, der zeigt, was ich meine:

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Person)) {
            return false;
        }
        Person other = (Person) obj;
        if (firstName == null) {
            if (other.firstName != null)
                return false;
        } else if (!firstName.equals(other.firstName))
            return false;
        if (lastName == null) {
            if (other.lastName != null)
                return false;
        } else if (!lastName.equals(other.lastName))
            return false;
        return true;
    }



Den Test auf null habe ich weggelassen. Aber die erste if-Anweisung, die das instanceof enthält, muss unbedingt vorhanden sein. Das liegt an der - meiner Meinung nach nicht ganz glücklichen Implementierung der Methode Objects.equals(). In einer guten IDE können wir uns die Implementierung ansehen:

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}


Sie sehen, dass eigentlich nur der erste Parameter auf null überprüft wird. Und dann wird die equals()-Methode mit dem zweiten Parameter aufgerufen. Ist der zweite Parameter also null und wird dann in equals() eine Methode aufgerufen wie z.B. getClass(), kracht es auch hier mit einer NullPointerException. Es hat ein paar Minuten gedauert, bis ich das mit einer Standardimplementierung der equals()-Methode von Eclipse herausgefunden habe. An der Implementierung sehen Sie allerdings auch, dass Sie sich ebenfalls den Referenzvergleich mit == schenken können.

Also so ganz hält die Methode ihr Versprechen nicht. Aber ein instanceof müssen Sie in der equals()-Methode sowieso verwenden, bevor Sie beide Parameter von Object in den jeweiligen Objekttyp casten. Dann schreiben Sie instanceof einfach direkt an den Anfang und arbeiten nicht mit der getClass()-Methode.

Natürlich können Sie Ihre equals()-Methode nur derart kürzen, wenn Sie und Ihr Team konsequent Objects.equals() für Objektvergleiche einsetzen.

Das oben beschriebene Problem kann auch noch anders umgangen werden. Objects bietet auch noch die Methode deepEquals() an, mit der man auch bspw. Arrays miteinander vergleichen kann. Betrachten wir mal deren Implementierung:

    public static boolean deepEquals(Object a, Object b) {
        if (a == b)
            return true;
        else if (a == null || b == null)
            return false;
        else
            return Arrays.deepEquals0(a, b);
    }



Und hier ist zu sehen, dass tatsächlich beide Parameter auf null geprüft werden. Mit dieser Methode wäre man also entsprechend noch sicherer unterwegs bei den folgenden Vergleichen.

System.out.println("null equals Person1: " + Objects.deepEquals(null, person1));
System.out.println("null equals Person1: " + Objects.deepEquals(person1, null));
System.out.println("Person1 equals null: " + Objects.deepEquals(null, null));

Strings aneinanderhängen

Diese Situation dürfte jeder schon einmal erlebt haben: Wir haben eine Liste mit String-Objekten und möchten diese zu einem String verbinden. Dabei sollen die einzelnen Elemente durch Kommas, Bindestriche oder sonst irgendeinem Zeichen voneinander getrennt werden.

Was programmiert man? Man baut eine for-each-Schleife, die durch die Liste läuft und die Strings aneinander hängt. Dann muss noch eine if-Anweisung rein, damit das Trennzeichen nicht noch an das letzte Element in der Ausgabe angehängt wird. Und schon hat man einiges an Programmieraufwand.

Seit Java 8 ist das anders. Die Klasse String stellt die statische Methode join() zur Verfügung. Diese erhält als Übergabe ein Trennzeichen und einen Iterable (z.B. eine Liste) mit den Strings. Und um den Rest kümmert sich die Methode. Im Folgenden ist ein kleines Beispiel zu sehen:

List<String> workdays = Arrays.asList("Montag", "Dienstag",
            "Mittwoch", "Donnerstag", "Freitag");
String joinedWorkdays = String.join(" - ", workdays);
System.out.println(joinedWorkdays);

In diesem Fall lautet die Ausgabe also:

Montag - Dienstag - Mittwoch - Donnerstag - Freitag

Dienstag, 6. August 2019

Maven: Invalid target release

Ein Problem, das immer mal wieder bei Maven auftaucht, ist die folgende schöne Fehlermeldung: invalid target release:x.x

Vor kurzem hatte ich dieses Problem beim Übersetzen eines Maven-Projekts wieder: invalid target release: 1.11.

In meiner pom.xml hatte ich Java 11 eingetragen. Dabei ergab im Terminal ein java -version eindeutig, dass die Java-Version 12 genutzt wurde. Das sollte also für Java 11 kein Problem darstellen. Als ich dann mvn -version eingab, erhielt ich als Ausgabe, dass Maven mit Java8 arbeitet.

Also irgendjemand lügt mich an: entweder Maven oder Java.

Ein wenig Recherche hat dann ergeben, dass keiner von beiden lügt. Maven arbeitet nicht mit der Java-Version, die in der PATH-Variablen angegeben ist, sondern mit der Java-Version in JAVA_HOME. Wenn JAVA_HOME immer noch auf eine alte Version verweist, in meinem Fall also Java 8, kommt Maven nicht damit klar.

Die Lösung war also ziemlich einfach: Ich habe die Umgebungsvariable JAVA_HOME einfach auf die aktuelle Java-Installation gesetzt und schon lief das Übersetzen meines Programms ohne Probleme.