W3docs

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 fallback

If 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.jar
String 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.host are 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 ResourceBundle for i18n. PropertyResourceBundle is a ResourceBundle backed by a .properties file 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.

java— editable, runs on the server

What to take from the run:

  • The three-layer configuration (defaults → environment file → -D overrides) resolves correctly. Defaults fill in what nobody overrode; the environment file changes log.level and feature.beta; the -D flag wins for server.port.
  • store produced a portable .properties text with a comment and a timestamp at the top, holding all four resolved keys. You could feed that file straight back into load and get the same map — because we flattened the layers first.
  • setProperty("age", 30) would not compile (it requires a String). put("age", 30) does compile, the entry lands in the table, and stringPropertyNames filters it out — but store does not silently skip it: it throws ClassCastException as soon as it tries to cast the Integer to String. The lesson: never put a non-string on a Properties — always setProperty.

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.

Practice

Practice
You write `props.put('port', 8080)` (an `Integer`) on a `Properties` object and then call `props.store(out, null)`. What happens?
You write `props.put('port', 8080)` (an `Integer`) on a `Properties` object and then call `props.store(out, null)`. What happens?
Was this page helpful?