Przewodnik po BufferedReader

1. Przegląd

BufferedReader to klasa, która upraszcza odczytywanie tekstu ze strumienia wejściowego znaku. Buforuje znaki, aby umożliwić efektywny odczyt danych tekstowych.

W tym samouczku przyjrzymy się, jak używać klasy BufferedReader .

2. Kiedy używać BufferedReader

Ogólnie BufferedReader przydaje się, jeśli chcemy czytać tekst z dowolnego źródła wejściowego, czy to plików, gniazd, czy czegoś innego.

Mówiąc najprościej, pozwala nam zminimalizować liczbę operacji we / wy, odczytując fragmenty znaków i przechowując je w wewnętrznym buforze. Podczas gdy bufor zawiera dane, czytnik będzie czytał z niego zamiast bezpośrednio z bazowego strumienia.

2.1. Buforowanie innego czytnika

Podobnie jak większość klas we / wy Java, BufferedReader implementuje wzorzec Decorator, co oznacza, że ​​oczekuje czytnika w swoim konstruktorze. W ten sposób umożliwia nam elastyczne rozszerzenie instancji implementacji programu Reader o funkcjonalność buforowania:

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

Ale jeśli buforowanie nie ma dla nas znaczenia, możemy po prostu użyć bezpośrednio FileReadera :

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

Oprócz buforowania BufferedReader zapewnia również kilka przydatnych funkcji pomocniczych do odczytywania plików wiersz po wierszu . Tak więc, nawet jeśli bezpośrednie użycie FileReader może wydawać się prostsze , BufferedReader może być dużą pomocą.

2.2. Buforowanie strumienia

Ogólnie rzecz biorąc, możemy skonfigurować BufferedReader do przyjmowania dowolnego rodzaju strumienia wejściowegojako podstawowe źródło . Możemy to zrobić używając InputStreamReader i opakowując go w konstruktor:

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

W powyższym przykładzie czytamy z System.in, który zwykle odpowiada wejściu z klawiatury. Podobnie moglibyśmy przekazać strumień wejściowy do odczytu z gniazda, pliku lub dowolnego możliwego typu tekstowego wejścia. Jedynym warunkiem wstępnym jest istnienie odpowiedniej implementacji InputStream .

2.3. BufferedReader vs Scanner

Alternatywnie moglibyśmy użyć klasy Scanner, aby uzyskać taką samą funkcjonalność, jak w przypadku BufferedReader.

Istnieją jednak znaczne różnice między tymi dwiema klasami, które mogą sprawić, że będą one dla nas mniej lub bardziej wygodne, w zależności od naszego przypadku użycia:

  • BufferedReader jest zsynchronizowany (bezpieczny wątkowo), podczas gdy Scanner nie
  • Scanner może analizować typy pierwotne i ciągi znaków przy użyciu wyrażeń regularnych
  • BufferedReader umożliwia zmianę rozmiaru bufora, podczas gdy Scanner ma stały rozmiar bufora
  • BufferedReader ma większy domyślny rozmiar buforu
  • Scanner ukrywa IOException , podczas gdy BufferedReader zmusza nas do jego obsługi
  • BufferedReader jest zwykle szybszy niż Scanner, ponieważ odczytuje tylko dane bez ich analizowania

Mając to na uwadze, jeśli analizujemy poszczególne tokeny w pliku, Scanner będzie czuł się nieco bardziej naturalnie niż BufferedReader. Ale tylko czytanie linii na raz to miejsce, w którym BufferedReader świeci.

W razie potrzeby mamy również przewodnik po skanerze .

3. Czytanie tekstu za pomocą BufferedReader

Przejdźmy przez cały proces budowania, używania i niszczenia BufferReadera, aby czytać z pliku tekstowego.

3.1. Inicjowanie BufferedReader

Po pierwsze, stwórzmy BufferedReader używając swojego BufferedReader (czytnik) konstruktora :

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

Takie zawijanie FileReadera to dobry sposób na dodanie buforowania jako aspektu dla innych czytelników.

Domyślnie użyje to bufora o rozmiarze 8 KB. Jeśli jednak chcemy buforować mniejsze lub większe bloki, możemy skorzystać z konstruktora BufferedReader (Reader, int) :

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

Spowoduje to ustawienie rozmiaru bufora na 16384 bajtów (16 KB).

Optymalny rozmiar bufora zależy od takich czynników, jak typ strumienia wejściowego i sprzęt, na którym działa kod. Z tego powodu, aby osiągnąć idealny rozmiar bufora, musimy sami go znaleźć, eksperymentując.

Najlepiej jest używać potęg 2 jako rozmiaru bufora, ponieważ większość urządzeń sprzętowych ma moc 2 jako rozmiar bloku.

Wreszcie istnieje jeszcze jeden wygodny sposób tworzenia BufferedReader przy użyciu klasy pomocniczej Files z interfejsu API java.nio :

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

Tworzę tojest to dobry sposób na buforowanie, jeśli chcemy odczytać plik, ponieważ nie musimy najpierw ręcznie tworzyć FileReader, a następnie go zawijać.

3.2. Czytanie wiersz po wierszu

Następnie przeczytajmy zawartość pliku metodą 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(); }

Możemy zrobić to samo, co powyżej, używając nieco prostszej metody linii wprowadzonej w Javie 8 :

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

3.3. Zamknięcie strumienia

Po użyciu BufferedReader musimy wywołać jego metodę close () w celu zwolnienia wszelkich zasobów systemowych z nim związanych. Odbywa się to automatycznie, jeśli używamy bloku try-with-resources :

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

4. Inne przydatne metody

Teraz skupmy się na różnych przydatnych metodach dostępnych w BufferedReader.

4.1. Czytanie pojedynczego znaku

Możemy użyć metody read () do odczytania pojedynczego znaku. Przeczytajmy całą treść znak po znaku, aż do końca transmisji:

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

W tym krótkim samouczku nauczyliśmy się odczytywać strumienie wprowadzania znaków na praktycznym przykładzie przy użyciu BufferedReader .

Wreszcie, kod źródłowy przykładów jest dostępny na Github.