Java Properties-Klasse
String-Schlüssel-Wert-Paare in Java mit der Properties-Klasse laden und speichern, einschließlich .properties-Dateien.
Properties ist der JDK-Container für String-zu-String-Konfigurationen — Anwendungseinstellungen, Umgebungsüberschreibungen, lokalisierte Meldungen, JDBC-Verbindungsparameter. Er erweitert Hashtable<Object, Object> (eine historische Entscheidung, die wir heute bereuen, aber damit leben) und fügt drei Dinge hinzu: ein .properties-Dateiformat mit Reader und Writer, einen XML-Reader und -Writer sowie das Konzept eines Default-Properties-Objekts, das abgefragt wird, wenn ein Schlüssel lokal nicht gefunden wird.
System.getProperties() gibt ein Properties-Objekt zurück. Jeder Aufruf von System.getProperty("user.home") geht durch es. Sobald man die Klasse kennt, sieht man sie überall.
Der Vertrag: Strings auf beiden Seiten
Obwohl die Klasse die Methode put(Object, Object) von Hashtable erbt, ist das einzig sichere API das String-typisierte Paar:
Properties config = new Properties();
config.setProperty("server.port", "8080");
config.setProperty("server.host", "localhost");
String port = config.getProperty("server.port"); // "8080"
String log = config.getProperty("log.level", "INFO"); // default fallbackWenn man setProperty umgeht und put("server.port", 8080) mit einem Integer aufruft, landet der Eintrag zwar in der Tabelle, man hat aber eine Falle gelegt: stringPropertyNames() filtert ihn still heraus, und die Dateischreibmethoden (store, storeToXML) werfen eine ClassCastException, sobald sie versuchen, diesen Integer in String umzuwandeln. Behandle Properties wie Properties<String, String>, auch wenn die Generics das nicht ausdrücken.
Das .properties-Dateiformat
Klartext, zeilenorientiert, key=value. Leerzeichen um = sind erlaubt. Zeilen, die mit # oder ! beginnen, sind Kommentare. Ein abschließender Backslash setzt den Wert in der nächsten Zeile fort. Unicode-Escapes (\uXXXX) werden unterstützt, aber seit Java 9 liest die Überladung load(Reader) nativ UTF-8, sodass man sie selten braucht.
# server.properties — last edited 2026-05-12
server.host = localhost
server.port = 8080
server.path = /api/v1
greeting = Welcome, \
user!load und store verwenden dieses Format. loadFromXML und storeToXML verwenden das entsprechende XML-Format, das durch properties.dtd definiert wird — es existiert, ist gelegentlich hilfreich, wird aber fast nie der Textform vorgezogen.
Laden und Speichern
Properties config = new Properties();
try (var in = Files.newBufferedReader(Path.of("server.properties"))) {
config.load(in); // UTF-8 text
}
config.setProperty("server.port", "9090");
try (var out = Files.newBufferedWriter(Path.of("server.properties"))) {
config.store(out, "edited by setup script"); // comment becomes the first line
}Die Methode store schreibt nach dem Benutzerkommentar einen Zeitstempel-Kommentar, sortiert nichts (Einträge erscheinen in der Iterationsreihenfolge von Hashtable) und maskiert Sonderzeichen (=, :, #, führende Leerzeichen) für das Round-Tripping. Die Ausgabe ist zwischen JVMs portierbar.
Für Ressourcen, die mit der Anwendung gebündelt sind, wird statt aus dem Dateisystem aus dem Classpath geladen:
try (var in = MyApp.class.getResourceAsStream("/app.properties")) {
config.load(in); // load(InputStream) defaults to ISO-8859-1
}load(InputStream) ist die historische Überladung und verwendet ISO-8859-1 (Latin-1) mit \u-Escapes. load(Reader) verwendet den Zeichensatz, mit dem der Reader geöffnet wurde. Bevorzuge die Reader-Form, wenn du die Kodierung kontrollierst.
Default-Properties: geschichtete Konfiguration
Das zweiargumentige getProperty(key, default) gibt einen Fallback zurück, wenn der Schlüssel fehlt. Der Konstruktor Properties(Properties defaults) macht dasselbe, aber auf Objektebene — das zweite Properties-Objekt wird abgefragt, wenn das erste den Schlüssel nicht enthält:
Properties base = new Properties();
base.setProperty("server.port", "8080");
base.setProperty("log.level", "INFO");
Properties override = new Properties(base); // base is the defaults
override.setProperty("log.level", "DEBUG"); // override wins
override.getProperty("server.port"); // "8080" (from base)
override.getProperty("log.level"); // "DEBUG" (from override)Das ist das Standardmuster für "Mit der App gelieferte Standardwerte, die der Benutzer pro Umgebung überschreiben kann." Zwei Ebenen sind der häufige Fall; man kann auch mehr verketten.
System-Properties und -D-Flags
Die JVM hat eine globale Properties-Instanz, die über System.getProperties() und System.getProperty(key) zugänglich ist. Standardschlüssel sind java.version, user.home, user.dir, os.name, file.separator und line.separator. Das -Dkey=value-Flag in der JVM-Befehlszeile fügt Einträge hinzu, bevor main ausgeführt wird:
java -Dserver.port=9090 -Dlog.level=DEBUG -jar app.jarString port = System.getProperty("server.port", "8080");Das ist die einfachste "Kommandozeilen-Konfiguration", die man einem Java-Programm geben kann. Bei größeren Konfigurationen ist die Konvention eine .properties-Datei, die mit der App ausgeliefert wird und beim Start mit System-Properties zusammengeführt wird (die als Überschreibungen fungieren).
Was Properties nicht ist
- Keine Map mit beliebigen Typen. Nur Strings.
Integer.parseInt(config.getProperty("port"))muss man selbst parsen. - Nicht hierarchisch. Schlüssel wie
db.primary.hostsind nur Strings; die Punkte sind konventionell, nicht strukturell. Wenn eine echte Hierarchie benötigt wird, verwendet man eine YAML/JSON-Konfigurationsbibliothek. - Nicht thread-sicher für zusammengesetzte Operationen. Jede Methode ist synchronisiert (von
Hashtablegeerbt), aber Check-then-Act ist immer noch einer Race-Condition ausgesetzt. Dieselbe Einschränkung wie bei der Elternklasse. - Kein Ersatz für
ResourceBundlefür i18n.PropertyResourceBundleist einResourceBundle, das durch eine.properties-Datei unterstützt wird und Locale-Lookup hinzufügt; das ist das richtige Werkzeug für übersetzte Strings.
Ein ausgearbeitetes Beispiel: Standardwerte laden, pro Umgebung überschreiben, zurückschreiben
Das folgende Programm erstellt eine geschichtete Konfiguration (Standardwerte im JAR, Umgebungsdatei außerhalb), liest eine System-Property als -D-Überschreibung, flacht das Ergebnis ab, sodass es zur Inspektion in einen .properties-Puffer geschrieben werden kann, und demonstriert die Nicht-String-store-Falle.
Eine Feinheit, die das Beispiel bewusst behandelt: store schreibt nur die eigenen Einträge eines Properties-Objekts — es durchläuft nie die geerbte defaults-Kette. Um eine vollständige, round-trippable Datei zu erhalten, kopieren wir daher jeden aufgelösten Schlüssel in ein flaches Properties-Objekt, bevor wir speichern, anstatt store auf einem Objekt aufzurufen, das sich auf sein defaults-Elternobjekt stützt.
Was man aus dem Ergebnis mitnimmt:
- Die dreilagige Konfiguration (Standardwerte → Umgebungsdatei →
-D-Überschreibungen) löst sich korrekt auf. Standardwerte füllen auf, was niemand überschrieben hat; die Umgebungsdatei ändertlog.levelundfeature.beta; das-D-Flag gewinnt fürserver.port. storeerzeugte einen portablen.properties-Text mit einem Kommentar und einem Zeitstempel am Anfang, der alle vier aufgelösten Schlüssel enthält. Man könnte diese Datei direkt wieder inloadeinlesen und dieselbe Map erhalten — weil wir die Ebenen zuvor abgeflacht haben.setProperty("age", 30)würde nicht kompilieren (es erfordert einenString).put("age", 30)kompiliert, der Eintrag landet in der Tabelle, undstringPropertyNamesfiltert ihn heraus — aberstoreüberspringt ihn nicht still: Es wirft eineClassCastException, sobald es versucht, denIntegerinStringumzuwandeln. Die Lehre: Niemals einen Nicht-String in einProperties-Objektputten — immersetPropertyverwenden.
Was kommt als Nächstes
Properties war das letzte "Datenstruktur"-Kapitel in diesem Teil. Die verbleibenden Kapitel handeln von Operationen auf Collections: ihre Durchquerung (Iterators und ListIterator), der Vergleich von Elementen (Comparable und Comparator) sowie die statischen Hilfsmethoden zum Sortieren, Suchen und Verpacken (die Collections-Klasse). Das nächste Kapitel beginnt mit dem Fundament — dem Iterator-Interface, das jede for-each-Schleife im Verborgenen nutzt.