Перейти к основному содержимому

Преобразование свойств Java в HashMap

· 5 мин. чтения

1. Введение

Многие разработчики решают хранить параметры приложения вне исходного кода. Один из способов сделать это в Java — использовать внешний файл конфигурации и читать его через класс java.util.Properties .

В этом руководстве мы сосредоточимся на различных подходах к преобразованию java.util.Properties в HashMap<String, String> . Мы будем реализовывать различные методы для достижения нашей цели, используя обычную Java, lambdas или внешние библиотеки. На примерах мы обсудим плюсы и минусы каждого решения.

2. Конструктор HashMap

Прежде чем мы реализуем наш первый код, давайте проверим Javadoc для java.util.Properties . Как мы видим, этот служебный класс наследуется от Hashtable<Object, Object> , который также реализует интерфейс Map . Более того, Java оборачивает свои классы Reader и Writer для работы непосредственно со строковыми значениями.

В соответствии с этой информацией мы можем преобразовать свойства в HashMap<String, String>, используя приведение типов и вызовы конструктора.

Предполагая, что мы правильно загрузили наши свойства , мы можем реализовать:

public static HashMap<String, String> typeCastConvert(Properties prop) {
Map step1 = prop;
Map<String, String> step2 = (Map<String, String>) step1;
return new HashMap<>(step2);
}

Здесь мы реализуем наше преобразование в три простых шага.

Во-первых, в соответствии с графом наследования нам нужно преобразовать наши свойства в необработанную карту . Это действие вызовет первое предупреждение компилятора, которое можно отключить с помощью аннотации @SuppressWarnings(“rawtypes”) .

После этого мы приводим нашу необработанную карту в Map<String, String> , вызывая еще одно предупреждение компилятора, которое можно опустить, используя @SupressWarnings("unchecked") .

Наконец, мы создаем нашу HashMap с помощью конструктора копирования . Это самый быстрый способ конвертировать наши свойства , но у этого решения также есть большой недостаток, связанный с безопасностью типов : наши свойства могут быть скомпрометированы и изменены до преобразования.

Согласно документации, класс Properties имеет методы setProperty() и getProperty() , которые заставляют использовать значения String . Но также есть методы put() и putAll() , унаследованные от Hashtable , которые позволяют использовать любой тип в качестве ключей или значений в наших свойствах :

properties.put("property4", 456);
properties.put(5, 10.11);

HashMap<String, String> hMap = typeCastConvert(properties);
assertThrows(ClassCastException.class, () -> {
String s = hMap.get("property4");
});
assertEquals(Integer.class, ((Object) hMap.get("property4")).getClass());

assertThrows(ClassCastException.class, () -> {
String s = hMap.get(5);
});
assertEquals(Double.class, ((Object) hMap.get(5)).getClass());

Как мы видим, наше преобразование выполняется без ошибок, но не все элементы в HashMap являются строками . Таким образом, даже если этот метод выглядит самым простым, мы должны помнить о некоторых проверках, связанных с безопасностью, в будущем.

3. API гуавы

Если мы можем использовать сторонние библиотеки, API Google Guava пригодится. Эта библиотека предоставляет статический метод Maps.fromProperties() , который делает почти все за нас. Согласно документации, этот вызов возвращает ImmutableMap , поэтому, если мы хотим получить HashMap, мы можем использовать:

public HashMap<String, String> guavaConvert(Properties prop) {
return Maps.newHashMap(Maps.fromProperties(prop));
}

Как и ранее, этот метод отлично работает, когда мы полностью уверены, что свойства содержат только строковые значения. Наличие некоторых несоответствующих значений приведет к неожиданному поведению:

properties.put("property4", 456);
assertThrows(NullPointerException.class,
() -> PropertiesToHashMapConverter.guavaConvert(properties));

properties.put(5, 10.11);
assertThrows(ClassCastException.class,
() -> PropertiesToHashMapConverter.guavaConvert(properties));

API Guava не выполняет никаких дополнительных сопоставлений. В результате это не позволяет нам преобразовывать эти свойства , вызывая исключения.

В первом случае возникает исключение NullPointerException из-за значения Integer , которое невозможно получить с помощью свойств. getProperty() и в результате интерпретируется как null . Во втором примере создается исключение ClassCastException, связанное с нестроковым ключом, встречающимся на карте входных свойств.

Это решение дает нам лучший контроль типов, а также информирует нас о нарушениях , которые происходят в процессе преобразования.

4. Реализация пользовательского типа безопасности

Пришло время решить проблемы безопасности нашего типа из предыдущих примеров. Как мы знаем, класс Properties реализует методы, унаследованные от интерфейса Map . Мы будем использовать один из возможных способов перебора карты , чтобы реализовать правильное решение и обогатить его проверками типов.

4.1. Итерация с использованием цикла for

Давайте реализуем простой цикл for :

public HashMap<String, String> loopConvert(Properties prop) {
HashMap<String, String> retMap = new HashMap<>();
for (Map.Entry<Object, Object> entry : prop.entrySet()) {
retMap.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue()));
}
return retMap;
}

В этом методе мы перебираем свойства так же, как и для обычной карты . В результате у нас есть доступ один за другим к каждому отдельному значению пары ключей, представленному классом Map.Entry .

Прежде чем помещать значения в возвращенный HashMap , мы можем выполнить дополнительные проверки, поэтому решили использовать метод String.valueOf() .

4.2. API стримов и коллекторов

Мы даже можем реорганизовать наш метод, используя современный способ Java 8:

public HashMap<String, String> streamConvert(Properties prop) {
return prop.entrySet().stream().collect(
Collectors.toMap(
e -> String.valueOf(e.getKey()),
e -> String.valueOf(e.getValue()),
(prev, next) -> next, HashMap::new
));
}

В этом случае мы используем сборщики потоков Java 8 без явного построения HashMap . Этот метод реализует точно такую же логику, как и в предыдущем примере.

Оба решения немного сложнее, потому что они требуют некоторой пользовательской реализации , которой нет в примерах приведения типов и Guava.

Однако у нас есть доступ к значениям до помещения их в результирующий HashMap , поэтому мы можем реализовать дополнительные проверки или сопоставления :

properties.put("property4", 456);
properties.put(5, 10.11);

HashMap<String, String> hMap1 = loopConvert(properties);
HashMap<String, String> hMap2 = streamConvert(properties);

assertDoesNotThrow(() -> {
String s1 = hMap1.get("property4");
String s2 = hMap2.get("property4");
});
assertEquals("456", hMap1.get("property4"));
assertEquals("456", hMap2.get("property4"));

assertDoesNotThrow(() -> {
String s1 = hMap1.get("property4");
String s2 = hMap2.get("property4");
});
assertEquals("10.11", hMap1.get("5"));
assertEquals("10.11", hMap2.get("5"));

assertEquals(hMap2, hMap1);

Как видим, мы решили наши проблемы, связанные с нестроковыми значениями. Используя этот подход, мы можем вручную настроить логику сопоставления для достижения надлежащей реализации.

5. Вывод

В этом руководстве мы проверили различные подходы к преобразованию java.util.Properties в HashMap<String, String> .

Мы начали с решения для приведения типов, которое, возможно, является самым быстрым преобразованием, но также приводит к предупреждениям компилятора и потенциальным ошибкам безопасности типов .

Затем мы рассмотрели решение с использованием API Guava, которое разрешает предупреждения компилятора и вносит некоторые улучшения в обработку ошибок.

Наконец, мы реализовали собственные методы, которые обрабатывают ошибки безопасности типов и дают нам максимальный контроль.

Все фрагменты кода из этого руководства доступны на GitHub .