Programowanie średnio-zaawansowane #23: strumienie wejścia/wyjścia

Wiesz już duż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ę korzystać ze strumieni wejścia lub wejścia, które możesz wykorzystać np. do pracy na plikach.

Klasami w standardzie Javy, które odpowiadają za obsługę różnych strumieni zewnętrznych*

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***

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

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

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.

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ąć.

*Nie myl strumieni wejścia i wyjścia 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.

Dodaj komentarz