Zapis i odczyt plików w Java 7+

Historycznie praca z plikami w Javie nie była niczym przyjemnym – duża ilość kodu związanego z buforami, brak wygodnego API dostępowego do samych plików, już nie wspominając o operacjach na nich. Na szczęście wszystkie te bolączki przestały istnieć, kiedy wprowadzona została Java 7 wraz z pakietem zmian pod egidą NewIO 2. Java 8 wraz ze strumieniami tylko polepszyła dobry stan rzeczy. Jako że post jest pisany w odpowiedzi na życzenie jednego z naszych czytelników, to jako przykład weźmiemy przypadek użycia zaproponowany przez Niego. Mianowicie: mamy plik tekstowy zawierający informację o obiekcie, następnie wczytujemy ten plik, tworzymy nowy obiekt na jego podstawie, modyfikujemy go, a finalnie zapisujemy jako nowy plik, tym razem w formacie JSON.

NewIO 2 – odczyt pliku w Java 7

Zacznijmy od utworzenia naszego przykładowego pliku z danymi:

EmployeeData
Paweł,Ćwik
32
y
EmployeeData
Dawid,Nowak
32
n

Plik przechowuje informacje o pracowniku – jego imię, nazwisko, wiek oraz czy jest zatrudniony na pełny etat. Stwórzmy więc w pierwszej kolejności klasę Employee, która będzie korzystać z tych danych:

package pl.devfoundry.newio.demo.domain;

public class Employee {

    private String firstName;
    private String lastName;

    private int age;

    private boolean fullTime;
    public Employee(String firstName, String lastName, int age, boolean fullTime) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.fullTime = fullTime;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", age=" + age +
                ", fullTime=" + fullTime +
                '}';
    }

}

Jeśli chcesz prześledzić zmiany krok po kroku w formie wideo, to zapraszamy na nasz kanał na YouTube.


Skoro mamy już nasz plik oraz klasę wzorcową, czas zabrać się za odczyt z pliku. Od Javy 7 służą nam (w podstawowych przypadkach) dwie klasy – Path i File. Pierwsza z nich odpowiada za wszelkiego rodzaju ścieżki w naszym systemie – zarówno do plików, jak i katalogów. Nie operujemy więc już na czystych ścieżkach. File, jak łatwo wywnioskować, służy do przechowywania informacji o pliku. Z tymi dwoma klasami spokrewnione są dwie klasy „utilsowe” o nazwach (jak to w Javie się przyjęło) Paths i Files. Dzięki nim możemy w bardzo prosty sposób porównać ze sobą np. dwie ścieżki, pobierać plik z danej ścieżki, odczytywać zawartość pliku, sprawdzać prawa dostępowe i wiele innych. Nas jednak interesuje obecnie najprostszy przypadek użycia – czyli odczyt wszystkich linijek pliku. Kod rozwiązania będzie prezentował się tak:

package pl.devfoundry.newio.demo;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class MainApp {

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

        Path path = Paths.get("./employees.txt");

        List<String> lines = Files.readAllLines(path);

    }
}

Używamy metody .get() z klasy Paths by utworzyć nową ścieżkę, która będzie wskazywać na plik employees.txt, który znajduje się w katalogu projektu. Następnie używamy klasy pomocniczej Files, by odczytać całą zawartość pliku podanego w ścieżce, linia po linii. I to tyle. Lista lines zawiera wszystkie linijki tekstu z pliku wejściowego. Czas więc na odrobinę logiki biznesowej i ich obróbkę.

package pl.devfoundry.newio.demo;

import pl.devfoundry.newio.demo.domain.Employee;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class MainApp {

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

        Path path = Paths.get("./employees.txt");

        List<String> lines = Files.readAllLines(path);

        List<Employee> employees = new ArrayList<>();

        for(int i=0;i<lines.size();i++) {
            if(lines.get(i).equals("EmployeeData")) {
                String[] name = lines.get(i + 1).split(",");
                int age = Integer.parseInt(lines.get(i+2));
                boolean fullTime = lines.get(i + 3).equalsIgnoreCase("y");

                employees.add(new Employee(name[0],name[1],age,fullTime));
            }
        }

        employees.forEach(System.out::println);

    }
}

Czyli wyszukujemy linijkę z napisem „EmployeeData”, a następnie pobieramy dane z trzech kolejnych i na ich podstawie tworzymy nowego pracownika. Oczywiście nie ma tutaj żadnej kontroli czy dane są odpowiednie i czy nie przekroczymy indeksu – w aplikacji produkcyjnej taka walidacja musiałaby się oczywiście pojawić. Tutaj dla zachowania większej przejrzystości po prostu przechodzimy dalej.

NewIO 2 – zapis do pliku

Po pierwsze potrzebujemy logiki, która nasz obiekt przerobi na format JSON. W 99% przypadków takie rzeczy robi się za pomocą bibliotek zewnętrznych, jednak tu mamy prosty przypadek, dla którego nie warto dociągać całych bibliotek, skoro wystarczy jedna prosta metoda toJson() dla obiektu i druga dla całej listy. Do klasy Emplyee dodajemy

public String toJson() {
    return "{" +
                "\"firstName\": \"" + this.firstName + "\", " +
                "\"lastName\": \"" + this.lastName + "\", " +
                "\"age\":" + this.age + ", " +
                "\"fullTime\": " + Boolean.toString(this.fullTime) +
            "}";
}

Natomiast do klasy MainApp dorzucamy metodę statyczną

public static String toJson(List<Employee> employees) {
    String empl = employees.stream()
                    .map(Employee::toJson)
                    .collect(Collectors.joining(","));
    return "{\"employees\": ["+empl+"]}";
}

Tak oto za pomocą kilku linijek kodu, bez użycia i narzutu zewnętrznych bibliotek, mamy nasze dane w formacie JSON. Jedyne, co pozostało, to zapisać je do pliku. Na szczęście dla nas Java 7 również bardzo ułatwiła sprawę. Przez „bardzo” mam na myśli fakt, że całą tę operację zmieścimy w dwóch linijkach:

Path savePath = Paths.get("./employees.json");

Files.write(savePath,toJson(employees).getBytes(Charset.forName("UTF-8")));

W pierwszej z nich tworzymy (nieistniejącą jeszcze) ścieżkę do pliku i ponownie używamy klasy Files, tym razem by zapisać nasz plik. Zawsze warto do metody .getBytes() dorzucić kodowanie, w jakim chcemy zapisać nasze dane – by uniknąć potencjalnych szlaczków zamiast np. polskich znaków.

Podsumowanie

Jak powyższe przykłady pokazują – Java 7 bardzo wiele zmieniła, jeśli chodzi o obsługę plików. Wszystko sprowadza się do tego, że w końcu można zapamiętać jak wczytać i zapisać plik, co przy wcześniejszych rozwiązaniach było dla mnie niemożliwością 🙂 i zawsze powodowało wspomaganie się StackOverflow 🙂

Kod rozwiązania jest dostępny na platformie github.


Podziel się tym wpisem:

Dodaj komentarz