Посібник з BufferedReader

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 синхронізується (безпечно для потоку), а сканер - ні
  • Сканер може аналізувати примітивні типи та рядки за допомогою регулярних виразів
  • BufferedReader дозволяє змінювати розмір буфера, тоді як Scanner має фіксований розмір буфера
  • BufferedReader має більший розмір буфера за замовчуванням
  • Сканер приховує IOException , тоді як BufferedReader змушує нас це обробляти
  • BufferedReader, як правило, швидший за 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);

This will set the buffer size to 16384 bytes (16 KB).

The optimal buffer size depends on factors like the type of the input stream and the hardware on which the code is running. For this reason, to achieve the ideal buffer size, we have to find it ourselves by experimenting.

It's best to use powers of 2 as buffer size since most hardware devices have a power of 2 as the block size.

Finally, there is one more handy way to create a BufferedReader using the Files helper class from the java.nioAPI:

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

Creating itlike this is a nice way to buffer if we want to read a file because we don't have to manually create a FileReader first and then wrap it.

3.2. Reading Line-by-Line

Next, let's read the content of the file using the readLine method:

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(); }

We can do the same thing as above using the lines method introduced in Java 8 a bit more simply:

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

3.3. Closing the Stream

After using the BufferedReader, we have to call its close() method to release any system resources associated with it. This is done automatically if we use a try-with-resources block:

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

4. Other Useful Methods

Now let's focus on various useful methods available in BufferedReader.

4.1. Reading a Single Character

We can use the read() method to read a single character. Let's read the whole content character-by-character until the end of the stream:

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(); }

This will read the characters (returned as ASCII values), cast them to char and append them to the result. We repeat this until the end of the stream, which is indicated by the response value -1 from the read() method.

4.2. Reading Multiple Characters

If we want to read multiple characters at once, we can use the method 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; }

In the above code example, we'll read up to 5 characters into a char array and construct a string from it. In the case that no characters were read in our read attempt (i.e. we've reached the end of the stream), we'll simply return an empty string.

4.3. Skipping Characters

We can also skip a given number of characters by calling the skip(long n) method:

@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); }

In the above example, we read from an input string which contains numbers separated by two underscores. In order to construct a string containing only the numbers, we are skipping the underscores by calling the skip method.

4.4. mark and reset

We can use the mark(int readAheadLimit) and reset() methods to mark some position in the stream and return to it later. As a somewhat contrived example, let's use mark() and reset() to ignore all whitespaces at the beginning of a stream:

@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); }

In the above example, we use the mark() method to mark the position we just read. Giving it a value of 1 means only the code will remember the mark for one character forward. It's handy here because, once we see our first non-whitespace character, we can go back and re-read that character without needing to reprocess the whole stream. Without having a mark, we'd lose the L in our final string.

Note that because mark() can throw an UnsupportedOperationException, it's pretty common to associate markSupported() with code that invokes mark(). Though, we don't actually need it here. That's because markSupported() always returns true for BufferedReader.

Of course, we might be able to do the above a bit more elegantly in other ways, and indeed mark and reset aren't very typical methods. They certainly come in handy, though, when there is a need to look ahead.

5. Conclusion

У цьому короткому посібнику ми дізналися, як читати потоки введення символів на практичному прикладі за допомогою BufferedReader .

Нарешті, вихідний код для прикладів доступний на Github.