Obsługa plików

Póki co wszystkie dane, które wprowadzaliśmy do aplikacji ginęły po jej zamknięciu. No cóż, dane w pamięci maszyny wirtualnej niestety są ulotne, dlatego musimy znaleźć jakiś sposób na przechowywanie danych.

Jednym rozwiązaniem jest podpięcie bazy danych, jednak na to jeszcze nie czas – wyjmiemy mniejszą armatę. 😉

Z pomocą przychodzą nam pliki – czyli zapis informacji do pliku i ewentualny odczyt podczas uruchamiania aplikacji. Zobaczmy, więc jak ugryźć obsługę plików – do dzieła!

Pliki

Pliki mogą być tekstowo i binarnie. W tej lekcji skupimy się na obsłudze plików tekstowych.

Czym w ogóle różnią się te pliki? Pliki tekstowe to takie, które jesteśmy w stanie otworzyć w jakimś edytorze tekstowym i jesteśmy w stanie przeczytać. Czyli możemy zapisywać znaki z klawiatury – a dokładniej z tablicy ASCII.

Czym jest tablica ASCII – jest to tablica znaków, gdzie każdemu znakowi jest przyporządkowywana wartość liczbowa. Pamiętasz lekcję z typami danych? Każdy typ danych jest zapisywany w inny sposób w komputerze – znaki są własnie zapisywane wartościami z tablicy ASCII.

Tablica ASCII wygląda tak:

Znalezione obrazy dla zapytania ascii

Jak widać są wszystkie znaki z klawiatury zakodowane liczbami od 0 do 127 – jest to podstawowa tablica ASCII. Istnieje jeszcze rozszerzona tablica ASCII, która ciągnie się aż do 255 numer i zawiera polskie znaki.

Podsumujmy pliki tekstowe są to pliki, które jesteśmy w stanie przeczytać – zawiera w sobie tylko znaki drukowalne, czyli z tablicy ASCII – podstawowej lub rozszerzonej. Zależy czy używamy np. polskich znaków.

Co zaś plikami binarnymi? Do plików tekstowych zapisywaliśmy wartości od 0 do 127 – lub do 255. W plikach binarnych możemy dodawać przeróżne wartości – niekoniecznie te, które mają odpowiednik w tablicy ASCII.

Czy miałeś kiedyś styczność z plikami binarnymi? Z pewnością tak nie raz go otworzyłeś i wtedy nie zobaczyłeś zbyt sensownego tekstu, zobaczyłeś tzw. krzaki. Czyli tekst niemożliwy do przeczytania przez człowieka. Ze względu, że do pliku możemy zapisać pojedyńcze bajty to złączenie ich obok siebie podczas wyświetlania pliku może czasami spowodować znaku z tablicy ASCII i dlatego pliki czasami przeplatają się literkami choć cały tekst nie jest zrozumiały.

Pliki binarne sobie jednak zostawimy na następne etapy nauki – dziś zajmijmy się plikami tekstowymi.

PrintWriter

Do zapisywania pliku skorzystamy z metod klasy PrintWriter – aby stworzyć obiekt tej klasy skorzystamy z konstruktora, który potrzebuje m.in nazwę pliku, do którego będziemy zapisywać dane – jeżeli nie istnieje to zostanie on utworzony.

Do zapisywania danych będziemy używali metody write gdzie jako argument będziemy podawać dane do zapisu – w formie Stringu lub po prostu jakieś liczby.

BufferedReader

Do odczytu danych tekstowych z pliku skorzystamy z klasy BufferedReader. Do stworzenia obiektu tej klasy wykorzystamy m.in. nazwę pliku, z którego będą odczytywane wartości. W przypadku, gdy plik nie będzie istniał otrzymamy stosowną o tym informację.

Do odczytywania danych będziemy używali metody readLine() – która będzie zczytywały całą linię tekstu – aż do napotkania znaku końca linia (entera). Można zastosować również metodę read która wczytuję określoną liczbę znaków, jednak my skupimy się na tej pierwszej.

Pierwszy plik!

Muszę Cię uprzedzić na samym początku – obsługa plików w Javie nie jest zbyt przyjaźnie stworzona. Takie jest moje skromne zdanie, myślę, że inne języki robią to lepiej. Na szczęście, rzadko z nich korzystam. Jednak to nie jest powód, aby pominąć to zagadnienie, dlatego lecimy!

Na początku musimy stworzyć obiekt PrintWriter – aby móc cokolwiek zapisywać do niego:

String fileName = "dane.txt";
PrintWriter printWriter = new PrintWriter(fileName);

W tym momencie IntelliJ będzie Ci podkreślał, że musimy wykorzystać blok try catch lub dodać coś do metody – jest to tak zwany wyjątek.

Nie chcę zbyt rozpisywać się w tej lekcji czym są wyjątki – wyjątki są czymś co można “rzucać” w trakcie działania aplikacji – dzieje się to, gdy coś pójdzie nie tak. Przypuśćmy, że chcemy podzielić przez zero – no to hop, rzucamy wyjątek – który później czasami trzeba, a czasami można “złapać”. My w tym przypadku musimy, jednak możemy go “wypchnąć” dalej. Przy dodaniu czegoś takiego do metody:

throws IOException

Z pewnością spotkałeś się już z wyjątkami – gdy np. do swojej aplikacji zamiast liczby podałeś znak lub na odwrót możliwe, że w konsoli wydrukował się czerwony napis, który właśnie rzucenie wyjątku przez aplikację. Na tyle wystarczy – na wyjątkach skupimy się w czwartym tygodniu kursu. Rozpracujemy je od stóp do głów.

Czyli nasza funkcja main aktualnie wygląda tak:

public static void main(String[] args) throws IOException {

    String fileName = "dane.txt";
    PrintWriter printWriter = new PrintWriter(fileName);
}

Czas na dodanie tekstu do pliku – jak wspominałem wcześniej użyjemy do tego celu metody write.

printWriter.write("Witaj mój pliku. ");

Dodajmy jeszcze coś więcej:

printWriter.write("Obługuję je trudne pliki!. ");

Jeżeli skończyliśmy już zapisywanie do pliku to nadchodzi czas na najważniejszą część obsługi pliku – czyli zamknięcie pliku!

Tworząc obiekt PrintWriter w pamięci mamy stworzony zaczep na plik, który może spowodować wyciek danych jeżeli go nie zamkniemy. W skrócie zły użytkownik może w ten sposób w łatwy dosyć sposób dostać się do nieporządanych danych aplikacji – choć nas to zbytnio nie dotyczy to trzeba pamiętać o zamykaniu plików.

Zrobimy to przy użyciu metody close().

printWriter.close();

Nasz kod wygląda teraz tak:

package pl.maniaq;


import java.io.*;

public class Main {

    public static void main(String[] args) throws IOException {

        String fileName = "dane.txt";
        PrintWriter printWriter = new PrintWriter(fileName);
        printWriter.write("Witaj mój pliku. ");
        printWriter.write("Obługuję te trudne pliki!. ");
        printWriter.close();

    }
}

Czas na uruchomienie aplikacji i obserwowanie rezultatu:

No i konsola pusta, a utworzył się plik o nazwie dane.txt w katalogu root naszego projektu. Po otwarciu pliku widzimy taki tekst:

Witaj mój pliku. Obługuję te trudne pliki!.

Czyli wszystko dobrze działa.

Spróbujmy teraz odczytać te dane przy użyciu klasy BufferedReader.

Obiekt możemy stworzyć w trochę bardziej skomplikowany sposób – potrzebujemy jeszcze takie małego pośrednika między BufferedReader, a samym plikiem czyli obiektu FileReader.

FileReader fileReader = new FileReader(fileName);

Przyjmuje ona nazwę pliku, zapewnia on komunikację z plikiem – i tego właśnie obiektu wymaga konstruktor BufferedReader.

BufferedReader reader = new BufferedReader(fileReader);

I w taki oto sposób mamy gotowy obiekt reader, który pozwoli nam odczytać tekst z pliku.

Możemy to zrobić przy użyciu readLine() – który zwróci odczytanego Stringa aż do napotkania znaku końca linii – jeżeli go nie napotka to wczyta wszystko do końca pliku.

String readOneLineFromFile = reader.readLine();

Skoro już wszystko przeczytaliśmy to również zamykamy nasz plik analogicznie metodą close.

reader.close();

I na koniec wyświetlić wczytany tekst w konsoli:

System.out.println("Wczytałem: "+readOneLineFromFile);

Czyli nasz cały kod wygląda teraz tak:

package pl.maniaq;

import java.io.*;

public class Main {

    public static void main(String[] args) throws IOException {

        String fileName = "dane.txt";
        PrintWriter printWriter = new PrintWriter(fileName);
        printWriter.write("Witaj mój pliku. ");
        printWriter.write("Obługuję te trudne pliki!. ");
        printWriter.close();

        FileReader fileReader = new FileReader(fileName);
        BufferedReader reader = new BufferedReader(fileReader);
        String readOneLineFromFile = reader.readLine();
        reader.close();
        System.out.println("Wczytałem: "+readOneLineFromFile);

    }
}

Po uruchomieniu kodu otrzymujemy taki rezultat:

Wczytałem: Witaj mój pliku. Obługuję te trudne pliki!.

I mamy obsłużoną podstawową obsługę plików, teraz przedstawię Ci jeszcze kilka ciekawostek, które mogą Ci się nie raz przydać podczas pracy z plikiem – zrobię to jednak trochę krócej niż wyżej.

Znak nowej linii

Czasami byśmy chcieli, aby zapisać enter do naszego pliku – w końcu to normalne zapisywać miejsca, gdzie linia się kończy.

Do tego możemy posłużyć się znakiem końca linii czyli – \n. Wpisując go jako String w pliku pojawi się na to miejsce enter. 😉

Ponownie używamy metody write – tym razem do pliku zapiszę liczby z tablicy liczb i każda będzie w nowej linii.

int [] numbers = {1, 2, 5,  2, 1};
String fileName = "dane.txt";
PrintWriter printWriter = new PrintWriter(fileName);
for(int i = 0; i< numbers.length; i++) {
    printWriter.write("Jestem liczbą numer: " + i + ", a moja wartość to: " + numbers[i] + "\n");
}
printWriter.close();

Po uruchomieniu aplikacji w pliku otrzymujemy:

Jestem liczbą numer: 0, a moja wartość to: 1
Jestem liczbą numer: 1, a moja wartość to: 2
Jestem liczbą numer: 2, a moja wartość to: 5
Jestem liczbą numer: 3, a moja wartość to: 2
Jestem liczbą numer: 4, a moja wartość to: 1

Jak widzisz \n faktycznie powoduje wstawienie entera do pliku. 😉

Dopisywanie do pliku

Jak pewnie zauważyłeś nasza metoda write póki co zawsze nadpisywała plik – czyli usuwała najpierw wszystko z niego, a następnie dopisywała część, którą miała faktycznie zapisać. Aby dopisać do pliku możemy wykorzystać jeden z dwóch sposób.

Pierwszym złym pomysłem jest to, aby najpierw wczytać plik, do odczytanej treści dodać nową i ponownie zapisać. Jednak to zupełnie bez sensu.

Drugi dużo lepszym pomysłem jest dodanie obiektu, który za nas to obsłuży. Tak naprawdę musimy w inny sposób stworzyć obiekt PrintWriter – musimy mu dać obiekt odpowiedzialny za taką komunikację z plikiem.

Tworzymy, więc obiekt FileOutputStream.

FileOutputStream fileOutputStream = new FileOutputStream(fileName, true);

Jako pierwszy argument przyjmuje nazwę pliku – zaś drugim jest true lub false. True oznacza, że treść do pliku będzie dopisywana – false zaś oznacza nadpisywanie całego pliku. My oczywiście wybieramy True bo chcemy dopisywać do pliku.

Obiekt PrintWriter tworzymy teraz podając mu obiekt utworzony wcześniej.

PrintWriter printWriter = new PrintWriter(fileOutputStream);

I reszta kodu może zostać taka sama:

int [] numbers = {1, 2, 5,  2, 1};
String fileName = "dane.txt";
FileOutputStream fileOutputStream = new FileOutputStream(fileName, true);
PrintWriter printWriter = new PrintWriter(fileOutputStream);
for(int i = 0; i< numbers.length; i++) {
    printWriter.write("Jestem liczbą numer: " + i + ", a moja wartość to: " + numbers[i] + "\n");
}
printWriter.close();

Tym razem uruchamiając kilkukrotnie aplikację plik będzie wyglądał między innymi tak:

Jestem liczbą numer: 0, a moja wartość to: 1
Jestem liczbą numer: 1, a moja wartość to: 2
Jestem liczbą numer: 2, a moja wartość to: 5
Jestem liczbą numer: 3, a moja wartość to: 2
Jestem liczbą numer: 4, a moja wartość to: 1
Jestem liczbą numer: 0, a moja wartość to: 1
Jestem liczbą numer: 1, a moja wartość to: 2
Jestem liczbą numer: 2, a moja wartość to: 5
Jestem liczbą numer: 3, a moja wartość to: 2
Jestem liczbą numer: 4, a moja wartość to: 1
Jestem liczbą numer: 0, a moja wartość to: 1
Jestem liczbą numer: 1, a moja wartość to: 2
Jestem liczbą numer: 2, a moja wartość to: 5
Jestem liczbą numer: 3, a moja wartość to: 2
Jestem liczbą numer: 4, a moja wartość to: 1

Jak widać wartości są dopisywane. 😉

Odczytywanie do końca pliku liniami

Czasami cały plik będziemy chcieli odczytać liniami  do oczytu linii użyjemy readLine. Możemy opakować ją w pętle np. while, która będzie się wykonywała, aż do napotkania końca pliku.

Co symbolizuje koniec pliku w przypadku użycia metody readLine? Metoda readLine zwraca null, gdy osiągnie koniec pliku. Naszym warunkiem pętli while będzie, więc: wykonuj dopóki wczytana wartość jest różna od null.

Mamy, więc obiekt BufferedReader:

FileReader fileReader = new FileReader(fileName);
BufferedReader reader = new BufferedReader(fileReader);

I potrzebujemy odczytać teraz wartość i wykonywać to aż do napotkania null

String readOneLineFromFile = reader.readLine();
while(readOneLineFromFile != null) {
  readOneLineFromFile = reader.readLine();
}

Czyli pierwsza linia odpowiada za wczytanie pierwszej linii – jeżeli jest różna od nulla to wejdziemy w pętle while i ponownie wczytamy kolejną linię pliku. Dodatkowo możemy dodać wyświetlanie wczytanej linii:

while(readOneLineFromFile != null) {
    System.out.println("Wczytałem wartość: " + readOneLineFromFile);
    readOneLineFromFile = reader.readLine();
}

Czyli mamy taki kod:

FileReader fileReader = new FileReader(fileName);
BufferedReader reader = new BufferedReader(fileReader);
String readOneLineFromFile = reader.readLine();
while(readOneLineFromFile != null) {
    System.out.println("Wczytałem wartość: " + readOneLineFromFile);
    readOneLineFromFile = reader.readLine();
}
reader.close();

U mnie po uruchomieniu wygląda to tak:

Wczytałem wartość: Jestem liczbą numer: 0, a moja wartość to: 1
Wczytałem wartość: Jestem liczbą numer: 1, a moja wartość to: 2
Wczytałem wartość: Jestem liczbą numer: 2, a moja wartość to: 5
Wczytałem wartość: Jestem liczbą numer: 3, a moja wartość to: 2
Wczytałem wartość: Jestem liczbą numer: 4, a moja wartość to: 1
Wczytałem wartość: Jestem liczbą numer: 0, a moja wartość to: 1
Wczytałem wartość: Jestem liczbą numer: 1, a moja wartość to: 2
Wczytałem wartość: Jestem liczbą numer: 2, a moja wartość to: 5
Wczytałem wartość: Jestem liczbą numer: 3, a moja wartość to: 2
Wczytałem wartość: Jestem liczbą numer: 4, a moja wartość to: 1
Wczytałem wartość: Jestem liczbą numer: 0, a moja wartość to: 1
Wczytałem wartość: Jestem liczbą numer: 1, a moja wartość to: 2
Wczytałem wartość: Jestem liczbą numer: 2, a moja wartość to: 5
Wczytałem wartość: Jestem liczbą numer: 3, a moja wartość to: 2
Wczytałem wartość: Jestem liczbą numer: 4, a moja wartość to: 1

Jak widać został wczytany cały plik.

Wczytywanie całego pliku

Do wczytania całego pliku możesz użyć metody read z klasy BufferedReader. Ten temat mogę zostawić do własnego opracowania, ponieważ materiał i tak już jest spory, a powyższe zagadnienia w zupełności nam na razie wystarczą. 😉

Mogę dać Ci małą pomoc – metoda read do wczytania całego pliku potrzebuje tablicy charów, do której wpisuje wartości, jako drugi argument wymaga miejsca startowego – np. 0 mając na myśli początek pliku i jako trzeci argument miejsca, do którego ma być czytany plik.

Aby przeczytać plik potrzebujemy długości pliku – czyli ile znaków się w nim znajduję, ilość znaków wtedy możemy przekazać jako trzeci argument. Długość pliku można zdobywać metodą length() wywołaną na obiekcie File, który jest tworzony przez konstruktor parametrowy, który wymaga nazwy pliku. 😉

Podsumowanie

W tej lekcji dowiedziałeś się jak obsłużyć pliki tekstowe przy użyciu klas PrintWriter – do zapisu oraz BufferedReader – do odczytu. Choć nadal moim zdaniem jest to trochę prostsze w innych językach programowania np. python. Nie martw się, zbyt często z tego się nie korzysta. 😉

Jednak, aby utrwalić swoją wiedzę wykonaj poniższe zadania, aby mieć pewność, wszystko rozumiesz.

  1. Stwórz tablicę na liczby i wczytaj do niej liczby od użytkownika. Następnie zapisz wszystkie liczby do pliku – każda w nowej linii. Po uruchomieniu sprawdź czy plik został poprawnie utworzony.
  2. Stwórz tablicę na Stringi i wczytaj do niej imiona od użytkownika. Następnie zapisz wszystkie te imiona do pliku w formacie: “Podano imię: {tutaj-imie}”. Sprawdź rezultat w pliku.
  3. Stwórz plik, gdzie w pierwszej linii będzie liczba symbolizująca ilość liczb w kolejnych liniach. W kolejnych liniach zapisz te liczby. Wczytaj pierwszą liczbę – na jej podstawie utwórz tablicę o odpowiednim rozmiarze, resztę liczb wczytaj do tablicy i w kolejnej pętli wylicz sumę tych liczb i wypisz tą sumę na ekran. Do wczytania jednej liczby możesz użyć metody readLine i zrzutować Stringa na inta przy użyciu Interger.parseInt np. String readLine = bufferReader.readLine(); int number = Integer.parseInt(readLine);
  4. Stwórz plik, w którym każda nowa linia to jedno imię i nazwisko. Wczytaj cały plik linia po linii – każde imię i nazwisko, które możesz dodawać do wcześniej utworzonej listy Stringów poprzez List<String> lines = new ArrayList<String>() metodą add. Na koniec wypisz ile imion i nazwisk znaleziono w pliku i wyświetl je.
  5. Stwórz klasę Student, w której będą pola: id, name, lastName, age – zdefiniuj metodę toString(). W main stwórz listę Studentów przy użyciu: List<Student> students = new ArrayList<Student>() – dodaj do niej kilku studentów przy użyciu metody add i na koniec w pętli np. foreach zapisz wszystkich studentów do pliku: jeden student – jedna linia. Skorzystaj z wcześniej zdefiniowanej metody toString.

Jeżeli nie pamiętasz jak tworzyć Listę to wróc do poprzedniej lekcji, gdzie z grubsza o tym opowiadałem lub do większego artykułu, który stworzyłem na temat kolekcji w Javie.

Przykładowe moje rozwiązania zadań możesz znaleźć tutaj.

Jeśli wszystko poszło gładko to widzimy się w następnej części – już nie lekcji, tylko aplikacji domowej, gdzie przedstawię Ci Twoje zadanie na ten tydzień.

Kamil Klimek

Od 2016 jestem programistą Java. Przez pierwsze 4 lata pracowałem jako Full Stack Java Developer. Później postanowiłem postawić nacisk na Javę, żeby jeszcze lepiej ją poznać.

Subscribe
Powiadom o
guest
4 komentarzy
najstarszy
najnowszy oceniany
Inline Feedbacks
View all comments
Poul
Poul
5 lat temu

Małe pytanko, czy tak rozwiązane wczytanie z pliku w zadaniu 4 jest dobrym przykładem czy nie za bardzo?
String newLine;
while((newLine = reader.readLine()) != null)
{
System.out.println(newLine);
lines.add(newLine);
}

PS
Mały błąd przy końcu tekstu, BufferedReader do odczytu, zamiast BufferedWriter 😉

Kamil Klimek
Kamil Klimek
5 lat temu
Reply to  Poul

Jasne, taka forma jest w porządku. Nie chciałem jednak wprowadzać utrudnień w kursie, po prostu tamto rozwiązanie jest czytelniejsze dla nowej osoby. 😉

Mateusz
Mateusz
2 lat temu

Cześć,
nie mam bladego pojęcia ale nie zapisują mi się dane do pliku gdy robię zadanie nr 1 choć już nawet zaglądałem do rozwiązania zadania:

import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.Scanner;

public class ex1_1024kb {

    public static void main(String[] args) throws FileNotFoundException {
        final int MAX_NUMBER = 5;
        int[] numbers = new int[MAX_NUMBER];
        String fileName = "Numbers3.txt";

        Scanner userInput = new Scanner(System.in);
        for (int i = 0; i < MAX_NUMBER; i++) {
            System.out.println("Podaj liczbę nr " + (i+1));
            numbers[i] = userInput.nextInt();
        }
        PrintWriter writer = new PrintWriter(fileName);
        for (int number : numbers) {
            writer.write(number + "\n");
        }
        userInput.close();
    }
}

Po prostu tworzy się wciąż pusty plik.

Pozdrawiam,
Mateusz

Mateusz
Mateusz
2 lat temu
Reply to  Mateusz

Okej, już znalazłem błąd. Zamiast zamknąć PrintWriter writer to zamykałem Scanner userInput

4
0
Would love your thoughts, please comment.x