Класс Properties в Java
Загрузка и хранение пар ключ-значение в Java с помощью класса Properties, включая файлы .properties.
Properties — это контейнер JDK для строковой конфигурации — настроек приложения, переопределений среды, локализованных сообщений, параметров подключения JDBC. Он расширяет Hashtable<Object, Object> (историческое решение, о котором сейчас сожалеют, но которое уже не изменить) и добавляет три вещи сверху: формат файла .properties с читателем и писателем, XML-читатель и XML-писатель, а также концепцию объекта свойств по умолчанию, который используется, когда ключ не найден локально.
System.getProperties() возвращает Properties. Каждый вызов System.getProperty("user.home") проходит через него. Как только вы познакомитесь с этим классом, вы будете встречать его повсюду.
Контракт: строки с обеих сторон
Хотя класс наследует метод put(Object, Object) от Hashtable, единственным безопасным API является пара с типом string:
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Если вы обойдёте setProperty и вызовете put("server.port", 8080) с Integer, запись всё равно попадёт в таблицу, но вы заложите мину: stringPropertyNames() молча отфильтрует её, а методы записи файлов (store, storeToXML) выбросят ClassCastException, как только попытаются привести этот Integer к String. Относитесь к Properties как к Properties<String, String>, даже если generics этого не говорят.
Формат файла .properties
Обычный текст, построчный формат, key=value. Пробелы вокруг = допустимы. Строки, начинающиеся с # или !, являются комментариями. Обратная косая черта в конце строки продолжает значение на следующей строке. Поддерживаются Unicode-экранирования (\uXXXX), но начиная с Java 9 перегрузка load(Reader) читает UTF-8 нативно, поэтому они редко нужны.
# server.properties — last edited 2026-05-12
server.host = localhost
server.port = 8080
server.path = /api/v1
greeting = Welcome, \
user!load и store работают с этим форматом. loadFromXML и storeToXML работают с эквивалентным XML-форматом, определённым в properties.dtd — он существует, иногда полезен, но почти никогда не предпочтителен перед текстовой формой.
Загрузка и сохранение
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
}Метод store записывает комментарий с временной меткой после пользовательского комментария, ничего не сортирует (записи попадают в порядке итерации Hashtable) и экранирует специальные символы (=, :, #, начальные пробелы) для правильного восстановления. Вывод переносим между JVM.
Для ресурсов, включённых в ваше приложение, загружайте из classpath вместо файловой системы:
try (var in = MyApp.class.getResourceAsStream("/app.properties")) {
config.load(in); // load(InputStream) defaults to ISO-8859-1
}load(InputStream) — историческая перегрузка, использующая ISO-8859-1 (Latin-1) с \u-экранированиями. load(Reader) использует кодировку, с которой был открыт читатель. Предпочитайте форму с читателем, когда контролируете кодировку.
Свойства по умолчанию: многоуровневая конфигурация
Двухаргументный getProperty(key, default) возвращает запасное значение, когда ключ отсутствует. Конструктор Properties(Properties defaults) делает то же самое, но на уровне объекта — второй Properties используется, когда первый не содержит ключ:
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)Это стандартный паттерн для "умолчаний, поставляемых с приложением, которые пользователь может переопределить для каждой среды". Два уровня — наиболее распространённый случай; можно строить цепочки длиннее.
Свойства System и флаги -D
JVM имеет глобальный экземпляр Properties, доступный через System.getProperties() и System.getProperty(key). Стандартные ключи включают java.version, user.home, user.dir, os.name, file.separator и line.separator. Флаг -Dkey=value в командной строке JVM добавляет значения до запуска main:
java -Dserver.port=9090 -Dlog.level=DEBUG -jar app.jarString port = System.getProperty("server.port", "8080");Это простейшая "командная строчная конфигурация", которую можно дать Java-программе. Для более крупных конфигураций принято использовать файл .properties, поставляемый с приложением и объединяемый при запуске со свойствами системы (которые действуют как переопределения).
Чем Properties не является
- Не map произвольных типов. Только строки. Разбирайте
Integer.parseInt(config.getProperty("port"))самостоятельно. - Не иерархический. Ключи вроде
db.primary.host— просто строки; точки — это соглашение, а не структура. Если нужна настоящая иерархия, используйте библиотеку конфигурации YAML/JSON. - Не потокобезопасный для составных операций. Каждый метод синхронизирован (унаследован от
Hashtable), но операции "проверить, затем выполнить" всё равно могут гонять потоки. Та же оговорка, что и у родительского класса. - Не замена
ResourceBundleдля i18n.PropertyResourceBundle— этоResourceBundle, основанный на файле.properties, который добавляет поиск по локали; это правильный инструмент для переведённых строк.
Рабочий пример: загрузка умолчаний, переопределение по среде, запись обратно
Программа ниже строит многоуровневую конфигурацию (умолчания внутри JAR, файл среды снаружи), читает системное свойство как переопределение -D, сглаживает результат для записи обратно в буфер .properties для проверки и демонстрирует ловушку с нестроковым store.
Одна тонкость, которую пример намеренно обрабатывает: store записывает только собственные записи объекта Properties — он никогда не обходит унаследованную цепочку defaults. Поэтому, чтобы получить полный, переносимый файл, мы копируем каждый разрешённый ключ в плоский Properties перед сохранением, а не вызываем store на объекте, который опирается на родителя defaults.
Что можно вынести из результата выполнения:
- Трёхуровневая конфигурация (умолчания → файл среды → переопределения
-D) разрешается правильно. Умолчания заполняют то, что никто не переопределил; файл среды изменяетlog.levelиfeature.beta; флаг-Dпобеждает дляserver.port. storeсоздал переносимый текст.propertiesс комментарием и временной меткой вверху, содержащий все четыре разрешённых ключа. Вы могли бы подать этот файл обратно вloadи получить ту же карту — потому что мы предварительно сгладили уровни.setProperty("age", 30)не скомпилируется (требуетсяString).put("age", 30)компилируется, запись попадает в таблицу, иstringPropertyNamesфильтрует её — ноstoreне пропускает её молча: он выбрасываетClassCastException, как только пытается привестиIntegerкString. Урок: никогда не используйтеputдля нестрок вProperties— всегда используйтеsetProperty.
Что дальше
Properties был последней главой о "структурах данных" в этой части. Оставшиеся главы посвящены операциям над коллекциями: обход (Iterators и ListIterator), сравнение элементов (Comparable и Comparator) и статические утилиты для сортировки, поиска и обёртывания (класс Collections). Следующая глава начинается с основ — интерфейса Iterator, который тайно использует каждый цикл for-each.