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 .