Статьи

Разбор файла с помощью Stream API в Java 8

Потоки повсюду в Java 8. Просто посмотрите вокруг, и вы наверняка найдете их. Это также относится к java.io.BufferedReader . Парсинг файла в Java 8 с Stream API чрезвычайно прост.

У меня есть файл CSV, который я хочу прочитать. Пример ниже:

1
2
3
username;visited
jdoe;10
kolorobot;4

Контракт для моего читателя заключается в предоставлении заголовка в виде списка строк и всех записей в виде списка строк. Мой читатель принимает java.io.Reader в качестве источника для чтения.

Начну с чтения шапки. Алгоритм чтения заголовка выглядит следующим образом:

  • Откройте источник для чтения,
  • Получить первую строку и разобрать ее,
  • Разделить линию разделителем,
  • Получить первую строку и разобрать ее,
  • Преобразовать строку в список строк и вернуть.

И реализация:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
class CsvReader {
 
    private static final String SEPARATOR = ";";
 
    private final Reader source;
 
    CsvReader(Reader source) {
        this(source);
    }
    List<String> readHeader() {
        try (BufferedReader reader = new BufferedReader(source)) {
            return reader.lines()
                    .findFirst()
                    .map(line -> Arrays.asList(line.split(SEPARATOR)))
                    .get();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }   
}

Довольно просто. Интуитивно понятный. Точно так же я создал метод для чтения всех записей. Алгоритм чтения записей выглядит следующим образом:

  • Откройте источник для чтения,
  • Пропустить первую строку,
  • Разделить линию разделителем,
  • Примените маппер к каждой строке, которая отображает строку в список строк.

И реализация:

01
02
03
04
05
06
07
08
09
10
11
12
13
class CsvReader {
 
    List<List<String>> readRecords() {
        try (BufferedReader reader = new BufferedReader(source)) {
            return reader.lines()
                    .substream(1)
                    .map(line -> Arrays.asList(line.split(separator)))
                    .collect(Collectors.toList());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    
}

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

1
2
Function<String, List<String>> mapper
    = line -> Arrays.asList(line.split(separator));

Чтобы закончить, я создал простой тест.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CsvReaderTest {
 
    @Test
    public void readsHeader() {
        CsvReader csvReader = createCsvReader();
        List<String> header = csvReader.readHeader();
        assertThat(header)
                .contains("username")
                .contains("visited")
                .hasSize(2);
    }
 
    @Test
    public void readsRecords() {
        CsvReader csvReader = createCsvReader();
        List<List<String>> records = csvReader.readRecords();
        assertThat(records)
                .contains(Arrays.asList("jdoe", "10"))
                .contains(Arrays.asList("kolorobot", "4"))
                .hasSize(2);
    }
 
    private CsvReader createCsvReader() {
        try {
            Path path = Paths.get("src/test/resources", "sample.csv");
            Reader reader = Files.newBufferedReader(
                path, Charset.forName("UTF-8"));
            return new CsvReader(reader);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}