Properties und Preferences

Einstellungen

Oftmals ist es sinnvoll, das Verhalten eines Programms über Voreinstellungen konfigurierbar zu gestalten. In Java gibt es dafür grundsätzlich zwei verschiedene Möglichkeiten: Der Einsatz von Properties Dateien oder die Benutzung der Preferences API.

Properties

Properties Dateien und die zugehörige Klasse java.util.Properties gibt es seit Java 1.0. Dazu schreibt man beliebige Key-Value-Paare zeilenweise in eine Textdatei:

# Example of a property file

# The name to use
name = Hallo

count = 4

Mit # beginnende Zeilen sind Kommentare, leere Zeilen werden ignoriert. Properties beginnen mit dem Schlüssel (Key) gefolgt von dem zugehörigen Wert (Value). Sie sind durch Leerzeichen und/oder = oder : getrennt. Sehr lange Werte lassen sich durch Verwendung des Backslash \ als letztes Zeichen auf der folgenden Zeile fortsetzen.

Meist wählt man für eine solche Properties Datei die Extension .properties. Das Einlesen in einem Java Programm erfolgt mittels der load() Methode des Properties Objektes:

import java.io.*;
import java.util.*;

// ...
Properties myProps = new Properties();
try (InputStream inStream = ClassLoader.getSystemResourceAsStream("de/kompf/prefs/demo.properties")) {
  if (inStream != null) {
    myProps.load(inStream);
  }
}

Das hier gezeigte Verfahren geht davon aus, dass die Properties Datei durch den Classloader erreichbar ist, zum Beispiel weil sie im gleichen Directory wie die .class Datei liegt. Alternativ kann man sie natürlich auch über einen FileInputStream und die Angabe eines relativen oder absoluten Pfades direkt aus dem Dateisystem laden.

Sowohl Schlüssel als auch Wert sind immer vom Typ String. Da Properties das Interface java.util.Map implementiert, kann die Abfrage der Werte prinzipiell auch mit den generischen Methoden von Map erfolgen. Komfortabler ist die Verwendung der spezifischen Methoden des Properties Objektes, da diese direkt Strings zurückliefern und die Angabe eines Defaultwertes erlauben:

String name = myProps.getProperty("name", "");
int count = Integer.parseInt(myProps.getProperty("count", "0"));

System.out.printf("%s %d%n", name, count);

Das Verfahren kommt oft bei Serverprogrammen zur Anwendung, die keine Benutzeroberfläche besitzen. Hier bieten die in jedem Texteditor zu bearbeitenden Properties Dateien eine einfache und wirkungsvolle Möglichkeit, das Verhalten der Software von außen zu beeinflussen. Fehleranfällig ist oft das Bestimmen des korrekten Pfades zur Properties Datei. Wenn diese nicht im Klassenpfad liegt, muss der Entwickler oft zusätzliche Umgebungsvariablen oder Systemproperties auswerten, da man ja nicht vorhersagen kann, wohin der Anwender das Programm später installiert. Außerdem ist zu beachten, dass beim gezeigten Verfahren des Ladens über einen InputStream die Properties Datei immer mit dem Zeichensatz ISO 8859-1 kodiert sein muss! Das birgt potentielle Risiken, wenn zum Beispiel ein Anwender UTF-8 als Standardkodierung verwendet.

Prinzipiell lassen sich Properties Objekte auch programmatisch per setProperty(key, value) modifizieren und dann mittels store() in eine Datei zurückschreiben. Leider werden dabei sämtliche Kommentare und Leerzeilen entfernt und die Reihenfolge der Properties zerstört. Das gerade als Vorteil dargestellte Editieren der Datei in einem externen Texteditor gerät dann zur Qual und mündet in Frustation der Anwender.

Preferences

Um diese Nachteile zu umgehen, bietet Java seit Version 1.4 mit der Klasse java.util.prefs.Preferences eine fortschrittlichere Methode des Umgangs mit Voreinstellungen. Ein typisches Anwendungsbeispiel für das Lesen von Preferences kommt mit wenigen Zeilen aus:

import java.util.prefs.*;

public class PreferencesDemo {
  public void readUserPreferences() {
    Preferences prefs = Preferences.userNodeForPackage(getClass());
    String name = prefs.get("name", "");
    int count = prefs.getInt("count", 0);

    System.out.printf("%s %d%n", name, count);
  }
}

Das Schreiben von Preferences ist ebenfalls mit wenig Aufwand erledigt:

  public void writeUserPreferences(int count, String name) {
    Preferences prefs = Preferences.userNodeForPackage(getClass());

    prefs.put("name", name);
    prefs.putInt("count", count);
  }

Die Auswahl eines eindeutigen Speicherortes für eine konkrete Anwendung erfolgte im Beispiel durch die Übergabe der Anwendungsklasse an die Methode userNodeForPackage. Die so gespeicherten Voreinstellungen gelten nur für den akzuellen Benutzer. Für systemweite Preferences benutzt man dagegen systemNodeForPackage.

Insbesondere braucht sich der Programmierer jetzt keine Gedanken mehr um den Speicherort und das Encoding der Schlüssel-Werte-Paare zu machen. Das Verfahren eignet sich auch sehr gut für das persistente Ablegen von internen Zuständen eines Programms zwischen einzelnen Aufrufen. Bietet eine Anwendung zum Beispiel die Auswahl einer Datei mittels eines javax.swing.JFileChooser an, dann ist es durchaus im Sinne der Benutzer, das zuletzt ausgewählte Verzeichnis für den nächsten Programmaufruf zwischenzuspeichern. Dafür eignet sich die Preferences API hervorragend.

Natürlich ist jetzt das Verändern der Voreinstellungen über einen Texteditor nicht mehr möglich, da man ja nicht weiß, wo und wie die Java Laufzeitumgebung die Preferences persistiert. Der Softwareentwickler ist jetzt also gezwungen, für das Editieren von Voreinstellungen eine geeigenete Benutzeroberfläche in seiner Anwendung bereit zu stellen. Dies sollte für eine positive Nutzererfahrung aber sowieso der Fall sein!

Auf Linuxsystemen mit OpenJDK liegen die Benutzervoreinstellungen als Dateien unterhalb von $HOME/.java/.userPrefs und die systemweiten Einstellungen unter /etc/.java. Die Windows-JRE von Oracle speichert die Werte in der Registry unter HKEY_CURRENT_USER\Software\JavaSoft\Prefs beziehungsweise HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Prefs.

Inventur

Beide APIs bieten auch Methoden zur Erkundung der vorhandenen Voreinstellungen an. Bei einem Properties Objekt kann man einfach die Iteratoren des Map Interfaces verwenden:

  public void printAll(Properties props) {
    for (Map.Entry<Object, Object> entry : props.entrySet()) {
      System.out.printf("%s = %s%n", entry.getKey(), entry.getValue());
    }
  }

Preferences sind hierarchisch organisiert, demzufolge bietet sich eine rekursive Technik zu ihrer Erkundung an:

  public void printAll(Preferences prefs) throws BackingStoreException {
    System.out.printf("Path: %s%n", prefs.absolutePath());
    for (String key : prefs.keys()) {
      System.out.printf("  %s = %s%n", key, prefs.get(key, "?"));
    }
    for (String child : prefs.childrenNames()) {
      printAll(prefs.node(child));
    }
  }

Neben den bisher gezeigten Methoden enthält die Preferences API noch weitere, zum Beispiel zum Lesen und Schreiben von weiterer Datentypen wie double oder long, zum Exportieren und Importieren eines XML Dokuments sowie zum Registrieren von Listeners, die auf Änderungen an den Voreinstellungen reagieren. Details dazu finden sich in der Preferences API Dokumentation.