Kurs java dla początkujących - #6 Operacje wejścia i wyjścia

Kurs Java dla początkujących – #6 Operacje wejścia i wyjścia

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();
            }
        }
    }
}
  1. Tworzymy obiekt FileReader jako parametr podając plik wejściowy
  2. Tworzymy obiekt FileWriter jako parametr podajemy plik wyjściowy
  3. Odczytujemy plik znak po znaku (metoda .read() odczytuje pojedynczy znak)
  4. Zapisujemy każdy znak przy pomocy writera
  5. 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 bloku try wystąpi jakiś błąd. Jeśli w bloku try 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();
            }
        }
    }
}
  1. Tworzymy obiekt FileReader jako parametr podając plik wejściowy
  2. Tworzymy obiekt StringBuilder'a
  3. Odczytujemy plik znak po znaku (metoda .read() odczytuje pojedynczy znak)
  4. Każdy znak dodajemy do StringBuilder'a korzystając z metody .append() rzutując przy tym każdy kod znaku do char
  5. 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 instrukcji try instrukcję, która tworzy zasób implementujący interfejs AutoCloseable 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();
    }
}
  1. Tworzymy obiekt URL podając adres http
  2. Otwieramy połączenie metodą .openConnection()
  3. Tworzymy BufferedReader, który buforuje dane z InputStreamReader'a
  4. Odczytujemy źródło strony internetowej linia po linii
  5. 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&param2=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
    }
}
  1. Tworzymy obiekt URL podając adres http
  2. Otwieramy połączenie metodą .openConnection()
  3. Ustawiamy flagę pozwalającą na zapis, na danym połączeniu
  4. Tworzymy OutputStreamWriter dla bieżącego połączenia
  5. Zapisujemy parametry
  6. 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 i Select
  • 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.

 

Co powinien wiedzieć każdy początkujący programista?

 

Kurs Java dla początkujących

Spis Treści:

  1. Wprowadzenie
  2. Klasy i Obiekty
  3. Tablice
  4. Kolekcje
  5. Instrukcje warunkowe i pętle
  6. Operacje wejścia i wyjścia
  7. Dziedziczenie, Polimorfizm, Interfejsy
  8. 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

 

Mateusz Dąbrowski

Cześć jestem Mateusz, zajmuję się programowaniem już ponad 12 lat z czego ponad 8 programuję w Javie. Zapraszam Cię do lektury mojego bloga. Możesz przeczytać więcej o mnie >>TUTAJ<<