Ta część kursu opowiada o tym jak komunikować się z innymi aplikacjami, bądź z systemem operacyjnym, w którym uruchomiona jest aplikacja. Operacje wejścia i wyjścia możemy też określić operacjami odczytu i zapisu (np. zapis i odczyt pliku). Są one bardzo potrzebne, ponieważ bez nich aplikacje byłyby mało przydatne.
System.in i System.out
Jednym z podstawowych sposobów wymiany informacji z „otoczeniem” jest odczyt ze standardowego wejścia i wypisywanie informacji na standardowe wyjście. Każda aplikacja uruchamiana w konsoli może odczytywać z niej informacje oraz wypisywać na niej komunikaty. W Javie mamy do dyspozycji System.in
oraz System.out
(wielokrotnie już pojawiało się w tym kursie w postaci System.out.println("...")
). Poniżej przykładowy program odczytujący i zapisujący znaki na konsoli:
import java.io.IOException; import java.io.InputStreamReader; public class Scanner { public static void main(String[] args) throws IOException { InputStreamReader inputStreamReader = null; try { inputStreamReader = new InputStreamReader(System.in); System.out.println("Help: Podaj znak 'q' by zakończyć."); System.out.println("Podaj dowolny znak: "); char c; do { c = (char) inputStreamReader.read(); System.out.print(c); } while (c != 'q'); } finally { if (inputStreamReader != null) { inputStreamReader.close(); } } } }
Dodatkowo w Javie mamy dostępny System.err
, który jest przeznaczony do wyświetlania na konsoli komunikatów błędów. Jednak aplikacje, które są w całości oparte na wejściu i wyjściu konsoli to raczej rzadkość. Poza tym, w bibliotece standardowej jest dostępna specjalna klasa, która ułatwia operowanie na System.in
i jest to java.io.Console
.
Odczyt i zapis pliku
Zajmijmy się teraz odczytem i zapisem z pliku, co jest bardziej praktycznym przykładem:
import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public class FileInOut { public static void main(String[] args) throws IOException { FileReader fileReader = null; FileWriter fileWriter = null; try { fileReader = new FileReader("in.txt"); // 1 fileWriter = new FileWriter("out.txt"); // 2 int c; while ((c = fileReader.read()) != -1) { //3 fileWriter.write(c); // 4 } } finally { // 5 if (fileReader != null) { fileReader.close(); } if (fileWriter != null) { fileWriter.close(); } } } }
- Tworzymy obiekt FileReader jako parametr podając plik wejściowy
- Tworzymy obiekt FileWriter jako parametr podajemy plik wyjściowy
- Odczytujemy plik znak po znaku (metoda
.read()
odczytuje pojedynczy znak) - Zapisujemy każdy znak przy pomocy writera
- W bloku finally zamykamy readera i writera.
try-finally jest to specjalna konstrukcja pozwalająca wykonać kod zawarty w bloku
finally
, nawet jeśli w blokutry
wystąpi jakiś błąd. Jeśli w blokutry
zostanie rzucony wyjątek, to blok finally zostanie i tak wywołany – pozwala to pozwalniać otwarte zasoby takie jak pliki czy połączenie do bazy danych. Konstrukcja ta występuje również w odmianie try-catch-finally.
Powyższy przykład nadaje się doskonale do kopiowania zawartości dwóch plików, ale możemy go w prosty sposób zmodyfikować i użyć tylko do odczytu zawartości pliku:
import java.io.FileReader; import java.io.IOException; public class FileRead { public static void main(String[] args) throws IOException { FileReader fileReader = null; try { fileReader = new FileReader("in.txt"); // 1 int c; StringBuilder sb = new StringBuilder(); // 2 while ((c = fileReader.read()) != -1) { // 3 sb.append((char) c); // 4 } System.out.println(sb.toString()); // 5 } finally { // 5 if (fileReader != null) { fileReader.close(); } } } }
- Tworzymy obiekt
FileReader
jako parametr podając plik wejściowy - Tworzymy obiekt
StringBuilder'a
- Odczytujemy plik znak po znaku (metoda
.read()
odczytuje pojedynczy znak) - Każdy znak dodajemy do
StringBuilder'a
korzystając z metody.append()
rzutując przy tym każdy kod znaku dochar
- Wypisujemy ciąg znaków z buildera na konsolę
Odczyt i zapis pliku linia po linii
Powyższe przykłady odczytu i zapisu plików są dosyć nisko poziomowe i mało praktyczne, ponieważ zazwyczaj potrzebujemy odczytać tylko fragment pliku, zamiast wczytywać cały plik do pamięci. Często też zdarza się, że potrzebujemy odczytać linie z pliku (lub przetwarzać go linia po linii), wtedy z pomocą przychodzi nam klasa BufferedReader
:
import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; public class FileRead { public static void main(String[] args) throws IOException { BufferedReader br = null; try { br = new BufferedReader(new FileReader("in.txt")); StringBuilder sb = new StringBuilder(); String line; while ((line = br.readLine()) != null) { sb.append(line).append(" "); } System.out.println(sb.toString()); } finally { if (br != null) { br.close(); } } } }
Podobnie jak przy odczycie, mamy także możliwość zapisu pliku linia polinii. Korzystamy wtedy z BufferedWriter
:
import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.util.Arrays; import java.util.List; public class FileWrite { public static void main(String[] args) throws IOException { List<String> lines = Arrays.asList( "To jest pierwsza linia", "To jest druga linia", "To jest trzecia linia" ); BufferedWriter bw = null; try { bw = new BufferedWriter(new FileWriter("out.txt")); for (String line : lines) { bw.write(line); bw.newLine(); } } finally { if (bw != null) { bw.close(); } } } }
Wszystkie powyższe przykłady to idiomy zapisu i odczytu plików w Javie, które każdy programista Javy powinien znać. W wersji 7 Javy został dodana klasa Files (a w Javie 8 została rozszerzona), która opakowuje powyższe idiomy w bardziej zwięzłe i wygodniejsze w użyciu metody:
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; public class FileRead { public static void main(String[] args) throws IOException { System.out.println(new String(Files.readAllBytes(Paths.get("in.txt")))); } }
Tutaj odczyt całego pliku do String'a
można zmieścić w jednej linijce. Podobnie jest też z przetwarzaniem pliku linia po linii:
import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.stream.Stream; public class FileRead { public static void main(String[] args) throws IOException { StringBuilder contentBuilder = new StringBuilder(); try (Stream<String> stream = Files.lines(Paths.get("in.txt"), StandardCharsets.UTF_8)) { stream.forEach(line -> contentBuilder.append(line).append(" ")); System.out.println(contentBuilder.toString()); } } }
Przykład odczytujący pliki linia po linii korzysta ze Stream'ów
oraz lambdy, która jest przekazywana do metody .forEach()
, gdzie przetwarzane są linie (więcej o tych elementach w kolejnej części cyklu).
try-with-resources – to taka odmiana boku
try
, która pozwala w automatyczny sposób zamykać zasoby (resource). Jeśli umieścimy w nawiasach po instrukcjitry
instrukcję, która tworzy zasób implementujący interfejsAutoCloseable
np.BufferedReader
, to po wyjściu z tego bloku zasób zostanie automatycznie zamknięty. Dzięki temu możemy napisać kilka linijek kodu mniej. Instrukcja ta może przyjmować także formę try-catch-with-resources.
Klasa Files
zawiera wiele przydatnych metod. Warto się z nimi zapoznać i używać ich jak najczęściej. Nie znaczy to jednak, że musisz zawsze korzystać z klasy Files
. Niektóre metody tej klasy korzystają ze wspomnianych wyżej elementów takich jak Stream'y
. Mogą one w niektórych przypadkach powodować różnego rodzaju problemy. Wtedy łatwiej jest skorzystać z bardziej klasycznych (przedstawionych wyżej) metod odczytu czy zapisu plików. Pozwoli Ci to na większą kontrolę tego, co się dzieje w kodzie.
Odczyt i zapis zasobów przy użyciu sieci
Odczyt zasobu przez http
Kolejnym sposobem żeby wymieniać dane ze światem zewnętrznym jest odczyt i zapis adresów internetowych. Java ma również odpowiednie klasy pomocne przy tym zadaniu. By móc odczytać lub zapisać adres url potrzebujemy stworzyć odpowiedni obiekt klasy URL
podając adres http, a następnie otworzyć na nim połączenie metoda .openConnection()
:
import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; public class ReadUrl { public static void main(String[] args) throws Exception { URL url = new URL("https://nullpointerexception.pl"); // 1 URLConnection connection = url.openConnection(); // 2 BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); // 3 String inputLine; while ((inputLine = in.readLine()) != null) { // 4 System.out.println(inputLine); // 5 } in.close(); } }
- Tworzymy obiekt
URL
podając adres http - Otwieramy połączenie metodą
.openConnection()
- Tworzymy
BufferedReader
, który buforuje dane zInputStreamReader'a
- Odczytujemy źródło strony internetowej linia po linii
- Wypisujemy zawartość linia po linii
Zapis
Zapis możemy zrealizować na dwa sposoby:
Pierwszy: Przekazując parametry w url’u tak korzystamy wtedy z metody GET
https://nullpointerexception.pl?param1=1¶m2=2
Drugi: Przekazując parametry w ciele zapytania, korzystamy wtedy z metody POST
public class WriteUrl { public static void main(String[] args) throws Exception { URL url = new URL("https://nullpointerexception.pl"); // 1 URLConnection connection = url.openConnection(); // 2 connection.setDoOutput(true); // 3 OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream()); // 4 out.write("param1=1"); // 5 out.write("param2=2"); out.close(); // 6 } }
- Tworzymy obiekt
URL
podając adres http - Otwieramy połączenie metodą
.openConnection()
- Ustawiamy flagę pozwalającą na zapis, na danym połączeniu
- Tworzymy
OutputStreamWriter
dla bieżącego połączenia - Zapisujemy parametry
- Zamykamy writera
Z racji tego, że komunikacja pomiędzy aplikacjami w internecie jest coraz bardziej powszechna, powstało wiele bibliotek (tak zwanych klientów http), które ułatwiają zadanie wymiany danych pomiędzy klientem a serwerem. Takie biblioteki to np. Apache Commons Http Component, RestTemplate, OkHttp.
Łączenie z bazą
Kolejnym i najpowszechniej używanym sposobem komunikacji aplikacji ze źródłami danych jest łączenie się z bazą danych (najczęściej sql’ową). Jest to jednak na tyle skomplikowane zadanie, że wymaga dłuższego opisu. Dlatego na tym etapie nauki nie przedstawię żadnego fragmentu kodu. Dodam tylko, że aby połączyć się z bazą danych potrzebny jest:
- sterownik (driver) umożliwiający połączenie z daną bazą danych, zwykle w postaci dodatkowego pliku
Jar
, który trzeba wcześniej pobrać z internetu i odpowiednio umieścić w projekcie; - zainstalowany i działający serwer bazy danych (np. Mysql, Postgresql);
- podstawowa znajomość
SQL
pozwalająca utworzyć schemat bazy i napisanie najprostszych zapytańSQL
:Insert
iSelect
- nawiązanie połączenia z bazą danych i wykonywanie zapytań na danym połączeniu.
Podsumowanie
W tej części kursu nauczyliśmy się jak komunikować naszą aplikację ze światem zewnętrznym. Operacje wejścia i wyjścia dają nam nowe możliwości zasilania aplikacji danymi z różnych źródeł lub konfigurowania jej przy pomocy plików. A w wielu aplikacjach odczyt i zapis plików jest jedną z najczęściej wykonywanych operacji.
Kurs Java dla początkujących
Spis Treści:
- Wprowadzenie
- Klasy i Obiekty
- Tablice
- Kolekcje
- Instrukcje warunkowe i pętle
- Operacje wejścia i wyjścia
- Dziedziczenie, Polimorfizm, Interfejsy
- Stream’y i lambdy
Żródła:
https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html
https://docs.oracle.com/javase/tutorial/essential/io/bytestreams.html
https://docs.oracle.com/javase/tutorial/essential/io/charstreams.html
https://docs.oracle.com/javase/tutorial/essential/io/legacy.html
https://docs.oracle.com/javase/tutorial/networking/urls/readingWriting.html