Ten wpis będzie bardzo podobny do poprzedniego, także jeśli jeszcze tego nie zrobiłeś/aś, przeczytaj wpierw lekcję o strumieniach danych. Tym razem zamiast korzystać z strumieni wejścia i wyjścia, użyję jednej z wielu klas typu Reader i Writer. Strumienie znakowe działają bardzo podobnie jak klasy strumieni, jednak są bardziej przystosowane do pracy z plikami tekstowymi.
Wybrane klasy typu Reader lub Writer:
- FileReader/FileWriter
- BufferedWriter/BufferedReader
- CharArrayReader/CharArrayWriter
- OutputStreamWriter/InputStreamReader
- PushbackInput/StreamPushbackReader
- StringWriter/StringReader
- PipedWriter/PipedReader
- FilterWriter/FilterReader
Jak widzisz są one zbieżne z tymi, które obsługują strumienie. Co więcej możesz używać strumień za pomocą klas Reader/Writer, wrzucając go w konstruktor swojego odpowiednika. W takim razie, pewnie spytasz, po co specjalne klasy, które robią prawie to samo. Istotną różnicą pomiędzy nimi jest ich sposób użycia. Klasyczne strumienie pracują na danych binarnych, co sprawia, że w przypadku plików tekstowych (które oczywiście, jak każdy inny plik, też są w gruncie rzeczy jakimiś bajtami), że nie radzą sobie specjalnie dobrze z różnymi kodowaniami (ang encoding)*. Dlatego właśnie istnieją dedykowane klasy, które dużo lepiej spisują się przy pracy z danymi tekstowymi.
Strumienie znakowe buforowane
Jednymi z najbardziej wydajnych z nich są BufferedReader i BufferedWriter. Podobnie jak w przykładzie z ostatniej lekcji, zaimplementuję dla Ciebie dwie metody, które pokażą Ci, jak sobie z nimi radzić. Pierwsza z nich readFromConsole wczyta za pomocą strumienia wejścia (System.in) wpisaną przez Ciebie komendę, a następnie zwróci ją na konsoli. Druga metoda, będzie trochę bardziej skomplikowana. Wczytam w niej log, który stworzyłem w lekcji o logach, dodam do każdej linii nową datę i zapiszę całość do nowego pliku.
W przypadku pierwszej metody nie ma tu nic bardziej skomplikowanego. InputStreamReader opakowuję w BufferedReader i następnie używam go, tak jak zwykły strumień. Jedyna różnica jest, że wczytuję tu całą linię a nie pojedynczy bajt lub znak.
private static void readFromConsole() { Reader streamReader = new InputStreamReader(System.in); BufferedReader bufferedReader = null; try { bufferedReader = new BufferedReader(streamReader); System.out.println("Wprowadź komendę"); String readLine = bufferedReader.readLine(); System.out.println("Twoja komenda to: " + readLine); } catch (IOException e) { LOGGER.log(Level.WARNING, e.getMessage(), e); } finally { try { if (bufferedReader != null) { bufferedReader.close(); } } catch (IOException e) { LOGGER.log(Level.WARNING, e.getMessage(), e); } } }
Try with resources
Cały czas korzystam tu z klasycznej klauzuli try – catch, która z powodu zamykania strumienia, jest bardzo rozbudowana. Z pomocą przychodzi tu mechanizm dostępny w Javie 7 o nazwie try – with – resources.
private static void copyFileWithDate(String inputfilePath, String outputfilePath) { try (Reader streamReader = new FileReader(inputfilePath); BufferedReader bufferedReader = new BufferedReader(streamReader); Writer fileWriter = new FileWriter(outputfilePath); BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);){ // jakiś kod } catch (IOException e) { LOGGER.log(Level.WARNING, e.getMessage(), e); } }
Jak widzisz w sekcji try pojawiły się nawiasy, w które wrzucam wszystkie zasoby, które powinny być automatycznie zamknięte. Taki kod często nie jest zbyt czytelny, więc jeśli chcesz możesz go zapisać po prostu tak:
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(inputfilePath));
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(outputfilePath)))
Jest trochę krócej, jednak sam musisz zdecydować, który sposób dla Ciebie jest korzystniejszy. Ostatnim elementem jest użycie pętli while do pobrania każdej linii z osobna do zmiennej typu String, a następnie zapisanie edytowanego tekstu do nowego pliku.
String readLine; while((readLine = bufferedReader.readLine()) != null) { bufferedWriter.write("Copy date: " + Instant.now() + "\n" + readLine + "\n"); }
Nie musisz tutaj przypisać wartość zmiennej lokalnej readLine, ponieważ jest oczywiste, że albo przypisze jakąś wartość z pliku albo w najgorszym razie będzie ona nullem. Podczas zapisu poza skopiowaniem linijki z pierwotnego pliku dodałem przed nim nową datę kiedy odbywała się taka operacja. Skorzystałem tu z metody Instant.now(), która podaje mi aktualny timestamp** na podstawie zegara systemowego.
Cała metoda copyFileWithDate:
private static void copyFileWithDate(String inputfilePath, String outputfilePath) { try (BufferedReader bufferedReader = new BufferedReader(new FileReader(inputfilePath)); BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(outputfilePath))){ String readLine; while((readLine = bufferedReader.readLine()) != null) { bufferedWriter.write("Copy date: " + Instant.now() + "\n" + readLine + "\n"); } } catch (IOException e) { LOGGER.log(Level.WARNING, e.getMessage(), e); } }
Kodowanie
Warto podkreślić, że FileReader/Writer sam określa kodowanie pliku na UTF-8. Jeśli chcesz użyć klasy, która pozwala na samodzielne przypisanie innego rodzaju kodowań, to użyj np. klasy InputStreamReader :
Reader reader = new InputStreamReader(new FileInputStream(inputfilePath),”UTF-8″);
*Są różne rodzaje kodowań plików tekstowych. Wynika to z tego, że wiele języków korzysta z innych znaków niż te standardowe – łacińskie. Najpopularniejsze rodzaje kodowań to np. UTF-8 lub UTF-16.
Więcej informacji o unicode znajdziesz tutaj: https://unicode-table.com/en/alphabets/.
Polecam też przeczytać ten artykuł o kodowaniu ogólnie: https://www.w3.org/International/questions/qa-what-is-encoding
**Tutaj dodatkowa informacja o tym czym jest timestamp: https://www.epochconverter.com
Link do kodu: https://github.com/developeronthego/java-middle/blob/master/src/main/java/middle/lesson24/BufferedReaderAndWriter.java
Wczytywanie tekstu za pomocą NIO: Java #42: pakiet NIO (new input-output)