Classe Properties in Java
Carica e salva coppie chiave-valore stringa in Java con la classe Properties, inclusi i file .properties.
Properties è il contenitore del JDK per la configurazione stringa-su-stringa — impostazioni dell'applicazione, sovrascritture di ambiente, messaggi localizzati, parametri di connessione JDBC. Estende Hashtable<Object, Object> (una decisione storica di cui ora ci pentiremo, ma con cui dobbiamo convivere) e aggiunge tre cose in più: un formato di file .properties con lettore e scrittore, un lettore e scrittore XML, e il concetto di un oggetto Properties predefinito consultato quando una chiave non viene trovata localmente.
System.getProperties() restituisce un Properties. Ogni chiamata System.getProperty("user.home") lo attraversa. Una volta conosciuta la classe, la si vede ovunque.
Il contratto: stringhe su entrambi i lati
Sebbene la classe erediti un metodo put(Object, Object) da Hashtable, la sola API sicura è la coppia tipizzata come stringa:
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 fallbackSe si bypassa setProperty e si chiama put("server.port", 8080) con un Integer, l'entry finisce comunque nella tabella, ma si è piazzata una mina: stringPropertyNames() la filtra silenziosamente, e i metodi di scrittura su file (store, storeToXML) lanciano una ClassCastException nel momento in cui tentano di convertire quell'Integer in String. Trattate Properties come Properties<String, String> anche se i generici non lo dicono esplicitamente.
Il formato di file .properties
Testo semplice, orientato alle righe, key=value. Lo spazio attorno a = è consentito. Le righe che iniziano con # o ! sono commenti. Il backslash finale continua il valore sulla riga successiva. I caratteri di escape Unicode (\uXXXX) sono supportati, ma da Java 9 l'overload load(Reader) legge in modo nativo UTF-8, quindi raramente ne avete bisogno.
# server.properties — last edited 2026-05-12
server.host = localhost
server.port = 8080
server.path = /api/v1
greeting = Welcome, \
user!load e store gestiscono questo formato. loadFromXML e storeToXML gestiscono il formato XML equivalente definito da properties.dtd — esiste, a volte utile, quasi mai preferito rispetto alla forma testuale.
Caricamento e salvataggio
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
}Il metodo store scrive un commento con timestamp dopo il commento utente, non ordina nulla (le entry finiscono nell'ordine di iterazione di Hashtable), ed esegue l'escape dei caratteri speciali (=, :, #, spazio iniziale) per il round-tripping. L'output è portabile tra JVM.
Per le risorse incluse nell'applicazione, caricate dal classpath invece che dal filesystem:
try (var in = MyApp.class.getResourceAsStream("/app.properties")) {
config.load(in); // load(InputStream) defaults to ISO-8859-1
}load(InputStream) è l'overload storico e usa ISO-8859-1 (Latin-1) con escape \u. load(Reader) usa qualsiasi charset con cui il reader è stato aperto. Preferite la forma con reader quando controllate la codifica.
Properties predefinite: configurazione a livelli
getProperty(key, default) con due argomenti restituisce un valore di fallback quando la chiave è assente. Il costruttore Properties(Properties defaults) fa la stessa cosa ma a livello di oggetto — il secondo Properties viene consultato quando il primo non contiene la chiave:
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)Questo è il pattern standard per "valori predefiniti distribuiti con l'app, l'utente può sovrascrivere per ambiente." Due livelli è il caso più comune; è possibile concatenarne di più.
Proprietà di System e flag -D
La JVM ha un'istanza globale di Properties accessibile tramite System.getProperties() e System.getProperty(key). Le chiavi standard includono java.version, user.home, user.dir, os.name, file.separator e line.separator. Il flag -Dkey=value sulla riga di comando della JVM lo aggiorna prima che main venga eseguito:
java -Dserver.port=9090 -Dlog.level=DEBUG -jar app.jarString port = System.getProperty("server.port", "8080");Questa è la "configurazione da riga di comando" più semplice che si possa fornire a un programma Java. Per configurazioni più ampie, la convenzione prevede un file .properties distribuito con l'app, unito all'avvio con le proprietà di sistema (che fungono da sovrascritture).
Cosa Properties non è
- Non è una mappa di tipi arbitrari. Solo stringhe. Parsate
Integer.parseInt(config.getProperty("port"))da soli. - Non è gerarchico. Chiavi come
db.primary.hostsono solo stringhe; i punti sono convenzionali, non strutturali. Se serve vera gerarchia, usate una libreria di configurazione YAML/JSON. - Non è thread-safe per operazioni composte. Ogni metodo è sincronizzato (ereditato da
Hashtable), ma check-then-act è ancora soggetto a race condition. Stessa avvertenza della classe padre. - Non è un sostituto di
ResourceBundleper i18n.PropertyResourceBundleè unResourceBundlesupportato da un file.propertiese aggiunge la ricerca per locale; quello è lo strumento giusto per le stringhe tradotte.
Un esempio pratico: caricare i valori predefiniti, sovrascrivere per ambiente, scrivere di nuovo
Il programma seguente costruisce una configurazione a livelli (valori predefiniti dentro il JAR, file di ambiente fuori), legge una proprietà di sistema come sovrascrittura -D, appiattisce il risultato in modo che possa essere riscritto in un buffer .properties per l'ispezione, e dimostra la trappola dei valori non stringa con store.
Una sottigliezza che l'esempio gestisce deliberatamente: store scrive solo le entry proprie di un oggetto Properties — non percorre mai la catena ereditata dei defaults. Quindi per ottenere un file completo e ripristinabile, copiamo ogni chiave risolta in un Properties piatto prima di salvare, invece di chiamare store su un oggetto che si affida al suo genitore di default.
Cosa trarre dall'esecuzione:
- La configurazione a tre livelli (valori predefiniti → file di ambiente → sovrascritture
-D) si risolve correttamente. I valori predefiniti riempiono ciò che nessuno ha sovrascritto; il file di ambiente cambialog.levelefeature.beta; il flag-Dvince perserver.port. storeha prodotto un testo.propertiesportabile con un commento e un timestamp in cima, contenente tutte e quattro le chiavi risolte. Si potrebbe passare direttamente quel file aloade ottenere la stessa mappa — perché abbiamo prima appiattito i livelli.setProperty("age", 30)non compilerebbe (richiede unaString).put("age", 30)compila, l'entry finisce nella tabella, estringPropertyNamesla filtra — mastorenon la salta silenziosamente: lancia unaClassCastExceptionnon appena tenta di convertire l'IntegerinString. La lezione: non usate maiputcon un valore non stringa su unProperties— usate sempresetProperty.
Cosa c'è dopo
Properties è stato l'ultimo capitolo sulle "strutture dati" in questa parte. I capitoli rimanenti riguardano le operazioni sulle collezioni: scorrerle (Iterators e ListIterator), confrontare gli elementi (Comparable and Comparator), e le utility statiche per l'ordinamento, la ricerca e la composizione (la classe Collections). Il prossimo capitolo inizia con le fondamenta — l'interfaccia Iterator che ogni ciclo for-each usa segretamente.