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 + '}'; } }
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.