Properties Files: Reading and Writing .properties

The java.util.Properties class is a simple, persistent key–value store that reads and writes .properties files: text files where each line has the form key=value. Properties files are used for application configuration, locale-specific messages, and build settings. This article covers the full Properties API: reading from files and the classpath, writing and storing, the XML variant, system properties, and how Properties compares to using a plain Map.

Contents

What is a .properties file?

A .properties file is a plain text configuration format where each line contains a key and value separated by =, :, or whitespace. Lines starting with # or ! are comments. A backslash at the end of a line continues the value on the next line, and Unicode characters can be embedded using \uXXXX escape sequences. Keys are case-sensitive. Values are always strings: type conversion (to int, boolean, etc.) is the responsibility of the calling code. In Maven and Gradle projects, properties files are placed in src/main/resources and copied to the classpath root during the build.

Java
// A .properties file is a plain text file of key=value pairs.
// Format rules:
//   - Keys and values are separated by = or :
//   - Leading and trailing whitespace around the separator is trimmed
//   - Lines starting with # or ! are comments
//   - Long values can be split across lines with a backslash continuation
//   - Unicode escapes (\uXXXX) are supported
//   - Keys may contain any character except =, :, or whitespace (unescaped)

// Example file: app.properties
// # Database settings
// db.host = localhost
// db.port = 5432
// db.name = myapp
// db.user = admin
// db.password = s3cr3t!
//
// # Feature flags
// feature.dark-mode = true
// feature.beta      = false
//
// # Multi-line value
// welcome.message = Hello, welcome to \
//                   MyApp version 2.0!
//
// # Unicode
// greeting.japanese = \u3053\u3093\u306b\u3061\u306f

// Keys are case-sensitive:
// "DB.HOST" and "db.host" are different keys

// Colons and equals are both valid separators:
// server.port: 8080
// server.port = 8080  ← both are equivalent

// Values are always Strings β€” type conversion is the caller's responsibility

Reading with Properties.load()

Properties extends Hashtable and stores all keys and values as strings. load(InputStream) parses the file using ISO-8859-1 encoding; use load(Reader) with an explicit UTF-8 Reader to correctly handle non-ASCII characters without Unicode escapes. The stream or reader should be closed after loading: use try-with-resources. Calling load() on a Properties object that already has entries merges the new keys in, overwriting any duplicates.

Java
import java.util.Properties;
import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;

Properties props = new Properties();

// Load from a File β€” load(InputStream) uses ISO-8859-1 (legacy)
try (InputStream is = new FileInputStream("app.properties")) {
    props.load(is); // ISO-8859-1 decoding β€” fine for ASCII
}

// Load with explicit UTF-8 β€” use load(Reader) for Unicode support
// This is the PREFERRED approach for modern code
try (Reader reader = Files.newBufferedReader(
        Path.of("app.properties"), StandardCharsets.UTF_8)) {
    props.load(reader);
}

// Print all loaded properties
props.forEach((key, value) -> System.out.println(key + " = " + value));

// List to a PrintStream (useful for debugging)
props.list(System.out);

// Load preserves insertion order only with LinkedProperties workaround;
// standard Properties iteration order is not guaranteed

// Reload / merge β€” load() adds to existing entries, overwriting duplicates
Properties defaults = new Properties();
defaults.setProperty("timeout", "30");
defaults.setProperty("retry", "3");

Properties override = new Properties(defaults); // defaults as fallback
try (Reader r = Files.newBufferedReader(Path.of("app.properties"), StandardCharsets.UTF_8)) {
    override.load(r); // override-specific keys take priority
}

// size β€” number of entries
System.out.println("Loaded " + props.size() + " properties");
The load(InputStream) overload uses ISO-8859-1 encoding, not UTF-8. Non-ASCII characters (accented letters, CJK, emoji) must be written as \uXXXX Unicode escapes in the file if you use the InputStream overload. Use load(Reader) with an explicit UTF-8 Reader to support Unicode characters directly in the file.

getProperty() with defaults

getProperty(key) returns null when the key is absent. The two-argument form getProperty(key, defaultValue) returns the default instead of null, which is almost always the safer choice: it avoids null checks and makes the fallback value explicit at the call site. Avoid the inherited get(key) method from Hashtable: it returns Object, bypasses the default lookup chain, and skips keys whose values are not String instances.

Java
Properties props = new Properties();
props.setProperty("db.host", "localhost");
props.setProperty("db.port", "5432");
props.setProperty("db.name", "myapp");

// getProperty(key) β€” returns the value or null if key not found
String host = props.getProperty("db.host");     // "localhost"
String user = props.getProperty("db.user");     // null

// getProperty(key, defaultValue) β€” returns default if key not found
String dbUser = props.getProperty("db.user", "root"); // "root"
int port = Integer.parseInt(props.getProperty("db.port", "3306")); // 5432

// containsKey β€” check before getting (or just use the default overload)
if (props.containsKey("feature.beta")) {
    boolean betaEnabled = Boolean.parseBoolean(props.getProperty("feature.beta"));
    System.out.println("Beta: " + betaEnabled);
}

// getOrDefault β€” inherited from Hashtable (returns Object, requires cast)
String pass = (String) props.getOrDefault("db.password", "changeme");

// Iterating all entries
for (String key : props.stringPropertyNames()) { // returns Set<String>
    System.out.println(key + " = " + props.getProperty(key));
}

// Chained fallback: environment variable > properties file > hardcoded default
String dbHost = System.getenv("DB_HOST");                    // env var first
if (dbHost == null) dbHost = props.getProperty("db.host");  // then file
if (dbHost == null) dbHost = "localhost";                    // then hardcoded
System.out.println("Using host: " + dbHost);

// Type-safe getters using helper methods
static int getInt(Properties p, String key, int def) {
    String val = p.getProperty(key);
    if (val == null) return def;
    try { return Integer.parseInt(val.strip()); }
    catch (NumberFormatException e) { return def; }
}

static boolean getBool(Properties p, String key, boolean def) {
    String val = p.getProperty(key);
    return val == null ? def : Boolean.parseBoolean(val.strip());
}

Reading from the classpath

Properties files bundled inside a JAR do not have a filesystem path: they can only be accessed through the classpath using getResourceAsStream(). Do not use new FileInputStream("app.properties") in production code; it relies on the current working directory and breaks when the application runs from a JAR. Use ClassLoader.getResourceAsStream("config/app.properties") (always root-relative, no leading slash) or getClass().getResourceAsStream("/config/app.properties") (leading slash for root-relative). Always null-check the returned stream before passing it to Properties.load().

Java
// Properties files in src/main/resources are packaged into the JAR
// and accessed via the classpath β€” NOT via a file path

// Pattern 1: getClass().getResourceAsStream() β€” relative to the class package
Properties props = new Properties();
try (InputStream is = getClass().getResourceAsStream("/config/app.properties")) {
    if (is == null) throw new IllegalStateException("app.properties not found on classpath");
    props.load(new InputStreamReader(is, StandardCharsets.UTF_8));
}

// Pattern 2: ClassLoader.getResourceAsStream() β€” always absolute (no leading /)
try (InputStream is = Thread.currentThread()
        .getContextClassLoader()
        .getResourceAsStream("config/app.properties")) {
    if (is == null) throw new IllegalStateException("Resource not found");
    props.load(new InputStreamReader(is, StandardCharsets.UTF_8));
}

// Pattern 3: using the class's own ClassLoader (works in modular apps)
try (InputStream is = MyConfig.class.getClassLoader()
        .getResourceAsStream("config/app.properties")) {
    if (is == null) throw new IllegalStateException("Resource not found");
    props.load(new InputStreamReader(is, StandardCharsets.UTF_8));
}

// Typical Maven/Gradle project layout:
//   src/main/resources/
//     config/
//       app.properties         β†’ classpath: "config/app.properties"
//     application.properties   β†’ classpath: "application.properties"

// Merging defaults + overrides from classpath
Properties defaults = new Properties();
try (InputStream is = MyApp.class.getResourceAsStream("/defaults.properties")) {
    defaults.load(new InputStreamReader(is, StandardCharsets.UTF_8));
}

Properties config = new Properties(defaults); // use defaults as fallback
try (InputStream is = MyApp.class.getResourceAsStream("/app.properties")) {
    if (is != null) {
        config.load(new InputStreamReader(is, StandardCharsets.UTF_8));
    }
}
// Keys in app.properties override defaults.properties; missing keys fall back
System.out.println(config.getProperty("timeout")); // from whichever file has it
When loading a classpath resource, always check for null: getResourceAsStream() returns null (not an exception) if the resource is not found. A null InputStream causes a NullPointerException later when load() is called, which gives a confusing error message. Fail fast with a clear message instead.

Writing and storing properties

store(OutputStream, comment) writes the properties back to a stream in the standard key=value format, prefixed with a comment header and an auto-generated timestamp. Pass null as the comment to suppress the header line entirely. setProperty(key, value) adds a new entry or updates an existing one. Use store(Writer, comment) with an explicit UTF-8 writer if the values contain non-ASCII characters. Note that the output order of keys is not guaranteed: entries may appear in any order.

Java
import java.util.Properties;
import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;

Properties props = new Properties();
props.setProperty("app.name", "MyApp");
props.setProperty("app.version", "2.0.0");
props.setProperty("app.debug", "false");
props.setProperty("app.maxConnections", "10");

// store(OutputStream, comment) β€” writes ISO-8859-1, with a header comment
try (OutputStream os = new FileOutputStream("output.properties")) {
    props.store(os, "Application configuration β€” auto-generated");
}
// Output (order not guaranteed):
// #Application configuration β€” auto-generated
// #Mon Jan 01 12:00:00 UTC 2025
// app.name=MyApp
// app.version=2.0.0
// ...

// store(Writer, comment) β€” use with explicit UTF-8 writer for Unicode support
try (Writer writer = Files.newBufferedWriter(
        Path.of("output.properties"), StandardCharsets.UTF_8)) {
    props.store(writer, "Generated config");
}

// Add or update a key
props.setProperty("app.debug", "true"); // overwrites existing
props.setProperty("app.logLevel", "INFO"); // new key

// Remove a key
props.remove("app.debug");

// Update if absent (Java 8+) β€” inherited from Hashtable via Map
props.putIfAbsent("app.timeout", "30");

// Check and get in one step
String version = props.computeIfAbsent("app.version", k -> "1.0.0").toString();

// Merge from another Properties object
Properties extra = new Properties();
extra.setProperty("feature.x", "enabled");
extra.forEach((k, v) -> props.setProperty((String) k, (String) v));

// Store with sorted keys β€” default store() writes in an unpredictable order
// Workaround: use a TreeMap
Properties sorted = new Properties() {
    @Override
    public Set<Map.Entry<Object, Object>> entrySet() {
        return new TreeMap<>(super.entrySet().stream()
            .collect(java.util.stream.Collectors.toMap(
                Map.Entry::getKey, Map.Entry::getValue)))
            .entrySet();
    }
};
sorted.putAll(props);
try (Writer w = Files.newBufferedWriter(Path.of("sorted.properties"), StandardCharsets.UTF_8)) {
    sorted.store(w, "Sorted config");
}

XML properties format

Properties.loadFromXML() and storeToXML() use a structured XML format with <entry key="..."> elements instead of the flat key=value text format. Because the XML format enforces UTF-8 encoding, it is a better choice than the plain text format when property values contain non-ASCII characters that you want to store directly without Unicode escapes. This format is also useful when the properties file will be consumed or validated by XML-aware tools.

Java
// Properties can also be stored as XML (ISO 10646 encoding)
// Format: loadFromXML / storeToXML

// Example XML properties file:
// <?xml version="1.0" encoding="UTF-8"?>
// <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
// <properties>
//   <comment>Application configuration</comment>
//   <entry key="db.host">localhost</entry>
//   <entry key="db.port">5432</entry>
//   <entry key="db.name">myapp</entry>
// </properties>

import java.util.Properties;
import java.io.*;
import java.nio.file.*;

// Load from XML
Properties xmlProps = new Properties();
try (InputStream is = new FileInputStream("config.xml")) {
    xmlProps.loadFromXML(is);
}
System.out.println(xmlProps.getProperty("db.host")); // localhost

// Load from XML on classpath
try (InputStream is = getClass().getResourceAsStream("/config/app-config.xml")) {
    xmlProps.loadFromXML(is);
}

// Store to XML
Properties toSave = new Properties();
toSave.setProperty("server.host", "example.com");
toSave.setProperty("server.port", "443");
toSave.setProperty("server.ssl",  "true");

try (OutputStream os = new FileOutputStream("server.xml")) {
    toSave.storeToXML(os, "Server configuration", "UTF-8");
}
// Generates well-formed XML with UTF-8 encoding β€” better for Unicode than .properties

// When to use XML properties vs .properties:
// .properties β€” simpler format, human-editable, universally understood
// XML         β€” better Unicode support without escaping, schema validation possible,
//               useful when the file is also consumed by non-Java tools

System.getProperties() and system properties

JVM system properties are set at startup with -Dkey=value flags and are accessible at runtime via System.getProperty(key). They are the standard mechanism for passing environment-specific configuration (database URLs, log levels, feature flags) to a Java application without modifying bundled resource files. System.getProperties() returns all system properties as a Properties object, including JVM-provided values like java.version, os.name, and user.home. Avoid calling System.setProperty() in library code: it modifies global JVM state and affects all threads and classes loaded in the same JVM.

Java
// System properties β€” set by the JVM and overridable via -D on the command line
// System.getProperty(key) β€” get a single property
// System.getProperties() β€” get all as a Properties object

// Common built-in system properties
System.out.println(System.getProperty("java.version"));        // e.g. 21.0.2
System.out.println(System.getProperty("java.vendor"));         // e.g. Eclipse Adoptium
System.out.println(System.getProperty("java.home"));           // JDK/JRE install dir
System.out.println(System.getProperty("os.name"));             // Windows 11 / Linux / Mac OS X
System.out.println(System.getProperty("os.arch"));             // amd64 / aarch64
System.out.println(System.getProperty("user.name"));           // login name
System.out.println(System.getProperty("user.home"));           // home directory
System.out.println(System.getProperty("user.dir"));            // current working directory
System.out.println(System.getProperty("file.separator"));      // / or \
System.out.println(System.getProperty("line.separator"));      // \n or \r\n
System.out.println(System.getProperty("path.separator"));      // : or ;
System.out.println(System.getProperty("file.encoding"));       // UTF-8

// Get with default
String logDir = System.getProperty("app.logDir", "/var/log/myapp");

// Set a property programmatically
System.setProperty("app.env", "staging");
System.out.println(System.getProperty("app.env")); // staging

// Set via command line: java -Dapp.env=production -jar myapp.jar

// Access all properties as a Properties object
Properties sysProps = System.getProperties();
sysProps.stringPropertyNames().stream()
    .sorted()
    .forEach(k -> System.out.println(k + " = " + sysProps.getProperty(k)));

// Merging system properties with app properties (system overrides app)
Properties appProps = new Properties();
try (Reader r = Files.newBufferedReader(Path.of("app.properties"), StandardCharsets.UTF_8)) {
    appProps.load(r);
}
// Allow -D flags to override any app property
appProps.putAll(System.getProperties()); // system properties win

// Best practice: don't call System.setProperty() in library code β€”
// it modifies the global JVM state and affects all threads
Use -D flags (java -Dkey=value) to override configuration at startup without changing the properties file. This is the standard way to pass environment-specific settings (e.g., database URLs, log levels) to a Java application.

Properties vs Map

Properties is a legacy class that extends Hashtable<Object, Object>, which means the inherited put() and get() methods accept any Object, not just strings: a source of subtle bugs when non-string values are stored silently and then silently ignored by getProperty(). For new code that is not required to interoperate with file-based properties, a Map<String, String> is simpler, type-safe, and stream-friendly. Keep using Properties when loading .properties files, accessing system properties, or passing configuration to APIs that expect a Properties object.

Java
// Properties extends Hashtable<Object, Object> β€” a legacy design
// This means it inherits methods like put(Object, Object) that bypass type safety

Properties props = new Properties();

// SAFE β€” use the String-typed API
props.setProperty("key", "value");          // good
String v = props.getProperty("key");        // good

// UNSAFE β€” inherited Hashtable methods accept Object, not just String
props.put("key",  42);           // stores Integer β€” NOT a String!
props.put(100,    "value");      // stores Integer key β€” breaks iteration!

// getProperty() skips non-String values β€” so this returns null
props.put("count", 5);
System.out.println(props.getProperty("count")); // null β€” because value is Integer, not String
System.out.println(props.get("count"));         // 5   β€” using Map.get() works

// Always use setProperty/getProperty, never put/get for Properties
// Enforcing this with a wrapper or using a Map<String,String> instead:

Map<String, String> config = new LinkedHashMap<>();
config.put("db.host", "localhost");
config.put("db.port", "5432");

// Convert Map to Properties for legacy APIs
Properties fromMap = new Properties();
fromMap.putAll(config); // safe here because all keys/values are String

// Convert Properties to Map<String,String> (only String keys/values)
Map<String, String> toMap = props.stringPropertyNames().stream()
    .collect(Collectors.toMap(k -> k, props::getProperty));

// When to use Properties vs Map:
// Properties β€” reading .properties files, system properties, ResourceBundle integration
// Map        β€” new code where you control the data source; type-safe; stream-friendly

// Properties is thread-safe (Hashtable is synchronized) but Map is not
// For concurrent use of a Map, prefer ConcurrentHashMap<String, String>
Properties extends Hashtable<Object, Object>, so the inherited put(key, value) method accepts non-String values. These values are silently ignored by getProperty(), causing subtle bugs. Always use setProperty(String, String) and getProperty(String): never the raw Map methods.