Dienstag, 3. September 2019

Tests in JUnit deaktivieren

Tests können in JUnit temporär deaktiviert werden, d.h. sie werden einfach nicht ausgeführt. Natürlich sollte man nicht wahllos irgendwelche Tests ausschalten, sondern sich schon etwas dabei denken. Ein Grund könnte z.B. sein, dass man über fehlgeschlagene Tests nicht mehr den Überblick behalten kann und einfach einige ausblende möchte, an denen man im Moment sowieso nicht arbeiten möchte.

Wichtig ist, dass die Tests dann auch irgendwann wieder eingeschaltet werden. JUnit 4 und JUnit 5 bieten dazu jeweils unterschiedliche Annotationen an. Das erste Code-Beispiel gilt für JUnit 4.

@Test
@Ignore("Dieser Test löst eine Endlosschleife aus")
public void test() {
    // Testcode
}


Die Annotation @Ignore sorgt dafür, dass JUnit den Test nicht ausführt. Allerdings sollten Sie immer eine Erklärung dazu schreiben, damit man sieht, warum der Test deaktiviert wurde. Schließlich sollen auch die deaktivierten Tests nicht in Vergessenheit geraten. Aber dafür sorgt auch die Darstellung von JUnit selbst. Dazu gleich. Sehen wir uns erst mal noch die Annotation in JUnit 5 an:

@Test
@Disabled("Dieser Test löst eine Endlosschleife aus")
public void test() {
    // Testcode
}
Im Screenshot sehen Sie, dass der Test nicht vollkommen verschwindet, sondern von JUnit immer noch angezeigt wird. So kann er nicht total und ganz vergessen werden.




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.

Montag, 29. Juli 2019

LaTeX: Einrücken von Absätzen verhindern

Im Deutschen eher unüblich ist das Einrücken der ersten Zeile eines Absatzes. Im Englischen ist dies normal und da LaTeX nun einmal aus den USA stammt, rückt es automatisch die erste Zeile ein. Das können Sie jedoch mit einem ganz einfachen Kommando verhindern.

\setlength{\parindent}{0em}

Einfach dieses Kommando an den Anfang des Dokuments einfügen und die Einrückung ist Geschichte.

Samstag, 27. Juli 2019

Variable Methoden-Parameter

In Java 5 wurden die variablen Methoden-Parameter eingeführt, d.h. man kann einer Methode eine variable Anzahl an Parametern übergeben. Das klingt seltsam, also sehen wir uns die Sache mal an einem Beispiel an. Sie kennen das folgende:


public void myMethod(int intParameter1, int intParameter2, String stringParameter) { 
    // Mach was schlaue mit den Parametern
}


Hier haben wir eine feste Parameteranzahl: Wir übergeben der Methode zwei int-Parameter und einen String-Parameter. Und das ohne jede Ausnahme. Diese drei Parameter müssen wir übergeben. Wir können der Methode aber keinen dritten int-Parameter geben. Wir müssen uns mit diesen beiden zufrieden geben. Sehen Sie sich mal das nächste Beispiel an.

public void myMethod(int [] intParameters) {
for (int myInt: intParameters) {
System.out.println(myInt);
}
}


Jetzt arbeite ich mit einem Array. Darin kann ich mehrere Parameter unterbringen und dieses Array übergebe ich an die Methode. Ich kann nun ein Array mit einem Parameterwert übergeben, aber ich kann auch eines mit zehn Parameterwerten übergeben. Das geht also schon mehr in die Richtung von variablen Parametern. Das nächste Beispiel zeigt die Neuerung in Java 5.

public void myMethod(int ... intParameters) {
for (int myInt: intParameters) {
System.out.println(myInt);
}
}


Die einzige Änderung zu dem Beispiel mit dem Array sind die drei Punkte zwischen Datentyp und Parametername. Damit markieren wir Varargs, also variable Parameter. Wir können beliebig viele int-Parameter übergeben. Intern werden diese Varargs wie ein Array behandelt, ohne dass wir uns beim Aufruf mit einem Array herumplagen müssen. Und damit wären wir bei einer wichtigen Frage: Wie rufe ich das Ding eigentlich auf?

public void start() {
myMethod(4);
myMethod(4,5);
myMethod(1,2,3,4,5,6,7,8,9,0);
}


Oben sehen Sie drei mögliche Aufrufe. Wir können die Methode also tatsächlich mit beliebig vielen int-Parametern aufrufen.

Es gibt allerdings eine wichtige Einschränkung: Nach einem Vararg dürfen keine weiteren Parameter mehr folgen. Etwas wie im folgenden Listing ist also nicht erlaubt und produziert einen Compiler-Fehler:

public void myMethod(int ... intParameters, String test) {
for (int myInt: intParameters) {
System.out.println(myInt);
}
}


Damit es funktioniert, müssen die nicht variablen Parameter vor dem Vararg stehen - also wie im nächsten Listing zu sehen:

public void myMethod(String test, int ... intParameters) {
for (int myInt: intParameters) {
System.out.println(myInt);
}
}

Die Annotation @FunctionalInterface

Die Annotation @FunctionalInterface erinnert vielleicht ein wenig an @Override - auch sie dient dazu, Hilfe vom Compiler zu bekommen, um Programmierfehler zu vermeiden. Um was geht es? Natürlich um funktionale Interfaces. Dazu an dieser Stelle ein ganz kleiner Exkurs, damit auch jeder die Sache versteht.

 Funktionale Interfaces wurden in Java 8 eingeführt. Sie bezeichnen Interfaces, die nur eine einzige abstrakte Methode definieren. Der folgende Code zeigt zwei Interfaces - das erste ist funktional, das zweite nicht.

 public interface FunctionInterface{
     void myMethod();
}

public interface NotFunctionalInterface{
    void myFirstMethod();
    void mySecondMethod();
}


 Interfaces mit nur einer Methode hat es doch schon immer gegeben. Warum benötigen sie jetzt auf einmal einen eigenen Namen? Weil es viel mehr ist als nur ein Name. In Java 8 haben funktionale Interfaces eine wirkliche syntaktische Bedeutung im Rahmen von Lambda-Ausdrücken. Diesem Thema widme ich bei Gelegenheit noch eigene Blog-Beiträge. Jetzt erst einmal zur Annotation.

Wir können funktionale Interfaces mit @FunctionalInterface markieren. Damit sagen wir dem Compiler: "He, Compiler. Dieses Interface soll funktional sein. Pass mal bitte mit auf." Jetzt sollte jeder von uns bis eins zählen können - das traue ich jedem Leser dieses Blogs zu. Warum benötigt man dann diese Annotation?

Seine wirkliche Stärke erhält @FunctionalInterface, wenn ein Interface geändert wird. Nehmen wir mal das erste Beispiel von oben. Ich führe in das Interface FunctionalInterface noch eine zweite Methode ein, weil sich mein Programm ja weiter entwickelt. Mit einem Schlag würden alle Lambda-Ausdrücke, die dieses Interface verwenden, nicht mehr funktionieren. Wenn ich mein funktionales Interface mit @FunctionalInterface markiere und ich füge irgendwann aus Versehen eine weitere Methode ein, wird der Compiler sich darüber beschweren und mich auf diesen Fehler aufmerksam machen. Im Folgenden noch ein Beispiel mit der Annotation.

public interface FunctionInterface{
    @FunctionalInterface
    void myMethod();
}

Die Annotation @Deprecated

Methoden können mit der Annotation @Deprecated als veraltet markiert werden, d.h. sie sollten nicht mehr benutzt werden. Auch Klassen, Interfaces usw. können mit dieser Annotation versehen werden. Sie können diese Information natürlich auch in den JavaDoc-Kommentar schreiben. Dort wird er mit @deprecated angegeben. Die Annotation @Deprecated hat jedoch den Vorteil, dass der Compiler diese Information nun kennt. Das hat zur Folge, dass Ihre IDE diese veralteten Elemente markieren kann. So streicht Eclipse diese Elemente aus und zeigt eine Warnung an. Zudem ist die Annotation immer verfügbar und damit auch diese sehr wichtige Information, auch wenn das JavaDoc nicht zur Verfügung steht.

Mehr gibt es zu @Deprecated nicht mehr zu sagen. Daher im Folgenden mal ein Beispiel:

public class MyClass {
    @Deprecated
    public void deprecatedMethod() {
        System.out.println("Ich bin veraltet!");
   }
}

@Deprecated
public class DeprecatedClass {
    @Deprecated
    public void deprecatedMethod() {
        System.out.println("Ich bin veraltet! Aber auch meine Klasse ist veraltet!");
   }
}

Die Annotation @Override

Die Annotation @Override dürfte die Annotation sein, die ich am häufigsten verwende, denn ich finde sie wahnsinnig nützlich. Leider kennen viele Programmierer sie nicht und deswegen möchte ich sie als erstes vorstellen.

Eine Studentengruppe kam einmal zu mir und bat mich um Hilfe, weil ihre toString()-Methode nicht funktionierte. Nicht ihre überschreibende Methode wurde aufgerufen, sondern die Methode toString() von Object, also diejenige, welche die Speicheradresse des Objekts ausgibt.

Der Fehler war relativ einfach: Sie hatten die Methode falsch benannt. Statt sie toString() zu nennen, hatten sie sich verschrieben und die Methode hatte den Namen tostring(). Ein kleiner Fehler mit großer Wirkung, denn die Laufzeitumgebung konnte nicht erkennen, dass hier eine Methode aus der Klasse Object überschrieben werden sollte. Und an dieser Stelle kommt @Override ins Spiel, denn hätten die Studenten diese Annotation benutzt, hätten sie nicht mehrere Stunden nach diesem Fehler gesucht.

Sehen Sie sich mal den folgenden Code an:

public class MySuperClass {
    public void method() {
        System.out.println("method() in MySuperClass");
   }
}

public class MySubClass {
    public void metod() {
        System.out.println("method() in MySubClass");
   }
}


Ich möchte gerne die Methode method() in der Unterklasse überschreiben. Aber wie in meinem oben beschriebenen Beispiel habe ich mich verschrieben. Der Compiler bemerkt das nicht und erzeugt zwei unterschiedliche Methoden. Somit bekomme ich wegen meines kleinen Schreibfehlers einen ziemlich doofen Laufzeitfehler, der unter Umständen schwer zu finden ist. Im folgenden Code habe ich die Annotation @Override eingebaut.

public class MySuperClass {
    public void method() {
        System.out.println("method() in MySuperClass");
   }
}

public class MySubClass {
  @Override
  public void metod() {
        System.out.println("method() in MySubClass");
   }
}


Mit dieser Annotation sage ich dem Compiler: "He, Compiler. Ich glaube, ich überschreibe da eine Methode. Aber pass mal lieber mit auf!" Und in meinem Beispiel wird der Compiler jetzt maulen und sagen, dass er die Methode metod() nicht überschreiben kann, weil es sie in der Oberklasse nicht gibt. Und schon habe ich aus einem potentiellen Laufzeitfehler einen Übersetzungszeitfehler gemacht. Und wenn der Compiler einen Fehler finden kann, ist das Gold wert, denn ich erhalte überhaupt kein lauffähiges Programm. Laufzeitfehler suchen kann erheblich mühsamer werden.

Das tut die Annotation @Override: Sie markiert eine Methode für den Compiler mit dem Hinweis, dass diese Methode überschrieben werden soll. Diese Annotation hat gleich mehrere Vorteile:


  1. Wie schon beschrieben erhalten wir einen Compilerfehler, wenn wir eine Methode nicht überschreiben. Damit sind Schreibfehler schon einmal ausgeschlossen. Zudem können wir den Compilerfehler auch nutzen, wenn wir uns unsicher sind, ob diese Methode wirklich in der Oberklasse existiert.
  2. Dokumentation: Programmierer dokumentieren ja normalerweise eher ungerne - eine Eigenart, die ihre Software nicht unbedingt besser macht. Die Information, ob eine Methode überschrieben wird, ist jedoch sehr wichtig und sollte dokumentiert werden. Wenn wir @Override verwenden, bekommen wir die Dokumentation also frei Haus mit geliefert.
  3. Wenn der Name der überschriebenen Methode in der Oberklasse ändert, bekommen das die Klassen, die Methoden überschreiben möchten, normalerweise nicht mit. Hier ist also Vorsicht und Umsicht von Seiten des Programmierers angesagt - darauf sollte man sich nicht verlassen. Mit @Override passt der Compiler mit uns zusammen auf. 
  4. Gute IDEs wie Eclipse oder NetBeans können die Annotation nutzen, um Sie als Programmierer zu unterstützen. Mal angenommen, Sie schreiben eine Klasse, die ein Interface implementieren soll. Das Interface existiert bereits, aber es enthält die Methodendeklarationen noch nicht. Dann implementieren Sie die Methode in der Klasse, klatschen @Override darüber und die IDE legt dann die Methode im Interface an. Das ist ziemlich praktisch.
Grundsätzlich sollten Sie @Override immer verwenden, wenn Sie eine Methode überschreiben oder eine abstrakte Methode implementieren. Sie sehen oben, dass die Annotation auch Vorteile mit sich bringt, wenn man sich ganz sicher ist, dass man sich beim Methodennamen nicht verschrieben hat. Ich persönlich verwende @Override auch bei toString() und equals(). Diese Annotation ist einfach, aber unglaublich praktisch. 

Freitag, 26. Juli 2019

Was sind Annotationen?

Annotationen wurden mit der Java-Version 5 eingeführt. Das liegt heute, 2019, gut 14 Jahre zurück, aber noch immer muss ich erkennen, dass nicht jeder die Annotationen kennt - oft nicht einmal die Standard-Annotationen. Dabei sind sie ein mächtiges Werkzeug. Ich möchte in den nächsten Blog-Beiträgen ein wenig in das Thema Annotationen eintauchen.

Annotationen sind Markierungen im Quellcode, also Meta-Informationen, Informationen über Informationen. In unserem Fall sind es Informationen über den Quelltext. Wir markieren bestimmte Teile des Quelltextes. Dann brauchen wir noch jemanden, der den Quelltext inspiziert, die Markierungen findet und darauf reagiert. Das kann der Compiler sein, ein Framework oder eine Java-Klasse, die wir selbst geschrieben haben.

Annotationen erkennt man an dem vorgestellten @-Zeichen. Im folgenden Listing sehen Sie eine Annotation (@Override), die ich im nächsten Beitrag genauer erläutern werde.

@Override
public void meineMethode() {
    System.out.println("Hallo Annotationen");
}


Diese Annotation wird vom Compiler ausgewertet und ist enorm nützlich. An dem Beispiel ist zu sehen, dass Annotationen an einer bestimmten Stelle stehen müssen. Ich hätte @Override nicht an eine Klasse oder ein Attribut schreiben dürfen. Das würde der Compiler mit einem Fehler quittieren.

In diesem ersten Beitrag wollte ich nur einige grundsätzliche Dinge über Annotationen schreiben. Es geht dann weiter mit der oben erwähnten Annotation @Override.

Ich stelle die folgenden Standard-Annotationen vor:

  1. @Override
  2. @Deprecated
  3. @FunctionalInterface
  4. @SupressWarnings
  5. @SafeVarargs