Java Properties Class
Load and store string key-value pairs in Java with the Properties class, including .properties files.
Properties is the JDK's container for string-to-string configuration — application settings, environment overrides, localised messages, JDBC connection parameters. It extends Hashtable<Object, Object> (a historical decision we now regret, but stuck with) and adds three things on top: a .properties file format with reader and writer, an XML reader and writer, and the concept of a default properties object that's consulted when a key isn't found locally.
System.getProperties() returns a Properties. Every System.getProperty("user.home") call goes through it. Once you've seen the class, you see it everywhere.
The contract: strings on both sides
Although the class inherits a put(Object, Object) method from Hashtable, the only safe API is the string-typed pair:
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 fallbackIf you bypass setProperty and call put("server.port", 8080) with an Integer, the entry still lands in the table, but you have planted a landmine: stringPropertyNames() quietly filters it out, and the file-writing methods (store, storeToXML) throw ClassCastException the moment they try to cast that Integer to String. Treat Properties as Properties<String, String> even though the generics don't say so.
The .properties file format
Plain text, line-oriented, key=value. Whitespace around = is allowed. Lines starting with # or ! are comments. Trailing backslash continues the value onto the next line. Unicode escapes (\uXXXX) are supported, but since Java 9 the load(Reader) overload reads UTF-8 natively, so you rarely need them.
# server.properties — last edited 2026-05-12
server.host = localhost
server.port = 8080
server.path = /api/v1
greeting = Welcome, \
user!load and store deal with this format. loadFromXML and storeToXML deal with the equivalent XML format defined by properties.dtd — it exists, occasionally helpful, almost never preferred over the text form.
Loading and storing
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
}The store method writes a timestamp comment after the user comment, sorts nothing (entries land in Hashtable iteration order), and escapes special characters (=, :, #, leading whitespace) for round-tripping. The output is portable across JVMs.
For resources bundled with your application, load from the classpath instead of the filesystem:
try (var in = MyApp.class.getResourceAsStream("/app.properties")) {
config.load(in); // load(InputStream) defaults to ISO-8859-1
}load(InputStream) is the historical overload and uses ISO-8859-1 (Latin-1) with \u escapes. load(Reader) uses whatever charset the reader was opened with. Prefer the reader form when you control the encoding.
Default properties: layered configuration
The two-arg getProperty(key, default) returns a fallback when the key is absent. The constructor Properties(Properties defaults) does the same thing but at the object level — the second Properties is consulted when the first doesn't contain the key:
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)That's the standard pattern for "defaults shipped with the app, user can override per environment." Two layers is the common case; you can chain more.
System properties and -D flags
The JVM has a global Properties instance accessible via System.getProperties() and System.getProperty(key). Standard keys include java.version, user.home, user.dir, os.name, file.separator, and line.separator. The -Dkey=value flag on the JVM command line adds to it before main runs:
java -Dserver.port=9090 -Dlog.level=DEBUG -jar app.jarString port = System.getProperty("server.port", "8080");This is the simplest "command-line config" you can give a Java program. For larger configurations the convention is a .properties file shipped with the app, merged at startup with system properties (which act as overrides).
What Properties is not
- Not a map of arbitrary types. Strings only. Parse
Integer.parseInt(config.getProperty("port"))yourself. - Not hierarchical. Keys like
db.primary.hostare just strings; the dots are conventional, not structural. If you need real hierarchy, use a YAML/JSON config library. - Not thread-safe for compound operations. Every method is synchronized (inherited from
Hashtable), but check-then-act still races. Same caveat as the parent class. - Not a replacement for
ResourceBundlefor i18n.PropertyResourceBundleis aResourceBundlebacked by a.propertiesfile and adds locale lookup; that's the right tool for translated strings.
A worked example: load defaults, override per environment, write back
The program below builds a layered configuration (defaults inside the JAR, environment file outside), reads a system property to act as a -D override, flattens the result so it can be written back to a .properties buffer for inspection, and demonstrates the non-string store trap.
One subtlety the example handles deliberately: store writes only a Properties object's own entries — it never walks the inherited defaults chain. So to get a complete, round-trippable file we copy every resolved key into a flat Properties before storing, rather than calling store on an object that relies on its defaults parent.
What to take from the run:
- The three-layer configuration (defaults → environment file →
-Doverrides) resolves correctly. Defaults fill in what nobody overrode; the environment file changeslog.levelandfeature.beta; the-Dflag wins forserver.port. storeproduced a portable.propertiestext with a comment and a timestamp at the top, holding all four resolved keys. You could feed that file straight back intoloadand get the same map — because we flattened the layers first.setProperty("age", 30)would not compile (it requires aString).put("age", 30)does compile, the entry lands in the table, andstringPropertyNamesfilters it out — butstoredoes not silently skip it: it throwsClassCastExceptionas soon as it tries to cast theIntegertoString. The lesson: neverputa non-string on aProperties— alwayssetProperty.
What's next
Properties was the last "data structure" chapter in this part. The remaining chapters are about operations on collections: walking them (Iterators and ListIterator), comparing elements (Comparable and Comparator), and the static utilities for sorting, searching, and wrapping (the Collections class). The next chapter starts with the foundation — the Iterator interface every for-each loop secretly uses.