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

Руководство по BufferedReader

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

Задача: Медиана двух отсортированных массивов

Даны два отсортированных массива размерами n и m. Найдите медиану слияния этих двух массивов.
Временная сложность решения должна быть O(log(m + n)) ...

ANDROMEDA

1. Обзор

BufferedReader — это класс, упрощающий чтение текста из потока ввода символов. Он буферизует символы, чтобы обеспечить эффективное чтение текстовых данных.

В этом уроке мы рассмотрим, как использовать класс BufferedReader .

2. Когда использовать BufferedReader

В общем, BufferedReader пригодится, если мы хотим прочитать текст из любого источника ввода, будь то файлы, сокеты или что-то еще.

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

2.1. Буферизация другого читателя

Как и большинство классов ввода-вывода Java, BufferedReader реализует шаблон Decorator, что означает, что он ожидает Reader в своем конструкторе. Таким образом, это позволяет нам гибко расширять экземпляр реализации Reader с помощью функций буферизации: `` ** ** ``

BufferedReader reader = 
new BufferedReader(new FileReader("src/main/resources/input.txt"));

Но если буферизация для нас не имеет значения, мы можем просто использовать FileReader напрямую:

FileReader reader = 
new FileReader("src/main/resources/input.txt");

Помимо буферизации, BufferedReader также предоставляет несколько полезных вспомогательных функций для построчного чтения файлов . Таким образом, даже если использование FileReader напрямую может показаться проще, BufferedReader может оказаться большим подспорьем.

2.2. Буферизация потока

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

BufferedReader reader = 
new BufferedReader(new InputStreamReader(System.in));

В приведенном выше примере мы читаем из System.in , что обычно соответствует вводу с клавиатуры. Точно так же мы можем передать входной поток для чтения из сокета, файла или любого вообразимого типа текстового ввода. Единственным предварительным условием является наличие подходящей реализации InputStream для него.

2.3. BufferedReader против сканера

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

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

  • BufferedReader синхронизирован (потокобезопасный), а Scanner — нет.
  • Сканер может анализировать примитивные типы и строки с помощью регулярных выражений.
  • BufferedReader позволяет изменять размер буфера, в то время как Scanner имеет фиксированный размер буфера.
  • BufferedReader имеет больший размер буфера по умолчанию.
  • Scanner скрывает IOException , а BufferedReader заставляет нас его обрабатывать
  • BufferedReader обычно быстрее, чем Scanner , потому что он только читает данные, не анализируя их.

Имея это в виду, если мы анализируем отдельные токены в файле, Scanner будет чувствовать себя немного более естественным, чем BufferedReader. Но простое чтение строк за раз — это то, где сияет BufferedReader .

При необходимости у нас также есть руководство по сканеру .

3. Чтение текста с помощью BufferedReader

Давайте пройдемся по всему процессу создания, использования и уничтожения BufferReader правильно для чтения из текстового файла.

3.1. Инициализация BufferedReader

Во- первых, давайте создадим BufferedReader , используя его конструктор BufferedReader(Reader) :

BufferedReader reader = 
new BufferedReader(new FileReader("src/main/resources/input.txt"));

Такая упаковка FileReader — хороший способ добавить буферизацию в качестве аспекта для других программ чтения.

По умолчанию будет использоваться буфер размером 8 КБ. Однако, если мы хотим буферизовать меньшие или большие блоки, мы можем использовать конструктор BufferedReader(Reader, int) :

BufferedReader reader = 
new BufferedReader(new FileReader("src/main/resources/input.txt")), 16384);

Это установит размер буфера в 16384 байта (16 КБ).

Оптимальный размер буфера зависит от таких факторов, как тип входного потока и оборудование, на котором выполняется код. По этой причине, чтобы добиться идеального размера буфера, мы должны найти его сами, экспериментируя.

Лучше всего использовать степень двойки в качестве размера буфера, так как большинство аппаратных устройств имеют степень двойки в качестве размера блока.

Наконец, есть еще один удобный способ создать BufferedReader с помощью вспомогательного класса Files из API java.nio : ``

BufferedReader reader = 
Files.newBufferedReader(Paths.get("src/main/resources/input.txt"))

Создание его `таким образом — хороший способ буферизации, если мы хотим прочитать файл, потому что нам не нужно сначала вручную создавать FileReader` , а затем обертывать его.

3.2. Чтение построчно

Далее давайте прочитаем содержимое файла с помощью метода readLine :

public String readAllLines(BufferedReader reader) throws IOException {
StringBuilder content = new StringBuilder();
String line;

while ((line = reader.readLine()) != null) {
content.append(line);
content.append(System.lineSeparator());
}

return content.toString();
}

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

public String readAllLinesWithStream(BufferedReader reader) {
return reader.lines()
.collect(Collectors.joining(System.lineSeparator()));
}

3.3. Закрытие потока

После использования BufferedReader мы должны вызвать его метод close() , чтобы освободить любые системные ресурсы, связанные с ним. Это делается автоматически, если мы используем блок try-with-resources :

try (BufferedReader reader = 
new BufferedReader(new FileReader("src/main/resources/input.txt"))) {
return readAllLines(reader);
}

4. Другие полезные методы

Теперь давайте сосредоточимся на различных полезных методах, доступных в BufferedReader.

4.1. Чтение одного символа

Мы можем использовать метод read() для чтения одного символа. Давайте прочитаем весь контент посимвольно до конца потока:

public String readAllCharsOneByOne(BufferedReader reader) throws IOException {
StringBuilder content = new StringBuilder();

int value;
while ((value = reader.read()) != -1) {
content.append((char) value);
}

return content.toString();
}

Это прочитает символы (возвращенные как значения ASCII), преобразует их в char и добавит к результату. Мы повторяем это до конца потока, на что указывает значение ответа -1 от метода read() .

4.2. Чтение нескольких символов

Если мы хотим прочитать сразу несколько символов, мы можем использовать метод read(char[] cbuf, int off, int len) :

public String readMultipleChars(BufferedReader reader) throws IOException {
int length;
char[] chars = new char[length];
int charsRead = reader.read(chars, 0, length);

String result;
if (charsRead != -1) {
result = new String(chars, 0, charsRead);
} else {
result = "";
}

return result;
}

В приведенном выше примере кода мы прочитаем до 5 символов в массив символов и создадим из него строку. В случае, если при нашей попытке чтения не было прочитано ни одного символа (т.е. мы достигли конца потока), мы просто вернем пустую строку.

4.3. Пропуск символов

Мы также можем пропустить заданное количество символов, вызвав метод skip(long n) :

@Test
public void givenBufferedReader_whensSkipChars_thenOk() throws IOException {
StringBuilder result = new StringBuilder();

try (BufferedReader reader =
new BufferedReader(new StringReader("1__2__3__4__5"))) {
int value;
while ((value = reader.read()) != -1) {
result.append((char) value);
reader.skip(2L);
}
}

assertEquals("12345", result);
}

В приведенном выше примере мы читаем из входной строки, которая содержит числа, разделенные двумя символами подчеркивания. Чтобы построить строку, содержащую только числа, мы пропускаем символы подчеркивания, вызывая метод skip .

4.4. отметить и сбросить

Мы можем использовать методы mark(int readAheadLimit) и reset() , чтобы отметить некоторую позицию в потоке и вернуться к ней позже. В качестве несколько надуманного примера давайте используем mark() и reset() для игнорирования всех пробелов в начале потока:

@Test
public void givenBufferedReader_whenSkipsWhitespacesAtBeginning_thenOk()
throws IOException {
String result;

try (BufferedReader reader =
new BufferedReader(new StringReader(" Lorem ipsum dolor sit amet."))) {
do {
reader.mark(1);
} while(Character.isWhitespace(reader.read()))

reader.reset();
result = reader.readLine();
}

assertEquals("Lorem ipsum dolor sit amet.", result);
}

В приведенном выше примере мы используем метод mark() , чтобы отметить только что прочитанную позицию. Присвоение ему значения 1 означает, что код запомнит метку только на один символ вперед. Это удобно здесь, потому что, как только мы увидим наш первый непробельный символ, мы можем вернуться и перечитать этот символ без необходимости повторной обработки всего потока. Без отметки мы потеряли бы букву L в нашей последней строке.

Обратите внимание: поскольку mark() может генерировать UnsupportedOperationException , довольно часто markSupported() связывают с кодом, вызывающим mark (). Впрочем, здесь он нам и не нужен. Это потому, что markSupported() всегда возвращает true для BufferedReader .

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

5. Вывод

В этом кратком руководстве мы научились читать потоки ввода символов на практическом примере с использованием BufferedReader .

Наконец, исходный код примеров доступен на Github .