Clase Properties de Java
Carga y almacena pares clave-valor de cadenas en Java con la clase Properties, incluyendo archivos .properties.
Properties es el contenedor del JDK para la configuración de cadena a cadena — ajustes de aplicación, anulaciones de entorno, mensajes localizados, parámetros de conexión JDBC. Extiende Hashtable<Object, Object> (una decisión histórica que ahora lamentamos, pero con la que convivimos) y agrega tres cosas encima: un formato de archivo .properties con lector y escritor, un lector y escritor XML, y el concepto de un objeto predeterminado de propiedades que se consulta cuando una clave no se encuentra localmente.
System.getProperties() devuelve un Properties. Cada llamada a System.getProperty("user.home") pasa por él. Una vez que conoces la clase, la ves en todas partes.
El contrato: cadenas en ambos lados
Aunque la clase hereda un método put(Object, Object) de Hashtable, la única API segura es el par tipado con cadenas:
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 fallbackSi evitas setProperty y llamas a put("server.port", 8080) con un Integer, la entrada aún cae en la tabla, pero has plantado una mina: stringPropertyNames() la filtra silenciosamente, y los métodos de escritura de archivos (store, storeToXML) lanzan ClassCastException en el momento en que intentan convertir ese Integer a String. Trata Properties como Properties<String, String> aunque los genéricos no lo digan.
El formato de archivo .properties
Texto plano, orientado a líneas, key=value. Se permiten espacios alrededor de =. Las líneas que comienzan con # o ! son comentarios. La barra invertida al final continúa el valor en la siguiente línea. Las secuencias de escape Unicode (\uXXXX) son compatibles, pero desde Java 9 la sobrecarga load(Reader) lee UTF-8 de forma nativa, por lo que raramente se necesitan.
# server.properties — last edited 2026-05-12
server.host = localhost
server.port = 8080
server.path = /api/v1
greeting = Welcome, \
user!load y store trabajan con este formato. loadFromXML y storeToXML trabajan con el formato XML equivalente definido por properties.dtd — existe, a veces es útil, pero casi nunca se prefiere sobre el formato de texto.
Carga y almacenamiento
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
}El método store escribe un comentario con marca de tiempo después del comentario del usuario, no ordena nada (las entradas llegan en orden de iteración de Hashtable) y escapa los caracteres especiales (=, :, #, espacios al inicio) para el ida y vuelta. La salida es portable entre JVM.
Para recursos incluidos con tu aplicación, carga desde el classpath en lugar del sistema de archivos:
try (var in = MyApp.class.getResourceAsStream("/app.properties")) {
config.load(in); // load(InputStream) defaults to ISO-8859-1
}load(InputStream) es la sobrecarga histórica y usa ISO-8859-1 (Latin-1) con escapes \u. load(Reader) usa el conjunto de caracteres con el que se abrió el lector. Prefiere la forma de lector cuando controlas la codificación.
Propiedades predeterminadas: configuración por capas
El getProperty(key, default) de dos argumentos devuelve un valor de reserva cuando la clave está ausente. El constructor Properties(Properties defaults) hace lo mismo pero a nivel de objeto — el segundo Properties se consulta cuando el primero no contiene la clave:
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)Ese es el patrón estándar para "valores predeterminados incluidos con la aplicación, el usuario puede anular por entorno". Dos capas es el caso más común; puedes encadenar más.
Propiedades de System y flags -D
La JVM tiene una instancia global de Properties accesible mediante System.getProperties() y System.getProperty(key). Las claves estándar incluyen java.version, user.home, user.dir, os.name, file.separator y line.separator. El flag -Dkey=value en la línea de comandos de la JVM lo añade antes de que se ejecute main:
java -Dserver.port=9090 -Dlog.level=DEBUG -jar app.jarString port = System.getProperty("server.port", "8080");Esta es la "configuración por línea de comandos" más sencilla que puedes darle a un programa Java. Para configuraciones más grandes, la convención es un archivo .properties incluido con la aplicación, combinado al inicio con las propiedades del sistema (que actúan como anulaciones).
Lo que Properties no es
- No es un mapa de tipos arbitrarios. Solo cadenas. Analiza
Integer.parseInt(config.getProperty("port"))por tu cuenta. - No es jerárquico. Las claves como
db.primary.hostson solo cadenas; los puntos son convencionales, no estructurales. Si necesitas jerarquía real, usa una biblioteca de configuración YAML/JSON. - No es seguro para hilos en operaciones compuestas. Cada método está sincronizado (heredado de
Hashtable), pero las operaciones de verificar-luego-actuar siguen siendo propensas a condiciones de carrera. La misma advertencia que la clase padre. - No es un reemplazo de
ResourceBundlepara i18n.PropertyResourceBundlees unResourceBundlerespaldado por un archivo.propertiesy añade búsqueda de configuración regional; esa es la herramienta correcta para cadenas traducidas.
Un ejemplo práctico: cargar valores predeterminados, anular por entorno, escribir de vuelta
El programa a continuación construye una configuración por capas (valores predeterminados dentro del JAR, archivo de entorno fuera), lee una propiedad del sistema para actuar como una anulación -D, aplana el resultado para que pueda escribirse de vuelta a un buffer .properties para inspección, y demuestra la trampa de store con valores no string.
Una sutileza que el ejemplo maneja deliberadamente: store escribe solo las entradas propias de un objeto Properties — nunca recorre la cadena de defaults heredada. Por eso, para obtener un archivo completo y procesable en ida y vuelta, copiamos cada clave resuelta en un Properties plano antes de almacenar, en lugar de llamar a store en un objeto que depende de su defaults padre.
Qué extraer de la ejecución:
- La configuración de tres capas (valores predeterminados → archivo de entorno → anulaciones
-D) se resuelve correctamente. Los valores predeterminados completan lo que nadie anuló; el archivo de entorno cambialog.levelyfeature.beta; el flag-Dgana paraserver.port. storeprodujo un texto.propertiesportable con un comentario y una marca de tiempo en la parte superior, con las cuatro claves resueltas. Podrías pasar ese archivo directamente aloady obtener el mismo mapa — porque aplanamos las capas primero.setProperty("age", 30)no compilaría (requiere unString).put("age", 30)sí compila, la entrada cae en la tabla, ystringPropertyNamesla filtra — perostoreno la omite silenciosamente: lanzaClassCastExceptionen cuanto intenta convertir elIntegeraString. La lección: nunca usesputcon un valor no string en unProperties— usa siempresetProperty.
Qué sigue
Properties fue el último capítulo de "estructuras de datos" en esta parte. Los capítulos restantes tratan sobre operaciones en colecciones: recorrerlas (Iterators y ListIterator), comparar elementos (Comparable and Comparator), y las utilidades estáticas para ordenar, buscar y envolver (the Collections class). El próximo capítulo comienza con la base — la interfaz Iterator que cada bucle for-each usa en secreto.