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

Чтение файла в карту в Java

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

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Обзор

Мы знаем, что Map содержит пары ключ-значение в Java. Иногда нам может понадобиться загрузить содержимое текстового файла и преобразовать его в карту Java .

В этом кратком руководстве давайте рассмотрим, как мы можем этого добиться.

2. Введение в проблему

Поскольку Map хранит записи ключ-значение, файл должен соответствовать определенному формату, если мы хотим импортировать содержимое файла в объект Java Map .

Пример файла может быстро объяснить это:

$ cat theLordOfRings.txt
title:The Lord of the Rings: The Return of the King
director:Peter Jackson
actor:Sean Astin
actor:Ian McKellen
Gandalf and Aragorn lead the World of Men against Sauron's
army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.

Как мы видим в файле theLordOfRings.txt , если мы рассматриваем символ двоеточия в качестве разделителя, большинство строк следуют шаблону « КЛЮЧ:ЗНАЧЕНИЕ », например « директор: Питер Джексон ».

Следовательно, мы можем прочитать каждую строку, разобрать ключ и значение и поместить их в объект Map .

Тем не менее, есть некоторые особые случаи, о которых нам нужно позаботиться:

  • Значения, содержащие разделитель — значение не должно быть усечено. Например, первая строка « заголовок: Властелин колец: Возвращение… »
  • Дублированные ключи — три стратегии: перезапись существующего, отбрасывание последнего и объединение значений в список в зависимости от требований. Например, у нас есть два ключа « актер » в файле.
  • Строки, которые не соответствуют шаблону « КЛЮЧ:ЗНАЧЕНИЕ ». Строка должна быть пропущена. Например, посмотрите последние две строки в файле.

Далее давайте прочитаем этот файл и сохраним его в объекте карты Java.

3. Перечисление DupKeyOption

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

Более того, если мы воспользуемся опцией перезаписи или сброса, у нас будет возвращенная Карта типа Map<String, String> . Однако, если мы хотим агрегировать значения для повторяющихся ключей, мы получим результат как Map<String, List<String>> .

Итак, давайте сначала рассмотрим сценарии перезаписи и удаления. В конце мы обсудим вариант агрегирования в отдельном разделе.

Чтобы сделать наше решение гибким, давайте создадим класс перечисления , чтобы мы могли передавать параметр в качестве параметра нашим методам решения:

enum DupKeyOption {
OVERWRITE, DISCARD
}

4. Использование классов BufferedReader и FileReader

Мы можем комбинировать BufferedReader и FileReader для чтения содержимого из файла построчно .

4.1. Создание метода byBufferedReader

Создадим метод на основе BufferedReader и FileReader :

public static Map<String, String> byBufferedReader(String filePath, DupKeyOption dupKeyOption) {
HashMap<String, String> map = new HashMap<>();
String line;
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
while ((line = reader.readLine()) != null) {
String[] keyValuePair = line.split(":", 2);
if (keyValuePair.length > 1) {
String key = keyValuePair[0];
String value = keyValuePair[1];
if (DupKeyOption.OVERWRITE == dupKeyOption) {
map.put(key, value);
} else if (DupKeyOption.DISCARD == dupKeyOption) {
map.putIfAbsent(key, value);
}
} else {
System.out.println("No Key:Value found in line, ignoring: " + line);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return map;
}

Метод byBufferedReader принимает два параметра: путь к входному файлу и объект dupKeyOption , который определяет, как обрабатывать записи с повторяющимися ключами.

Как видно из приведенного выше кода, мы определили объект BufferedReader для чтения строк из заданного входного файла. Затем мы анализируем и обрабатываем каждую строку в цикле while . Давайте пройдемся и поймем, как это работает:

  • Мы создаем объект BufferedReader и используем try-with-resources , чтобы гарантировать автоматическое закрытие объекта чтения .
  • Мы используем метод разделения с параметром limit, чтобы сохранить часть значения как есть, если она содержит символы двоеточия.
  • Затем проверка if отфильтровывает строку, которая не соответствует шаблону « KEY:VALUE ».
  • В случае наличия повторяющихся ключей, если мы хотим использовать стратегию «перезаписи», мы можем просто вызвать map.put(key, value)
  • В противном случае вызов метода putIfAbsent позволяет игнорировать последние приходящие записи с дублирующимися ключами.

Далее давайте проверим, работает ли метод должным образом.

4.2. Тестирование решения

Прежде чем мы напишем соответствующий тестовый метод, давайте инициализируем два объекта карты, содержащие ожидаемые записи:

private static final Map<String, String> EXPECTED_MAP_DISCARD = Stream.of(new String[][]{
{"title", "The Lord of the Rings: The Return of the King"},
{"director", "Peter Jackson"},
{"actor", "Sean Astin"}
}).collect(Collectors.toMap(data -> data[0], data -> data[1]));

private static final Map<String, String> EXPECTED_MAP_OVERWRITE = Stream.of(new String[][]{
...
{"actor", "Ian McKellen"}
}).collect(Collectors.toMap(data -> data[0], data -> data[1]));

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

Затем давайте проверим наш метод, чтобы увидеть, можем ли мы получить ожидаемые объекты Map :

@Test
public void givenInputFile_whenInvokeByBufferedReader_shouldGetExpectedMap() {
Map<String, String> mapOverwrite = FileToHashMap.byBufferedReader(filePath, FileToHashMap.DupKeyOption.OVERWRITE);
assertThat(mapOverwrite).isEqualTo(EXPECTED_MAP_OVERWRITE);

Map<String, String> mapDiscard = FileToHashMap.byBufferedReader(filePath, FileToHashMap.DupKeyOption.DISCARD);
assertThat(mapDiscard).isEqualTo(EXPECTED_MAP_DISCARD);
}

Если мы попробуем, тест пройден. Итак, мы решили проблему.

5. Использование Java -потока

Stream существует с Java 8. Кроме того, метод Files.lines может удобно возвращать объект Stream , содержащий все строки в файле .

Теперь давайте создадим мод с использованием Stream для решения проблемы:

public static Map<String, String> byStream(String filePath, DupKeyOption dupKeyOption) {
Map<String, String> map = new HashMap<>();
try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
lines.filter(line -> line.contains(":"))
.forEach(line -> {
String[] keyValuePair = line.split(":", 2);
String key = keyValuePair[0];
String value = keyValuePair[1];
if (DupKeyOption.OVERWRITE == dupKeyOption) {
map.put(key, value);
} else if (DupKeyOption.DISCARD == dupKeyOption) {
map.putIfAbsent(key, value);
}
});
} catch (IOException e) {
e.printStackTrace();
}
return map;
}

Как видно из приведенного выше кода, основная логика очень похожа на наш метод byBufferedReader . Пройдемся быстро:

  • Мы по-прежнему используем попытку с ресурсами для объекта Stream , поскольку объект Stream содержит ссылку на открытый файл. Мы должны закрыть файл, закрыв поток.
  • Метод filter пропускает все строки, которые не соответствуют шаблону « KEY:VALUE ».
  • Метод forEach практически аналогичен блоку while в решении byBufferedReader .

Наконец, давайте протестируем решение byStream :

@Test
public void givenInputFile_whenInvokeByStream_shouldGetExpectedMap() {
Map<String, String> mapOverwrite = FileToHashMap.byStream(filePath, FileToHashMap.DupKeyOption.OVERWRITE);
assertThat(mapOverwrite).isEqualTo(EXPECTED_MAP_OVERWRITE);

Map<String, String> mapDiscard = FileToHashMap.byStream(filePath, FileToHashMap.DupKeyOption.DISCARD);
assertThat(mapDiscard).isEqualTo(EXPECTED_MAP_DISCARD);
}

Когда мы выполняем тест, он также проходит.

6. Агрегирование значений по ключам

До сих пор мы видели решения для сценариев перезаписи и удаления. Но, как мы уже обсуждали, если это необходимо, мы также можем агрегировать значения по ключам. Таким образом, в итоге у нас будет объект Map типа Map<String, List<String>> . Теперь давайте создадим метод для реализации этого требования:

public static Map<String, List<String>> aggregateByKeys(String filePath) {
Map<String, List<String>> map = new HashMap<>();
try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
lines.filter(line -> line.contains(":"))
.forEach(line -> {
String[] keyValuePair = line.split(":", 2);
String key = keyValuePair[0];
String value = keyValuePair[1];
if (map.containsKey(key)) {
map.get(key).add(value);
} else {
map.put(key, Stream.of(value).collect(Collectors.toList()));
}
});
} catch (IOException e) {
e.printStackTrace();
}
return map;
}

Мы использовали подход Stream для чтения всех строк во входном файле. Реализация довольно проста. После того, как мы проанализировали ключ и значение из строки ввода, мы проверяем, существует ли уже ключ в объекте карты результатов. Если он существует, мы добавляем значение в существующий список. В противном случае мы инициализируем список , содержащий текущее значение, как один элемент: Stream.of(value).collect(Collectors.toList()).

Стоит отметить, что мы не должны инициализировать список с помощью Collections.singletonList(value) или List.of(value) . Это связано с тем, что оба метода Collections.singletonList и List.of (Java 9+) возвращают неизменяемый List . `` То есть, если тот же ключ приходит снова, мы не можем добавить значение в список.

Далее, давайте проверим наш метод, чтобы увидеть, работает ли он. Как обычно, сначала мы создаем ожидаемый результат:

private static final Map<String, List<String>> EXPECTED_MAP_AGGREGATE = Stream.of(new String[][]{
{"title", "The Lord of the Rings: The Return of the King"},
{"director", "Peter Jackson"},
{"actor", "Sean Astin", "Ian McKellen"}
}).collect(Collectors.toMap(arr -> arr[0], arr -> Arrays.asList(Arrays.copyOfRange(arr, 1, arr.length))));

Тогда сам метод тестирования довольно прост:

@Test
public void givenInputFile_whenInvokeAggregateByKeys_shouldGetExpectedMap() {
Map<String, List<String>> mapAgg = FileToHashMap.aggregateByKeys(filePath);
assertThat(mapAgg).isEqualTo(EXPECTED_MAP_AGGREGATE);
}

Тест проходит, если мы даем ему прогон. Это означает, что наше решение работает так, как ожидалось.

7. Заключение

В этой статье мы изучили два подхода к чтению содержимого из текстового файла и сохранению его в объекте карты Java: использование класса BufferedReader и использование Stream .

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

Как всегда, полная версия кода доступна на GitHub .