Java #40: strumienie wejścia/wyjścia

Wiesz już dużo o programowaniu, ale Twój program nie ma zbyt wielkiego sensu, jeśli dane wejściowego i wyjściowe za każdym razem będziesz musiał na stałe ustawiać w swoim programie. W tej lekcji nauczysz się wykorzystywać strumienie wejścia lub wejścia, które możesz zastosować np. do pracy na plikach.

Klasy w standardzie Javy, które odpowiadają za obsługę różnych strumieni zewnętrznych, to InputStream i OutputStream.*

Jeśli spróbujesz stworzyć sam InputStream to zostaniesz zaskoczony, bo Twoje IDE nie będzie wstanie skompilować taki kod. Gdy zajrzysz w implementację tej klasy lub jej odpowiednika strumienia wyjścia OutputStream, zobaczysz że obie te klasy są abstrakcyjne. Ma to sens, ponieważ każdy strumień różni się od siebie, więc należy skorzystać z konkretnej implementacji.**

Rodzaje strumieni dostępnych w pakiecie java.io:

  • ByteArrayStream
  • FileStream
  • PipedStream
  • BufferedStream
  • FilterStream
  • PushbackStream
  • DataStream
  • ObjectStream
  • SequenceStream***
strumienie wejścia
Hierarchia strumieni wejścia i wyjścia

Strumienie pracy na plikach

Jest ich sporo, jednak wszystkie działają podobnie, więc jeśli zrozumiesz ideę jednego to z łatwością będziesz wstanie używać inny. Jednym z najczęściej implementowanych z nich jest FileStream i na nim się teraz skupię.

Najpierw postaram się pokazać Ci w jaki sposób możesz wczytać plik a następnie zapisać do niego dane. Standardowo stworze klasę z metodą main. Zawierać będzie ona dwie metody: readFile oraz writeFile. W pierwszym przypadku postaram się wczytaj log, który stworzyłem w poprzedniej lekcji. Także, jeśli nie ma w swoim katalogu projektowym pliku o nazwie myapp-log.txt, stwórz go proszę manualnie. W przypadku pliku wyjściowego, nic nie musisz robić, Java sama go stworzy i wypełni odpowiednimi danymi.

public static void main(String[] args) {
	String inputfilePath = "myapp-log.txt";
	String outputfilePath = "outputfile.txt";
	String fileContent = "Hello world!";
	
	readFile(inputfilePath);
	writeFile(outputfilePath, fileContent);
}

Strumienie wejścia – wczytywanie danych z pliku

Teraz zaimplementuję metodę odpowiadającą za odczyt pliku. Aby otworzyć stream musisz go opakować w try – catch. Jest to wymagane od nas, ponieważ samo stworzenie obiektu klasy FileInputStream wyrzuca wyjątek sprawdzalny o nazwie FileNotFoundException. W praktyce jednak złapię wyjątek IOException, po którym dziedziczy FileNotFoundException. To taka ciekawa sztuczka, jeśli chodzi o łapanie wyjątków, że jeśli złapiesz wyjątek rodzica, to także łapiesz jednocześnie wszystkie jego dzieci. Tu się to przyda, ponieważ manipulowanie strumieniem i tak wymagałoby dodatkowe złapanie wyjątku IOException. Póki co metoda readFile wygląda tak jak poniżej:

private static void readFile(String filePath) {
	InputStream input = null;
	try {
		input = new FileInputStream(filePath);
	} catch (IOException e) {
		LOGGER.log(Level.WARNING, e.getMessage(), e);
	} finally {
		try {
			if (input != null) {
				input.close();
			}
		} catch (IOException e) {
			LOGGER.log(Level.WARNING, e.getMessage(), e);
		}
	}
}

Otwieram strumień i wczytuję plik znajdują się pod ścieżką zapisaną w parametrze filePath. Jednocześnie łapię wyjątek i w sekcji catch używam znanych już Ci loggerów. Pamiętaj, aby wcześniej zadeklarować pole LOGGER w swojej klasie:

private static final Logger LOGGER = Logger.getLogger(LoggerUsage.class.getName());

Zamykanie strumienia

W sekcji finally należy zamknąć strumień używając metody close(). Tutaj warto podkreślić, że aby użyć obiektu input w sekcji finally to niestety musiałem go zdeklarować przed słowem try i przypisać tam null. Dlatego właśnie przed zamknięciem, na wszelki wypadek, istnieje prosty if, sprawdzający czy input został poprawnie utworzony. Na koniec całość należy opakować w kolejny trycatch.

W mojej metodzie wciąż brakuj wczytywania danych. Tutaj niemiła wiadomość, bo metoda read wczytuje tekst znak po znaku i to w dodatku w postaci jego numeru w tablicy ASCII. Stąd też brakujący kod w sekcji try wygląda niezbyt ładnie:

try {
	input = new FileInputStream(filePath);
	int character = 0;
	while((character = input.read()) != -1) {
		System.out.print((char)character);
	}
}

W uzupełnionym kodzie wczytuję za pomocą metody read znak po znaku aż do wystąpienia wartości -1 (koniec pliku). Wciąż jednak po wyświetleniu pobranych danych będę miał numery kodów znaków w tablicy ASCII, a nie ich wartości. Rozwiązaniem tego problemu jest konwersja za pomocą typu char. Pamiętaj też o użyciu metody print zamiast println, bo będziesz mieć jeden znak wyświetlony w jednej linii.

Efekt na konsoli powinien być taki sam, jak zawartość pliku:

advanced.logger.LoggerUsage, main, Teraz logi trafia tez do pliku, 2020-05-28
advanced.logger.LoggerUsage, main, null, 2020-05-28

Skoro umiesz już wczytać plik do Javy, to warto by było nauczyć się jak zapisać dane do niego.

Strumienie wyjścia – zapisywanie danych do pliku

Metoda write wygląda bardzo podobnie do read, którą już napisałem. Oczywiście używam tu FileOutputStream zamiast FileInputStream, ale także należy tutaj opakować go w try – catch, oraz zamknąć strumień w sekcji finally. Tym razem jednak skorzystam z faktu, że FileStream nie tylko może wczytywać kolejne znaki, ale także można skorzystać z całego łańcucha znaków konwertowanego na tablicę bajtów.

private static void writeFile(String outputfilePath, String fileContent) {
	OutputStream input = null;
	try {
		input = new FileOutputStream(outputfilePath);
		byte[] bytes = fileContent.getBytes();
		input.write(bytes);
	} catch (IOException e) {
		LOGGER.log(Level.WARNING, e.getMessage(), e);
	} finally {
		try {
			if (input != null) {
				input.close();
			}
		} catch (IOException e) {
			LOGGER.log(Level.WARNING, e.getMessage(), e);
		}
	}
}

Na koniec ciekawostka. Co się stanie, jeśli nie zamkniesz strumienia danych? Ponieważ będzie on cały czas otwarty zyskujesz na czasie otwarcie w przypadku, gdy potrzebujesz dany plik wielokrotnie otwierać. Takie rozwiązanie jest wydajniejsze, jednak pamiętaj, gdy już jesteś pewien, że dany strumień nie jest potrzebny, aby go po wszystkich operacjach w końcu zamknąć.

*Strumienie wejścia i wyjścia nie powinny być mylone ze strumieniami w Javie 8. To kompletnie inna technologia.

**Użyto tutaj wzorca projektowego, o nazwie dekorator: https://en.wikipedia.org/wiki/Decorator_pattern

***Tak naprawdę to zamiast np. ByteArrayStream powinienem napisać ByteArrayInputStream lub ByteArrayOutputStream, jednak aby się nie dublować usunąłem słowa Input i Output. Wynika to oczywiście z faktu, że strumienie wejścia muszą mieć swoje odpowiedniki w strumieniach wyjścia.

Lekcja na githubie: https://github.com/developeronthego/java-middle/tree/master/src/main/java/middle/lesson23

Lekcja o wczytywanie za pomocą biblioteki NIO: Java #42: pakiet NIO (new input-output)

Może Ci się również spodoba

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.