Niniejszy rozdział jest w przeważającej części poświęcony jednej klasie: java.io.File
. Klasa ta pozwala na tworzenie listy dostępnych katalogów, określanie statusu plików, zmianę nazwy i usuwanie plików przechowywanych na dysku, tworzenie katalogów oraz wykonywanie wielu innych operacji na systemie plików. W niektórych systemach operacyjnych wiele możliwości funkcjonalnych tej klasy można by określić mianem „funkcji systemowych”; Java zapewnia ich przenośność, oczywiście w stopniu, w jakim jest to możliwe.
Należy zauważyć, że wiele spośród metod tej klasy próbuje modyfikować informacje przechowywane w pamięci zbioru plików, czyli w systemie plików w komputerze, gdzie jest wykonywany program. Oczywiście, program może nie dysponować uprawnieniami koniecznymi do wprowadzania określonych modyfikacji w niektórych plikach. Przy wykorzystaniu klasy SecurityManager
wirtualnej maszyny Javy można wykryć, czy podane zmiany mogą być wprowadzone, czy nie. Klasa ta zgłasza nieprzechwytywany wyjątek SecurityException
. Niemniej jednak niepowodzenia wykonania pewnych operacji mogą być także wykrywane przez system operacyjny, w jakim działa wirtualna maszyna Javy. Jeśli menedżer zabezpieczeń zezwoli na wykonanie jakiejś operacji, jednak użytkownik wykonujący program nie dysponuje uprawnieniami koniecznymi do jej wykonania, na przykład określenia zawartości katalogu, to zostanie zwrócona odpowiednia informacja (taka jak wartość false
) lub będzie zgłoszony obsługiwany wyjątek IOException
. Wyjątek ten należy przechwytywać (lub deklarować w klauzuli throws
) w każdym fragmencie kodu wywołującym jakiekolwiek metody operujące na systemie plików.
W wersji języka Java 7 została wprowadzona klasa Path
stanowiąca potencjalny zamiennik klasy File
; została ona przedstawiona dokładniej w dalszej części rozdziału.
Klasa File
posiada kilka metod „informacyjnych”. Aby użyć którejkolwiek z nich, należy stworzyć obiekt File
zawierający nazwę pliku, na którym chcemy operować. Koniecznie należy zaznaczyć, że tworzenie obiektów File
nie ma żadnego wpływu na system plików — są to jedynie obiekty przechowywane w pamięci wirtualnej maszyny Javy. Zmiany w systemie plików są wprowadzane wyłącznie jako rezultat wywoływania metod obiektu File
. Jak się wkrótce przekonamy, istnieje wiele metod pozwalających na modyfikowanie systemu plików, między innymi pozwalają one na tworzenie nowego (pustego) pliku bądź zmianę nazwy istniejącego pliku, oraz wiele metod, które wyłącznie zwracają różnego typu informacje. Niektóre metody zwracające informacje o plikach zostały przedstawione w Tabela 11-1.
Tabela 11-1. Metody klasy java.io.File
Typ wyniku | Nazwa metody | Znaczenie |
---|---|---|
|
| Zwraca wartość |
|
| Zwraca pełną nazwę. |
|
| Zwraca względną nazwę pliku. |
|
| Zwraca nazwę katalogu nadrzędnego. |
|
| Zwraca wartość |
|
| Zwraca wartość |
|
| Zwraca czas modyfikacji pliku. |
|
| Zwraca wielkość pliku. |
|
| Zwraca wartość |
|
| Zwraca wartość |
Nazwy zapisanej w obiekcie File
nie można zmienić. Za każdym razem, gdy należy się odwołać do innego pliku, trzeba utworzyć nowy obiekt.
/dir_file/FileStatus.java
public class FileStatus { public static void main(String[] argv) throws IOException { // Zapewniamy, że nazwa pliku (lub innego elementu systemu plików) // została podana jako argv[0]. if (argv.length == 0) { System.err.println("Sposób użycia: status nazwa_pliku"); System.exit(1); } for (String a : argv) { status(a); } } public static void status(String fileName) throws IOException { System.out.println("---" + fileName + "---"); // Tworzymy obiekt File reprezentujący dany plik. File f = new File(fileName); // Sprawdzamy, czy plik faktycznie istnieje. if (!f.exists()) { System.out.println("Nie znaleziono pliku."); System.out.println(); // Pusty wiersz. return; } // Wyświetlamy pełną nazwę. System.out.println("Nazwa kanoniczna " + f.getCanonicalPath()); // Jeśli to możliwe, wyświetlamy katalog nadrzędny. String p = f.getParent(); if (p != null) { System.out.println("Katalog nadrzędny: " + p); } // Czy zawartość pliku można odczytać? if (f.canRead()) { System.out.println("Zawartość pliku można odczytać."); } // Czy można zapisywać w pliku? if (f.canWrite()) { System.out.println("W pliku można zapisywać."); } // Informacje na temat czasu modyfikacji. Date d = new Date(f.lastModified()); System.out.println("Data ostatniej modyfikacji to " + d); // Sprawdzamy, czy to katalog bądź inny element systemu plików. // Jeśli jest to plik, wyświetlamy jego wielkość. if (f.isFile()) { // Zwracamy informacje o wielkości pliku. System.out.println("Plik ma wielkość " + f.length() + " bajtów."); } else if (f.isDirectory()) { System.out.println("To katalog"); } else { System.out.println("Nie potrafię określić typu! Nie jest to ani plik, ani katalog!"); } System.out.println(); // Pusty wiersz między informacjami // o kolejnych elementach systemu plików. } }
W przypadku przekazania w wywołaniu programu trzech poniższych argumentów generuje on następujące wyniki:
C:\javasrc\dir_file>java dir_file.FileStatus / /tmp/id /autoexec.bat
---/---
Nazwa kanoniczna C:\
Zawartość pliku można odczytać.
W pliku można zapisywać.
Data ostatniej modyfikacji to Mon Jan 01 01:00:00 CET 1970
To katalog
---/tmp/id---
Nie udało się odnaleźć pliku
---/autoexec.bat---
Nazwa kanoniczna C:\AUTOEXEC.BAT
Katalog nadrzędny: \
Zawartość pliku można odczytać.
W pliku można zapisywać.
Data ostatniej modyfikacji to Mon Sep 30 13:29:22 CEST 2002
Plik ma wielkość 10 bajtów.
Jak widać, tak zwana „nazwa kanoniczna” nie tylko zawiera określenie katalogu głównego (C:\), lecz także nazwę pliku zapisaną wielkimi literami. Można powiedzieć, że to dlatego, iż program został uruchomiony w systemie MS Windows. W systemach uniksowych program zachowuje się nieco inaczej:
$ java dir_file.FileStatus / /tmp/id /autoexec.bat
---/---
Nazwa kanoniczna /
Zawartość pliku można odczytać.
Data ostatniej modyfikacji to October 4, 1999 6:29:14 AM PDT
To katalog
---/tmp/id---
Nazwa kanoniczna /tmp/id
Katalog nadrzędny: /tmp
Zawartość pliku można odczytać.
W pliku można zapisywać.
Data ostatniej modyfikacji to October 8, 1999 1:01:54 PM PDT
Plik ma wielkość 0 bajtów.
---/autoexec.bat---
Nie udało się odnaleźć pliku
$
W typowych systemach Unix nie ma plików autoexec.bat. Co więcej, w systemach tych nazwy plików mogą zawierać zarówno duże, jak i małe litery (podobnie jak na komputerach Macintosh) — uzyskane wyniki zależą zatem od sposobu, w jaki zostanie zapisana nazwa pliku.
Nowy plik można w prosty sposób utworzyć poprzez zbudowanie obiektu klasy FileOutputStream
lub FileWriter
(patrz „10.8. Otwieranie pliku o podanej nazwie”). Jednak w tych przypadkach nie trzeba pamiętać o jawnym zamykaniu pliku. Może się zdarzyć, że będziemy chcieli jedynie utworzyć plik bez zapisywania w nim jakichkolwiek danych. Rozwiązanie to można wykorzystać na przykład jako bardzo prosty sposób wymiany informacji pomiędzy programami: pierwszy program może sprawdzać, czy określony plik istnieje, a fakt jego istnienia interpretować jako osiągnięcie pewnego stanu przez drugi program. Poniżej przedstawiłem kod, który tworzy pusty plik o podanej nazwie:
/dir_file/Creat.java
import java.io.*; /** * Tworzymy jeden lub kilka plików o podanych nazwach. * Ostatnie "e" zostało pominięte w hołdzie dla wywołań systemowych Uniksa. */ public class Creat { public static void main(String[] argv) throws IOException { // Upewniamy się, że w argv[0] została podana nazwa pliku // lub innego elementu systemu plików. if (argv.length == 0) { System.err.println("Sposób użycia: Creat nazwa_pliku"); System.exit(1); } for (String a : argv) { // Stworzenie obiektu File nie ma wpływu na system plików, // natomiast wywołanie metody createNewFile() ma wpływ. new File(a).createNewFile(); } } }
Ze względów znanych jedynie twórcom języka Java metoda renameTo()
wymaga podania nowego obiektu File
reprezentującego plik o nowej nazwie, a nie — jak można by się spodziewać — łańcucha znaków określającego nową nazwę pliku. Aby zatem zmienić nazwę pliku, należy utworzyć dwa obiekty File
— jeden reprezentujący istniejący plik oraz drugi reprezentujący plik o nowej nazwie. Następnie wywołujemy metodę renameTo()
obiektu reprezentującego istniejący plik, przekazując w jej wywołaniu drugi obiekt File
. Właściwie łatwiej to pokazać na przykładzie, niż opisać, a zatem oto przykład:
/dir_file/Rename.java
public class Rename { public static void main(String[] argv) throws IOException { // Tworzymy obiekt File; czynność ta NIE powoduje utworzenia pliku na dysku! File f = new File("Rename.java~"); // Kopia tego pliku źródłowego. // Zmieniamy nazwę kopii pliku na "junk.dat". // Zmiana nazwy wymaga użycia obiektu File reprezentującego // plik o nowej nazwie. f.renameTo(new File("junk.dat")); } }
Należy się posłużyć metodą delete()
klasy java.io.File
; za jej pomocą można usuwać pliki (dysponując odpowiednimi uprawnieniami) oraz katalogi (dysponując koniecznymi uprawnieniami oraz pod warunkiem, że usuwany katalog będzie pusty).
Ta czynność nie jest szczególnie złożona. Wystarczy utworzyć obiekt File
reprezentujący plik, jaki należy usunąć, a następnie wywołać jego metodę delete()
:
/dir_file/Delete.java
public class Delete { public static void main(String[] argv) throws IOException { // Tworzymy obiekt File reprezentujący plik kopii zapasowej // bieżącego pliku (utworzonej automatycznie podczas edycji // tego pliku). Ta kopia zapewne już istnieje. // Używany przeze mnie edytor tworzy kopie plików, dodając do // ich oryginalnej nazwy znak "~". File bkup = new File("Delete.java~"); // A teraz usuwamy plik. bkup.delete(); } }
Warto przypomnieć sobie informację dotyczącą praw dostępu podaną we wprowadzeniu na samym początku tego rozdziału. Otóż jeśli nie dysponujemy uprawnieniami koniecznymi do wykonania danej czynności, to realizująca ją metoda zwróci wartość false
lub zgłosi wyjątek SecurityException
. Należy także zwrócić uwagę na różnice występujące pomiędzy poszczególnymi systemami operacyjnymi. Niektóre wersje systemu Windows pozwalają Javie na usuwanie plików przeznaczonych tylko do odczytu, natomiast w systemach Unix nie można usuwać plików, jeśli nie mamy prawa zapisu do katalogu, w którym się one znajdują, ani usuwać katalogów, które nie są puste. Poniżej przedstawiłem drugą wersję programu Delete
, wyposażoną w mechanizmy kontroli błędów (i oczywiście wyświetlania informacji o nich):
/dir_file/Delete2.java
public class Delete2 { public static void main(String[] argv) { for (String a : argv) { delete(a); } } public static void delete(String fileName) { try { // Tworzymy obiekt File reprezentujący plik, który // chcemy usunąć. File target = new File(fileName); if (!target.exists()) { System.err.println("Plik " + fileName + " nie jest dostępny!"); return; } // Teraz usuwamy plik: if (target.delete()) System.err.println("** Usunięto " + fileName + " **"); else System.err.println("Nie udało się usunąć " + fileName); } catch (SecurityException e) { System.err.println("Nie udało się usunąć " + fileName + "(" + e.getMessage() + ")"); } } }
Po uruchomieniu tego programu można uzyskać następujące wyniki:
$ ls -ld ? -rw-r--r-- 1 ian ian 0 Oct 8 16:50 a drwxr-xr-x 2 ian ian 512 Oct 8 16:50 b drwxr-xr-x 3 ian ian 512 Oct 8 16:50 c $ java dir_file.Delete2 ? ** Usunięto a ** ** Usunięto b ** Nie udało się usunąć c $ ls -l c total 2 drwxr-xr-x 2 ian ian 512 Oct 8 16:50 d $ java dir_file.Delete2 c/d c ** Usunięto c/d ** ** Usunięto c ** $
Należy zauważyć, że w systemie Unix znaki specjalne powłoki systemowej, takie jak ?
, są przekształcane przez powłokę systemową (interpreter poleceń) na listę nazw plików jeszcze przed uruchomieniem programu. Natomiast w systemie Windows przekształcenie to może być przeprowadzane przez środowisko wykonawcze Javy.
Musimy stworzyć plik o unikalnej tymczasowej nazwie lub przygotować się na usunięcie pliku podczas zamykania programu.
Klasa File
udostępnia dwie metody — createTempFile()
oraz deleteOnExit()
. Pierwsza z nich tworzy plik o unikalnej nazwie (w razie gdyby ten sam program był jednocześnie wykonywany na serwerze przez kilku użytkowników), natomiast druga zapewnia, że dowolny podany plik (bez względu na to, w jaki sposób został on utworzony) zostanie usunięty w momencie zakończenia działania programu. W poniższym przykładzie nakazujemy usunięcie kopii programu oraz tworzymy plik tymczasowy, który także ma zostać usunięty w odpowiednim czasie. Po zakończeniu programu oba wskazane pliki już nie istnieją.
/dir_file/TempFiles.java
public class TempFiles { public static void main(String[] argv) throws IOException { // 1. Określamy, że istniejący plik będzie plikiem tymczasowym. // Tworzymy obiekt File dla kopii zapasowej utworzonej poprzez // edycję tego pliku źródłowego. Ta kopia najprawdopodobniej już // istnieje. Używany przeze mnie edytor tworzy takie kopie, // dodając znak "~" na końcu nazwy oryginalnego pliku. File bkup = new File("Rename.java~"); // Nakazujemy usunięcie tego pliku pod koniec działania programu. bkup.deleteOnExit(); // 2. Tworzymy nowy plik tymczasowy. // Tworzymy obiekt File dla pliku foo.tmp, umieszczonego // w domyślnym katalogu tymczasowym (tmp). File tmp = File.createTempFile("foo", "tmp"); // Wyświetlamy nazwę, jaka została nadana plikowi tymczasowemu. System.out.println("Używamy pliku tymczasowego: " + tmp.getCanonicalPath()); // Nakazujemy usunięcie pliku podczas kończenia programu. tmp.deleteOnExit(); // Teraz można w dowolny sposób wykorzystać plik tymczasowy, // bez konieczności pamiętania o tym, że należy go usunąć. writeDataInTemp(tmp.getCanonicalPath()); } public static void writeDataInTemp(String tempnam) { // Ta metoda jest pusta... można ją wykorzystać // wedle własnego uznania. } }
Warto zwrócić uwagę, że metoda createTempFile()
nie tworzy pliku na dysku, a zatem pod tym względem przypomina metodę createNewFile()
(patrz „11.2. Tworzenie pliku”). Należy także pamiętać, że w przypadku nagłego zakończenia działania wirtualnej maszyny Javy operacja usunięcia pliku tymczasowego najprawdopodobniej nie zostanie wykonana. I ostatnia sprawa: jeśli nakażemy usunięcie pliku przy wykorzystaniu metody deleteOnExit()
, to nie ma żadnego sposobu „odwołania” tego polecenia za wyjątkiem drastycznych rozwiązań, takich jak wyłączenie zasilania w komputerze przed zakończeniem działania programu.
Oprócz tego metoda deleteOnExit()
nie jest zapewne tym, czego chcielibyśmy używać w aplikacjach działających przez długi czas czy na przykład w większości aplikacji serwerowych. W takich przypadkach serwer mógłby działać przez wiele tygodni, miesięcy lub nawet lat, a w międzyczasie wszystkie pliki tymczasowe byłyby gromadzone, zaś JVM tworzyłoby długą listę prac, które ma wykonać podczas zamykania aplikacji. W takim przypadku mogłoby nam zabraknąć miejsca na dysku, pamięci lub jakichś innych zasobów systemowych. Dlatego w aplikacjach działających przez długi czas znacznie lepszym rozwiązaniem jest jawne używanie operacji delete()
bądź też skorzystanie z usługi cyklicznego uruchamiania zadań, by co jakiś czas usuwać stare pliki tymczasowe.
Zgodnie z informacjami podanymi w „11.1. Pobieranie informacji o pliku” dostępnych jest wiele metod zwracających informacje na temat pliku. Jednak metod pozwalających ma modyfikowanie plików jest stosunkowo niewiele.
Metoda setReadOnly()
ustawia atrybut „tylko do odczytu” dla podanego pliku lub katalogu. Metoda ta zwraca wartość true
, jeśli udało się zmienić ten atrybut, oraz wartość false
w przeciwnym przypadku.
Metoda setLastModified()
pozwala na zabawy z określaniem czasu modyfikacji pliku. Zazwyczaj nie są one wskazane, lecz przydają się w niektórych programach służących do tworzenia kopii bezpieczeństwa oraz ich odtwarzania. Metoda ta wymaga podania jednego argumentu wyrażającego liczbę milisekund (nie sekund), jaka upłynęła od początku czasu (czyli od 1 stycznia 1970 roku). Wartość tę dla wybranego pliku można pobrać przy użyciu metody getLastModified()
(patrz „11.1. Pobieranie informacji o pliku”), z kolei wartość reprezentującą konkretną datę można pobrać, wywołując metodę toInstant().getEpochSecond()
klasy ZonedDateTime
i mnożąc ją przez 1000
w celu uzyskania czasu wyrażonego w milisekundach. W przypadku prawidłowego wykonania metoda setLastModified()
zwraca true
, a w przeciwnym razie — false
.
Interesujące jest, że według dokumentacji języka Java „obiekty File
są niezmienne”, co oznacza, że nie można zmieniać wartości ich pól. Czy jednak wywołanie metody setReadOnly()
nie powoduje zmiany wartości zwracanej przez metodę canRead()
? Przekonajmy się:
/dir_file/ReadOnly.java
public class ReadOnly { public static void main(String[] a) throws IOException { File f = new File("f"); if (!f.createNewFile()) { System.out.println("Nie można utworzyć nowego pliku."); return; } if (!f.canWrite()) { System.out.println("Nie można zapisywać danych w nowym pliku!"); return; } if (!f.setReadOnly()) { System.out.println( "Wrrr! Nie można ustawić atrybutu \"tylko do odczytu\"."); return; } if (f.canWrite()) { System.out.println("Niemodyfikowalny, kapitanie!"); System.out.println("Ale wciąż po wywołaniu setReadOnly() metoda canWrite() zwraca true."); return; } else { System.out.println("Logiczne, kapitanie!"); System.out.println( "canWrite() zwraca false po wywołaniu setReadOnly()."); } } }
Wykonanie tego programu zwraca wyniki, których się spodziewałem (mam nadzieję, że Czytelnik również):
$ javac -d dir_file/ReadOnly.java $ java dir_file.ReadOnly Logiczne, kapitanie! canWrite() zwraca false po wywołaniu setReadOnly(). $
A zatem stwierdzenie, że obiektów File
nie można modyfikować po utworzeniu, dotyczy wyłącznie ścieżki dostępu do pliku, a nie jego atrybutów. Jest to oczywiście zupełnie zrozumiałe, gdyż w trakcie używania obiektu File
w jednej aplikacji inna mogła zmienić prawa dostępu do pliku; miałoby to dokładnie taki sam efekt jak działanie powyższej, niewielkiej aplikacji.
Klasa java.io.File
udostępnia kilka metod przeznaczonych do wykonywania operacji związanych z katalogami. Na przykład w celu sporządzenia listy elementów systemu plików dostępnych w katalogu wystarczy użyć następującego wywołania:
String names = new File(".").list();
Aby pobrać tablicę już utworzonych obiektów File
, a nie łańcuchów znaków, można się posłużyć poniższą instrukcją:
File[] list = new File(".").listFiles();
Przekształcenie tej instrukcji w pełny program wyświetlający zawartość katalogu nie wymaga wiele pracy:
/dir_file/Ls.java
public class Ls { public static void main(String args[]) { String[] dirs = new java.io.File(".").list(); // Pobranie listy nazw. Arrays.sort(dirs); // Sortowanie (patrz rozdział 7.) for (String dir : dirs) { System.out.println(dir); // Wyświetlenie nazw z listy. } } }
Oczywiście powyższy program daje ogromne możliwości rozbudowy. Nazwy plików można wyświetlać w kilku kolumnach. Można je wyświetlać w układzie pionowym, gdyż liczba prezentowanych elementów jest znana. Można także pomijać pliki, których nazwy rozpoczynają się od kropki, czyli tak jak robi to program ls
stosowany w systemach Unix. Można w pierwszej kolejności wyświetlać nazwy katalogów; kiedyś używałem programu służącego do wyświetlania zawartości katalogów, który działał właśnie w taki sposób, i doszedłem do wniosku, że jest to całkiem przydatne rozwiązanie. Używając metody listFiles()
, która tworzy obiekt File
dla każdej pobranej nazwy pliku, można wyświetlać dodatkowe informacje o plikach, na przykład ich wielkość; w taki sposób działa polecenie dir
w systemie DOS oraz uniksowy program ls
wywołany z opcją -l
(patrz „11.1. Pobieranie informacji o pliku”). Można także sprawdzać, czy poszczególne nazwy reprezentują pliki, katalogi, czy też jeszcze inne elementy systemu plików. W takim przypadku nazwy katalogów można by przekazywać w wywołaniu metody głównej i rekurencyjnie wyświetlać zawartość całego drzewa katalogów (w taki sposób działają uniksowe polecenia find
lub ls -R
bądź używane w systemie DOS polecenie dir /s
).
Bardziej elastycznym sposobem wyświetlenia listy zawartości systemu plików jest wykorzystanie metody list(FilenameFilter ff)
. FilenameFilter
jest niewielkim interfejsem zawierającym tylko jedną metodę: boolean accept(File inDir, String fileName)
. Załóżmy, że chcielibyśmy wyświetlić wyłącznie pliki związane z językiem Java (*.java, *.class, *.jar i tak dalej). Wystarczy stworzyć metodę accept()
, która będzie zwracać wartość true
dla interesujących nas plików oraz wartość false
w przeciwnych przypadkach. Poniżej przedstawiłem zmodyfikowaną wersję klasy Ls
wzbogaconą o możliwość wykorzystania obiektu FilenameFilter
implementowanego przez moją klasę OnlyJava
, która tworzy listę wybranych plików:
/dir_file/FNFilter.java
public class FNFilter { public static void main(String argh_my_aching_fingers[]) { // Tworzymy listę plików, wykorzystując przy tym obiekt File // przeznaczony do jednokrotnego użycia. String[] dirs = new java.io.File(".").list(new OnlyJava()); Arrays.sort(dirs); // Sortowanie (patrz rozdział 7.) for (String d : dirs) { System.out.println(d); // Wyświetlamy listę. } } /** Ta klasa implementuje interfejs FilenameFilter. * Metoda accept zwraca wartość true wyłącznie dla plików .java i .class. */ private static class OnlyJava implements FilenameFilter { public boolean accept(File dir, String s) { if (s.endsWith(".java") || s.endsWith(".class") || s.endsWith(".jar")) { return true; } // Inne pliki: projekty...? return false; } } }
Interfejs FilenameFilter
nie musi być implementowany w osobnej klasie. Kolejny program FNFilter2
, który można znaleźć wśród pozostałych przykładów dołączonych do tej książki, pokazuje, w jaki sposób można zaimplementować ten interfejs bezpośrednio w klasie głównej. Można zastosować także anonimową klasę wewnętrzną. Zamieszczony poniżej program FNFilterL
pokazuje wykorzystanie takiej klasy w formie wyrażenia lambda, które pozwala skrócić kod w jeszcze większym stopniu:
/dir_file/FNFilterL.java
// Generujemy wybiórczą listę, używając wyrażenia lambda. String[] dirs = new java.io.File(dirName).list( (dir, s) -> { return s.endsWith(".java") || s.endsWith(".class") || s.endsWith(".jar"); } ); Arrays.sort(dirs); // Sortowanie (patrz rozdział 7.) for (String d : dirs) { System.out.println(d); // Wyświetlamy listę. }
W normalnej, dużej aplikacji lista nazw plików akceptowanych przez obiekt FilenameFilter
byłaby zapewne określana dynamicznie, na podstawie aktualnie wykonywanych operacji i przetwarzanych danych. Jak się przekonamy w „14.13. Wybieranie plików przy użyciu klasy JFileChooser”, okno dialogowe służące do wyboru plików rozszerza funkcjonalność tego interfejsu, dając użytkownikom możliwość wyboru jednej z kilku dostępnych grup plików. Rozwiązanie takie znacznie ułatwia odnajdywanie interesujących nas plików, podobnie jak w naszym przypadku pozwala ono na ograniczenie ilości przetwarzanych plików.
Chcemy zdobyć informacje o wszystkich dostępnych katalogach głównych, takich jak C:\, D:\ itd., w systemach Windows.
Skoro zajmujemy się sporządzaniem list zawartości folderów, na pewno wiemy, że wszystkie nowoczesne systemy operacyjne przeznaczone dla komputerów biurowych prezentują zawartość dysków w postaci hierarchicznego drzewa katalogów. Jednak być może nie wiemy jeszcze, że w systemach Unix wszystkie pliki znajdują się „poniżej” jednego, wspólnego katalogu głównego określanego jako \
; z kolei w systemach operacyjnych firmy Microsoft katalog główny określany jako \
znajduje się na każdym dysku (dla pierwszej stacji dyskietek — jeśli jeszcze taką posiadasz! — jest on określany jako A:\
, dla pierwszego dysku twardego — C:\
, a kolejne litery są przydzielane następnym napędom i urządzeniom sieciowym). Jeśli należy zdobyć informacje o wszystkich plikach przechowywanych na wszystkich dyskach, w pierwszej kolejności trzeba będzie zdobyć informacje o wszystkich „katalogach głównych” dostępnych w danym systemie. Listę nazw wszystkich katalogów głównych dostępnych w danym systemie można pobrać przy użyciu metody listRoots()
, która zwraca tablicę obiektów File
. Poniżej przedstawiony został krótki program wyświetlający listę dostępnych katalogów głównych oraz wyniki jego działania.
/dir_file/DirRoots.java
public class DirRoots { public static void main(String argh_my_aching_fingers[]) { File[] drives = File.listRoots(); // Pobieramy listę nazw. for (File dr : drives) { System.out.println(dr); // Wyświetlamy nazwy z listy. } } }
C:> java dir_file.DirRoots
A:\
C:\
D:\
c:>
Jak widać, program wyświetlił informację o stacji dyskietek (choć była ona nie tylko pusta, lecz, co więcej, została w domu, gdy ja pisałem tę recepturę na komputerze przenośnym, siedząc w aucie na parkingu), dysku twardym oraz napędzie CD-ROM.
W systemach Unix jest dostępny tylko jeden katalog główny:
$ java dir_file.DirRoots
/
$
Elementami, które nie są wyświetlane na liście katalogów głównych, są nazwy plików UNC. Są one stosowane w systemach MS Windows w celu odwoływania się do zasobów sieciowych, które nie zostały dołączone lokalnie i skojarzone z literą oznaczającą dysk. Na przykład mój serwer (działający w systemie Unix, na którym zainstalowałem oprogramowanie serwera Samba SMB) nosi nazwę darian
(co stanowi połączenie pierwszych liter mojego nazwiska i imienia), a jego katalog główny został wyeksportowany i udostępniony pod nazwą ian
. Dzięki temu mogę się odwołać do katalogu books w katalogu głównym na tym serwerze, używając w tym celu nazwy UNC \\darian\ian\book. Taka nazwa jest całkowicie poprawna w Javie (zakładając, że wirtualna maszyna Javy działa na komputerze z systemem operacyjnym Windows), niemniej jednak metoda File.listRoots()
nie zwróci żadnych informacji o niej.
Dostępne są dwie metody służące do tworzenia katalogów; pierwsza z nich — mkdir()
— tworzy tylko jeden katalog, natomiast druga — mkdirs()
— tworzy także wszystkie niezbędne katalogi nadrzędne. Zakładając na przykład, że istnieje katalog /home/ian, następujące wywołania:
new File("/home/ian/bin").mkdir(); new File("/home/ian/src").mkdir();
zakończą się pomyślnie. Natomiast wywołanie:
new File("/home/ian/once/twice/again").mkdir();
zakończy się niepowodzeniem, zakładając, że katalog once nie istnieje. Jeśli należy stworzyć całą ścieżkę katalogów, to można to zrobić za jednym razem, wykorzystując w tym celu metodę mkdirs()
klasy File
:
new File("/home/ian/once/twice/again").mkdirs();
Obie metody zwracają wartość true
, jeśli udało się utworzyć żądany katalog lub katalogi, oraz wartość false
w przypadku przeciwnym. Warto zauważyć, że możliwa (choć mało prawdopodobna) jest sytuacja, w której metoda mkdirs()
utworzy kilka katalogów, a następnie zwróci wartość false
. W takim przypadku utworzone katalogi pozostaną dostępne w systemie plików.
Warto także zwrócić uwagę na sposób zapisu nazwy metody — mkdir()
— jak widać, jest ona zapisywana wyłącznie małymi literami. Oczywiście można by stwierdzić, że nie jest to zgodne z zasadami nazewnictwa metod stosowanymi w języku Java (w myśl których metoda ta powinna nosić nazwę mkDir()
), niemniej jednak taka nazwa odpowiada zarówno funkcji systemowej służącej do tworzenia katalogów, jak i poleceniom służącym do tego celu — i to zarówno w systemach Unix, jak i DOS (choć w systemie DOS można się także posłużyć poleceniem o nazwie md
).
Potrzebujemy więcej możliwości w porównaniu z tymi, które zapewnia standardowa klasa File
. Musimy przenosić, kopiować, usuwać pliki oraz wykonywać na nich wszelkie inne operacje, najlepiej minimalnym nakładem pracy.
Warto się zastanowić nad wykorzystaniem klasy Path
, stworzonej jako zamiennik dla klas File
oraz Files
.
Obiekty Path
realizują wiele spośród operacji udostępnianych wcześniej przez obiekty File
. W najprostszym przypadku sposób korzystania z nich jest bardzo podobny, z tą różnicą, że obiekty nie są tworzone bezpośrednio, lecz przy użyciu metody wytwórczej klasy Paths
:
Path p = Paths.getPath("/home/ian/.profile"); if (!p.exists()) { // Tu wyświetlamy jakieś ostrzeżenie } else { // Używamy metody p.size() itd. }
Jedną z możliwości klasy Path
, która nie była dostępna w klasach stanowiących jej pierwowzory, jest możliwość skonfigurowania (przy użyciu metody register()
) usługi obserwującej (ang. Watcher Service, opisanej dokładniej w „11.11. Stosowanie usługi WatchService do uzyskiwania informacji o zmianach pliku”), która informuje o zmianach w wybranym elemencie systemu plików (takich jak nowe pliki tworzone w katalogu).
Klasa Files
udostępnia grupę metod służących do operacji na ścieżkach dostępu do plików, pozwalających na wykonywanie takich operacji jak:
kopiowanie,
przenoszenie,
usuwanie.
Przedstawiony poniżej program PathsFilesDemo
pokazuje sposób wykonywania niektórych spośród tych operacji:
/dir_files/PathsFilesDemo.java
Path p = Paths.get("my_junk_file"); ➊ boolean deleted = Files.deleteIfExists(p); ➋ InputStream is = ➌ PathsFilesDemo.class.getResourceAsStream("/demo.txt"); long newFileSize = Files.copy(is, p); ➍ System.out.println(newFileSize); ➎ final Path realPath = p.toRealPath(); ➏ System.out.println(realPath); realPath.forEach(pc-> System.out.println(pc)); ➐ Files.delete(p); ➑
➊ Tworzy abstrakcyjną ścieżkę reprezentowaną przez obiekt Path
.
➋ Wywołanie zapewnia, że plik nie istnieje.
➌ Wywołanie pobiera strumień InputStream
pozwalający na skopiowanie zawartości pliku.
➍ Wywołanie kopiuje całą zawartość pliku.
➎ To wywołanie wyświetla wielkość pliku.
➏ Wywołanie pobiera pełną ścieżkę.
➐ To wywołanie wyświetla wszystkie elementy ścieżki (katalogi i nazwę pliku).
➑ Wywołanie usuwa utworzony wcześniej obiekt.
Chcemy być informowani o wszelkich zmianach wprowadzanych przez inne aplikacje w jednym lub kilku interesujących nas plikach.
Należy skorzystać z możliwości interfejsu WatchService
dostępnego w wersji Java 7, by automatycznie otrzymywać informacje o zmianach w plikach, dzięki czemu nie trzeba robić tego samodzielnie.
Stosunkowo często zdarza się, że duże aplikacje muszą być informowane o zmianach zachodzących w plikach, przy czym dla ich twórców najlepiej by było, gdyby nie musieli cyklicznie sprawdzać tego samemu. Na przykład serwer korporacyjny może być zainteresowany otrzymywaniem informacji o aktualizacjach serwletów oraz wszelkich innych komponentów. Wiele nowoczesnych systemów operacyjnych już od dłuższego czasu dysponuje takimi możliwościami, a teraz stały się one dostępne także w Javie.
Poniżej przedstawione zostały podstawowe czynności związane z korzystaniem z usługi WatchService
:
Utworzenie obiektu Path
reprezentującego katalog, który należy obserwować.
Pobranie obiektu WatchService
poprzez wywołanie na przykład metody FileSystems.getDefault().newWatchService()
.
Utworzenie tablicy z wartościami typu wyliczeniowego Kind
, określającej, jakie zmiany nas interesują (w naszym przykładzie będzie to utworzenie nowego pliku bądź modyfikacja pliku już istniejącego).
Zarejestrowanie obiektu WatchService
oraz tablicy typu Kind
w obiekcie Path
.
Oczekiwanie na nadsyłane powiadomienia. W typowym przypadku jest to realizowane poprzez wykonanie pętli while (true)
, wewnątrz której wywoływana jest metoda take()
obiektu WatchService
. Metoda ta umożliwia pobieranie „zdarzeń” oraz „interpretowanie tego, co się właśnie wydarzyło”.
Przykład 11-1 przedstawia program realizujący opisane wcześniej czynności. Oprócz tego program ten uruchamia dodatkowy wątek, który wykonuje różne operacje na systemie plików, dzięki czemu można obserwować faktyczne działanie usługi WatchService
.
Przykład 11-1. /nio/FileWatchServiceDemo.java
public class FileWatchServiceDemo { final static String tempDirPath = "/tmp"; static Thread mainRunner; static volatile boolean done = false; public static void main(String[] args) throws Throwable { String tempDirPath = "/tmp"; System.out.println("Rozpoczynanie obserwacji katalogu " + tempDirPath); Path p = Paths.get(tempDirPath); WatchService watcher = FileSystems.getDefault().newWatchService(); Kind<?>[] watchKinds = { ENTRY_CREATE, ENTRY_MODIFY }; p.register(watcher, watchKinds); mainRunner = Thread.currentThread(); new Thread(new DemoService()).start(); while (!done) { WatchKey key = watcher.take(); for (WatchEvent<?> e : key.pollEvents()) { System.out.println( "Zdarzenie " + e.kind() + " dotyczące " + e.context()); if (e.context().toString().equals("MyFileSema.for")) { System.out.println( "Znaleziono semafor, zamykanie obserwatora"); done = true; } } if (!key.reset()) { System.err.println("Nie udało się zresetować klucza!"); } } } static class DemoService implements Runnable { public void run() { try { Thread.sleep(1000); System.out.println("Tworzenie pliku"); new File(tempDirPath + "/MyFileSema.for").createNewFile(); Thread.sleep(1000); System.out.println("Zatrzymywanie WatchServiceDemo"); done = true; Thread.sleep(1500); mainRunner.interrupt(); } catch (Exception e) { System.out.println("Przechwycono NIEOCZEKIWANY wyjątek " + e); } } } }
Program przedstawiony w tej recepturze (patrz Przykład 11-2) implementuje niewielki podzbiór możliwości funkcjonalnych okna dialogowego Szukaj używanego w systemach Windows oraz polecenia find
stosowanego w systemach Unix. Struktura programu umożliwia rozbudowanie jego możliwości, tak aby były one bardziej zgodne z narzędziami, na których program jest wzorowany. Program używa filtra nazw, którego postać zależy od opcji -n
podawanej w wierszu wywołania i przetwarzanej przez obiekt GetOpt
(patrz „2.6. Analiza argumentów podanych w wierszu wywołania programu”). Program można rozszerzyć o możliwość filtrowania na podstawie wielkości pliku, lecz jej zaimplementowanie zostało pozostawione jako ćwiczenie do samodzielnego wykonania przez Czytelnika.
Przykład 11-2. /dir_file/Find.java
public class Find { /** Program główny */ public static void main(String[] args) { Find finder = new Find(); GetOpt argHandler = new GetOpt("n:s:"); int c; while ((c = argHandler.getopt(args)) != GetOpt.DONE) { switch(c) { case 'n': finder.filter.setNameFilter(argHandler.optarg()); break; case 's': finder.filter.setSizeFilter(argHandler.optarg()); break; default: System.out.println("Przekazana opcja: " + c); usage(); } } if (args.length == 0 || argHandler.getOptInd()-1 == args.length) { finder.doName("."); } else { for (int i = argHandler.getOptInd()-1; i<args.length; i++) finder.doName(args[i]); } } protected FindFilter filter = new FindFilter(); public static void usage() { System.err.println( "Sposób użycia: Find [-n filtr_nazw][-s filtr_wielkości][katalog...]"); System.exit(1); } /** doName - obsługuje poszukiwanie elementu systemu plików * na podstawie podanej nazwy. */ private void doName(String s) { Debug.println("działamy", "doName(" + s + ")"); File f = new File(s); if (!f.exists()) { System.out.println(s + " nie istnieje"); return; } if (f.isFile()) doFile(f); else if (f.isDirectory()) { // System.out.println("d " + f.getPath()); String objects[] = f.list(filter); for (String o : objects) doName(s + File.separator + o); } else System.err.println("Nieznany typ: " + s); } /** doFile - obsługuje jeden zwyczajny plik. */ private static void doFile(File f) { System.out.println("f " + f.getPath()); } }
Przykład 11-3 przedstawia klasę FindFilter
, która stanowi implementację interfejsu FilenameFilter
i odpowiada za selekcję plików.
Przykład 11-3. /dir_file/FindFilter.java
public class FindFilter implements FilenameFilter { boolean sizeSet; int size; String name; Pattern nameRE; boolean debug = false; void setSizeFilter(String sizeFilter) { size = Integer.parseInt(sizeFilter); sizeSet = true; } /** Konwertuje podany wzorzec znaków wieloznacznych na postać * używaną wewnętrznie - czyli wyrażenie regularne. */ void setNameFilter(String nameFilter) { name = nameFilter; StringBuilder sb = new StringBuilder('^'); for (char c : nameFilter.toCharArray()) { switch(c) { case '.': sb.append("\\."); break; case '*': sb.append(".*"); break; case '?': sb.append('.'); break; // Niektóre znaki mają specjalne znaczenie w wyrażeniach // regularnych i muszą być odpowiednio oznaczane. case '[': sb.append("\\["); break; case ']': sb.append("\\]"); break; case '(': sb.append("\\("); break; case ')': sb.append("\\)"); break; default: sb.append(c); break; } } sb.append('$'); if (debug) System.out.println("RE=\"" + sb + "\"."); try { nameRE = Pattern.compile(sb.toString()); } catch (PatternSyntaxException ex) { System.err.println("Błąd: nie udało się skompilować " + " wyrażenia regularnego: " + sb.toString() + " : " + ex); } } /** Realizacja filtrowania. W tym przypadku wyłącznie * na podstawie nazwy. */ public boolean accept(File dir, String fileName) { File f = new File(dir, fileName); if (f.isDirectory()) { return true; // Zezwalamy na rekurencję. } if (nameRE != null) { return nameRE.matcher(fileName).matches(); } // TODO Obsługa wielkości plików. // Cała reszta... return false; } public String getName() { return name; } }
Mam jeszcze ćwiczenie dla Czytelników: w przykładach dołączonych do niniejszej książki w katalogu zawierającym przykłady przedstawione w bieżącym rozdziale znajduje się także klasa o nazwie FindNumFilter
. Klasa ta ma w przyszłości pozwalać na relacyjne porównywanie wielkości, czasów modyfikacji i wszelkich innych parametrów plików w sposób, na jaki pozwala polecenie find
. Można stworzyć ją w formie programu obsługiwanego z poziomu wiersza poleceń, jak również programu dysponującego graficznym interfejsem użytkownika.