Java NIO.2

tele_img_06

Java NIO, czyli New Input/Output lub Non-blocking Input/Output jest dostępne od Javy 1.4 (JSR-51). Wraz z wydaniem Javy w wersji 7 API zostało znacząco rozbudowane – światło dzienne ujrzała implementacja NIO.2 (JSR-203). Dwie najistotniejsze zmiany wydane wraz z NIO.2 to asynchroniczne (nieblokujące) I/O oraz API dostępu do systemu plików (klasy z pakietów java.nio.file i java.nio.file.attribute). Poza tym dopracowano funkcjonalność już wcześniej dostępną w NIO (m.in. bufory, sockety).

W niniejszym wpisie chciałbym skupić się na nowym Filesystem API, ponieważ wydaje mi się najciekawszą zmianą w podejściu do obsługi wejścia/wyjścia w Javie.

W obsłudze wejścia/wyjścia aż do Javy 7 mieliśmy tak naprawdę tylko jedną klasę dedykowaną do realizacji wszystkich operacji w systemie plików – java.io.File. Każdy zapis, odczyt oraz dowolna operacja na plikach czy katalogach musiała przejść bezpośrednio lub pośrednio przez java.io.File. W NIO.2 ta odpowiedzialność została rozbita na kilka klas i interfejsów, z których główne to interfejs java.nio.file.Path i klasa java.nio.file.Files. Jako najpoważniejsze wady „starego” IO wymienia się złą obsługę wyjątków (wiele metod nie rzucało wyjątków w razie niepowodzenia operacji), brak wsparcia dla symbolic link’ów, słabą obsługę metadanych plików, problemy z wydajnością w przypadku dużych katalogów. Nowe Filesystem API rozwiązuje powyższe problemy.

Path

Obiekt implementujący interfejs Path reprezentuje ścieżkę do zasobu w systemie plików. Z punktu widzenia programowania jest to mniej więcej odpowiednik java.io.File. Path może wskazywać na zasób, który (jeszcze) nie istnieje. Interfejs Path nie służy do operacji na plikach czy katalogach, służy do operacji na ścieżce do zasobu – jego metody pozwalają na przykład na uzyskanie samej nazwy pliku ze ścieżki, uzyskanie katalogu głównego, czy liczby podkatalogów. Do operacji na plikach służy nowa klasa Files, o której za chwilę.

Obiekty implementujące interfejs Path tworzymy przy pomocy klasy pomocniczej Paths:

1
2
Path path1 = Paths.get("/home/atena/test.txt");
Path path2 = Paths.get(URI.create("file:///home/atena/test.txt"));

Interfejs Path definiuje metody pozwalające na wydobycie wielu informacji o ścieżce:

  • getFileName() – zwraca nazwę pliku lub ostatni element ze ścieżki
  • getName(int index) – zwraca konkretny element ścieżki
  • getNameCount() – zwraca liczbę elementów w ścieżce
  • subpath(int beginIndex, int endIndex) – zwraca “podścieżkę”
  • getParent() – zwraca część ścieżki będącą parentem
  • getRoot() – zwraca katalog główny

Interfejs definiuje też bardzo przydatne metody pozwalające na “posprzątanie” oraz konwersję ścieżki:

  • normalize() – usuwa wszystkie nadmiarowe elementy ze ścieżki, czyli „.” oraz „directory/..” – nie sprawdza przy tym czy zasób faktycznie istnieje w nowej ścieżce – w przypadku linków może to spowodować, że nowa ścieżka nie będzie wskazywała na ten sam zasób co przed normalizacją
  • toUri() – zwraca String z URI zasobu
  • toAbsolutePath() – zwraca bezwzględną ścieżkę do zasobu
  • toRealPath(boolean resolveLinks) – metoda zwraca rzeczywistą bezwzględną ścieżkę do zasobu, przy okazji usuwając nadmiarowe elementy i rozwiązując symbolic linki (w przypadku przekazania true jako argument).

Z ciekawszych metod warto wspomnieć jeszcze o resolve – metodzie, która łączy ścieżkę zawartą w Path z przekazaną jako argument oraz relativize – ta z kolei uprasza nawigację pomiędzy dwoma ścieżkami.

toPath(), toFile()
Uzyskanie java.io.File z obiektu implementującego interfejs Path jest możliwe przy użyciu nowej metody toFile(). W drugą stronę konwersji możemy dokonać wołając toPath() na obiekcie klasy File.

Files

Metody zdefiniowane przez interfejs Path to metody syntaktyczne – to znaczy, że operują jedynie na ścieżce do zasobu, a nie na samym zasobie. Do operacji na plikach i katalogach służy klasa pomocnicza Files z pakietu java.nio.file. Klasa dostarcza zestaw statycznych metod do wykonywania różnych operacji na plikach i katalogach. Metody klasy Files używają instancji Path do lokalizowania zasobów. Poniżej kilka najważniejszych metod z klasy Files pogrupowanych funkcjonalnie.

Operacje sprawdzające pliki lub katalogi:

  • exists(Path, LinkOption), notExists(Path, LinkOption)
  • isReadable(Path), isWritable(Path), isExecutabel(Path)
  • isSameFile(Path, Path)

Operacje kopiowania, przenoszenia i usuwania plików lub katalogów:

  • copy(Path, Path, CopyOption)
  • copy(InputStream, Path, CopyOption)
  • copy(Path, OutputStream)
  • move(Path, Path, CopyOption)
  • delete(Path)
  • deleteIfExists(Path)

Operacje na metadanych plików lub katalogów:

  • size(Path)
  • isDirectory(Path, LinkOption)
  • isRegularFile(Path, LinkOption)
  • isSymbolicLink(Path)
  • isHidden(Path)
  • getLastModifiedTime(Path, LinkOption)
  • setLastModifiedTime(Path, FileTime)
  • getOwner(Path, LinkOption)
  • setOwner(Path, LinkOption)
  • getAttribute(Path, String, LinkOption)
  • setAttribute(Path, String, Object, LinkOption)
  • probeContentType(Path)

Operacje odczytu i zapisu plików

Odczyt i zapis małych plików:

  • readAllBytes(Path)
  • readAllLines(Path, Charset)
  • write(Path, byte[], OpenOperation)
  • write(Path, Iterable< extends CharSequence>, Charset, OpenOption…)

Odczyt i zapis z użyciem BufferedReadera i BufferedWritera – używany z plikami tekstowymi:

  • newBufferedReader(Path, Charset)
  • newBufferedWriter(Path, Charset, OpenOption…)

Odczyt i zapis z użyciem strumieni (kompatybilne z java.io):

  • newInputStream(Path, OpenOption…)
  • newOutputStream(Path, OpenOption…)

Odczyt i zapis z użyciem kanałów (java.nio.Channel):

  • newByteChannel(Path, OpenOption…)
  • newByteChannel(Path, Set, FileAttribute…)

Tworzenie plików:

  • createFile(Path, FileAttribute)
  • createTempFile(Path, String, String, FileAttribute)
  • createTempFile(String, String, FileAttribute)

Operacje na katalogach:

  • getRootDirectories()
  • createDirectory(Path, FileAttribute)
  • createDirectories(Path, FileAttribute)
  • createTempDirectory(Path, String, FileAttribute)
  • createTempDirectory(String, FileAttribute)
  • newDirectoryStream(Path) – pozwala na odczytanie zawartości katalogu – zwraca DirectoryStream
  • newDirectoryStream(Path, String) – zwraca wyfiltrowaną zawartość katalogu

Linki statyczne i linki symboliczne:

  • createSymbolicLink(Path, Path, FileAttribute)
  • createLink(Path, Path)
  • isSymbolicLink(Path)
  • readSymbolicLink(Path)

Przeglądanie drzewa katalogów:

Nowe API przewidziało 2 metody do przeglądania drzewa katalogów/plików. Parametr Path to miejsce startu, FileVisitor to interfejs, którego implementacja określi co ma się dziać podczas chodzenia po katalogach (istnieje domyślna implementacja – SimpleFileVisitor):

  • walkFileTree(Path, FileVisitor)
  • walkFileTree(Path, Set, int, FileVisitor)

Enumy użyte w powyższych metodach:

  • LinkOption (wartości: NOFOLLOW_LINKS)
  • StandardOpenOption (wartości: APPEND, CREATE, CREATE_NEW, DELETE_ON_CLOSE, DSYNC, READ, SPARSE, SYNC, TRUNCATE_EXISTING, WRITE)

Inne:

Poza opisanymi powyżej nowe API dostarcza między innymi metody do wyszukiwania plików, nasłuchiwania na katalogach czy operacji na systemie plików (klasy FileSystem oraz FileStore).

Zwalnianie zasobów

Java w wersji 7 wprowadziła konstrukcję „try with resources”. Konstrukcja ta pozwala na umieszczenie operacji rezerwującej zasoby w try, a w przypadku wystąpienia wyjątku zasoby zostaną zwolnione automatycznie (tak naprawdę to kompilator generuje kod zamykający zasoby gdy nie są one już potrzebne). Zasób musi implementować interfejs java.io.Closable. Zwalnia to programistę z konieczności obsługi zamykania zasobów w bloku finally. Przykład poniżej:

1
2
3
4
5
6
7
Charset charset = Charset.forName("US-ASCII");
String s = ...;
try (BufferedWriter writer = Files.newBufferedWriter(file, charset)) {
writer.write(s, 0, s.length());
} catch (IOException x) {
System.err.format("IOException: %s%n", x);
}

Przykłady

Poniżej kilka przykładów operacji na systemie plików często używanych podczas wytwarzania oprogramowania. Większość metod z klasy Files jest tak oczywista i prosta w użyciu, że ciężko napisać o nich coś więcej niż nazwę metody z parametrami.

Kopiowanie oraz przenoszenie pliku z zachowaniem uprawnień i nadpisaniem pliku docelowego, usuwanie pliku:

1
2
3
4
5
6
7
8
9
10
Path source = Paths.get("c:\\file1.txt");
Path target1 = Paths.get("c:\\file2.txt");
Path target2 = Paths.get("c:\\file3.txt");
try {
Files.copy(source, target1, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
Files.move(source, target2, StandardCopyOption.REPLACE_EXISTING);
Files.delete(target1);
} catch (IOException e) {
e.printStackTrace();
}

Odczyt i zapis z użyciem readAllBytes i write:

1
2
3
4
5
Path file1 = Paths.get("c:\\file1.txt");
Path file2 = Paths.get("c:\\file2.txt");
byte[] fileArray;
fileArray = Files.readAllBytes(file1);
Files.write(file2, fileArray, StandardOpenOption.CREATE_NEW);

Odczyt z użyciem BufferedReader:

1
2
3
4
5
6
7
8
9
10
Path file = Paths.get("c:\\file1.txt");
Charset charset = Charset.forName("UTF-8");
try (BufferedReader bufferedReader = Files.newBufferedReader(file, charset)) {
String line = null;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException ex) {
ex.printStackTrace();
}

Zapis z użyciem BufferedWriter:

1
2
3
4
5
6
7
8
Path file = Paths.get("c:\\file2.txt");
Charset charset = Charset.forName("UTF-8");
String s = "Atena - NIO.2 test";
try (BufferedWriter writer = Files.newBufferedWriter(file, charset)) {
writer.write(s, 0, s.length());
} catch (IOException ex) {
ex.printStackTrace();
}

Odczytanie metadanych pliku:

1
2
3
4
5
6
7
8
9
10
Path file = Paths.get("c:\\file1.txt");
BasicFileAttributes attr = Files.readAttributes(file, BasicFileAttributes.class);
System.out.println("isDirectory: " + attr.isDirectory());
System.out.println("isOther: " + attr.isOther());
System.out.println("isRegularFile: " + attr.isRegularFile());
System.out.println("isSymbolicLink: " + attr.isSymbolicLink());
System.out.println("creationTime: " + attr.creationTime());
System.out.println("lastAccessTime: " + attr.lastAccessTime());
System.out.println("lastModifiedTime: " + attr.lastModifiedTime());
System.out.println("size: " + attr.size());

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *