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 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 try – catch.
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)