Programowanie średnio-zaawansowane #24: Strumienie znakowe, kodowanie i try-with-resources

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. Klasy te 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.

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

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

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

Dodaj komentarz