Velmi častým prostředkem komunikace programu s okolním světem je soubor. Protože je Java koncipována jako platformově nezávislá, musí se vypořádat s různými nástrahami, které rozličné souborové systémy skýtají. Že to není jednoduchý úkol, ale že není neřešitelný, se přesvědčíme při zkoumání javovských prostředků, které jsou pro tento účel k dispozici.
1.9.2005 06:00 | Lukáš Jelínek | read 69230×
DISCUSSION
Úkol nelehký
Pokud potřebujeme uložit data z programu (nebo je načíst), ve většině
případů k tomu použijeme soubor. A přestože soubor samotný se skoro na
všech platformách tváří úplně stejně (je to různě dlouhá posloupnost bajtů),
způsob práce se liší, a to dost zásadně.
Zajímají nás především dvě nejrozšířenější skupiny souborových systémů:
-
systémy unixového typu - je jich celá řada (UFS, Ext2, ReiserFS atd.), pro
všechny z nich je typické, že celý systém tvoří jediný adresářový strom,
jako oddělovač jednotlivých úrovní se používá běžné (dopředné) lomítko,
rozlišují se velká a malá písmena, téměř všechny znaky ASCII jsou platné
a pro ukládání názvů lze obecně používat různé kódové stránky.
-
systémy firmy Microsoft (FAT a NTFS) - rozlišují se jednotlivá logická
zařízení ("písmena disků"), oddělovačem je zpětné lomítko, velikost písmen
se nerozlišuje, okruh platných znaků je poměrně malý (i když to některé
systémové funkce řádně nekontrolují a lze tak na disk propašovat i soubory
s neplatnými názvy, které už pak nelze odstranit), kódování se liší podle
jednotlivých systémů (NTFS používá Unicode, FAT potom staré kódové stránky,
např. pro češtinu CP 852). Situaci ještě komplikuje používání názvů
souborů UNC.
Speciální situace se pak řeší, pokud se nějaký souborový systém používá
z operačního systému, ve kterém je cizí (typicky třeba FAT v Linuxu). Pak
platí pravidla, která jsou směsicí výše uvedeného a je třeba si dávat zvlášť
velký pozor.
Třída File
V balíku java.io
najdeme třídu, která nám poskytne vše potřebné k práci
se soubory - je to třída File
. Nepředstavuje přímo konkrétní soubor, nýbrž
tzv. abstraktní cestu (tedy obecně jakoukoli cestu identifikující nějaký
soubor). Může odkazovat na platný soubor, ale také nemusí. Důležité ale je,
že je to (podobně jako třeba String
) invariant, jak se jednou vytvoří, už
nelze změnit.
V řadě tříd ze standardní knihovny Javy najdeme metody, které vyžadují
jako svůj argument název souboru. Prakticky ve všech případech lze použít
jak textový řetězec (String
; to jsme doposud běžně dělali), tak právě instanci
třídy File
, což je přenositelnější a robustnější řešení, protože můžeme již
předem zjistit o daném názvu souboru nějaké informace nebo provést se souborem
potřebné operace. Jako příklad mohu uvést třeba konstruktor třídy
FileInputStream
.
Specifikace abstraktní cesty
Zdůrazňuji, že objekt File
může představovat jak soubory (běžné, ale i
speciální, třeba soubory zařízení), tak adresáře. Cesta může být absolutní
i relativní, může být dokonce i prázdná. Abstraktní cesta se vždy skládá
z prefixu (např. označení kořenového adresáře, úvodní označení UNC cesty;
u relativních cest prefix samozřejmě chybí)
a z posloupnosti názvů jednotlivých adresářových úrovní (samozřejmě včetně
případného názvu souboru na konci) oddělených separátorem.
Jak se s cestami pracuje, záleží na nastavení vlastností systému (system
properties; někdy později se na ně podíváme důkladněji), a toto nastavení se
samozřejmě liší podle platformy. Nejdůležitější je oddělovač názvů v cestě;
ve třídě File
je určen hodnotou konstanty separatorChar
(znakové vyjádření),
resp. separator
(řetězcové vyjádření). Na unixových systémech je oddělovačem
samozřejmě dopředné lomítko, na microsoftích systémech lomítko obrácené.
Když už jsme u těch konstant, třída File
obsahuje ještě konstanty
pathSeparatorChar
a pathSeparator
. Tyto konstanty představují oddělovač
cest (v případech kdy máme zapsaných několik cest za sebou) a mají hodnotu
dvojtečky (unixové systémy), resp. středníku (microsoftí systémy).
Vytvoření instance File
Jak jsem již řekl, jednou vytvořený objekt File
už nemůžeme měnit. Má to svoji
logiku, protože jestliže řetězec je v Javě neměnný, musí být jeho speciální případ
(což abstraktní cesta bezpochyby je) také neměnný.
Objekt File
můžeme vytvořit třemi způsoby: názvem souboru, názvem souboru
vzhledem k rodiči, a pomocí URI (Uniform Resource Identifier; viz
RFC 2396). Pokud ho vytáříme
přímo z celé (absolutní nebo relativní) cesty, je situace jednoduchá,
řetězec se pouze převede na abstraktní cestu. Pokud je dán rodič (ať už názvem
nebo instancí File
, a není null
), je abstraktní cesta vytvořena jako relativní vůči
této rodičovské cestě (adresáři), resp. proti výchozímu adresáři (pokud je
rodič prázdná abstraktní cesta). Pokud se instance File
vytváří z URI, musí
být splněny určité požadavky (schéma musí být "file
", cesta nesmí být prázdná
atd.).
Separátor v řetězci nemusí odpovídat dané platformě. Pokud je konstruktor
schopen ho normalizovat (tzn. převést do správné podoby), zpracuje se cesta bez
problémů.
Operace s instancí třídy File
Práce s cestou k souboru
Protože, jak bylo řečeno, je objekt File
abstraktní cestou k souboru, můžeme
s touto cestou pracovat a získávat různé její varianty. Lze získat celou
cestu v různých podobách, části cesty a některé další verze.
-
getPath()
- vrátí (normalizovanou) abstraktní cestu v podobě,
jak byla zadána při vytváření objektu (tedy absolutní zůstane absolutní atd.).
Stejný efekt má i metoda toString()
.
-
getAbsolutePath()
- vrátí cestu převedenou do absolutního tvaru.
Mechanismus případného převodu z relativní cesty na absolutní je platformově
závislý, většinou se ale jako báze použije domovský adresář uživatele.
-
getCanonicalPath()
- vrací kanonický tvar cesty. V praxi to
znamená, že se pokusí cestu maximálně "vyhodnotit", zpracovat. Odstraní
všechny označení stejného nebo nadřazeného adresáře (tečku a dvě tečky),
zpracuje symbolické odkazy atd.. Chování je silně platformově závislé
a liší se podle toho, zda cesta (nebo její části) existuje či nikoli.
-
getName()
- získá z cesty pouhý název souboru (bez adresářové
cesty).
-
getParent()
- vrací rodičovský adresář souboru. Vychází se
pouze z cesty, soubor ani rodičovský adresář nemusí existovat.
-
isAbsolute()
- zjistí, zda je cesta absolutní.
-
compareTo()
- lexikograficky porovná tuto abstraktní cestu
s jinou (ani jedna nemusí existovat). Porovnávání je platformově závislé,
podle systému se použije rozlišení malých/velkých písmen. Podobně pracuje
metoda equals()
, která pouze zjišťuje, zda jsou abstraktní cesty totožné.
Všechny uvedené metody, které vrací cestu nebo její část, mají jako
návratovou hodnotu textový řetězec. Kromě nich existují jejich obdoby, které
vracejí novou instanci typu File
. Jsou to metody getAbsoluteFile()
,
getCanonicalFile()
a getParentFile()
.
Abstraktní cestu lze také převést (metodou getURL()
) na odpovídající URL,
anebo na URI (metodou getURI()
). Protože však metoda getURL()
neumí správně
naložit se zakázanými znaky, doporučuje se vždy použít getURI()
a získaný
objekt převést jeho metodou getURL()
na URL (protože URL jsou podmnožinou
URI, často se ve skutečnosti ani vnitřně nic převádět nebude).
Zjišťování informací o souboru
Nyní už přejdeme k operacím, které se týkají souboru, na který abstraktní
cesta odkazuje. Množina operací není velká, musí být totiž dostatečně
přenositelná mezi platformami.
-
exists()
- základní věc: zjištění, zda vůbec soubor existuje.
Pokud se chystáme dělat s ním nějaké další věci, je dobré zavolat nejprve
tuto metodu a ověřit si jeho přítomnost.
-
isFile(), isDirectory()
- pomocí těchto metod poznáme, zda
se jedná o "normální" soubor nebo o adresář. Opět je to platformově závislé,
např. symbolický odkaz na soubor se tváří jako běžný soubor. Platí ale, že
jakékoli soubory/adresáře vytvořené z Javy zcela jistě projdou správně
těmito testy.
-
canRead(), canWrite()
- dozvíme se, zda můžeme číst či zapisovat
do daného souboru. Pokud byl ale přístup odepřen, už se nedozvíme proč.
To je daň za přenositelnost, nemáme možnost zjišťovat třeba přístupová práva.
-
isHidden()
- zjišťuje, zda je soubor označen jako "skrytý".
V unixových systémech za skryté soubory považuje ty, jejich název začíná
tečkou, ve Windows pak soubory s nastaveným atributem "hidden".
-
length()
- zjistí velikost souboru. Protože vrací hodnotu
typu long
, není problém ani s opravdu velkými soubory.
-
lastModified()
- jediný časový údaj, který můžeme o souboru
zjistit, je čas poslední modifikace. Ne všechny souborové systémy poskytují
další časové informace, proto je to takto omezeno. Navíc v praxi je to
právě ten nejpotřebnější údaj, podle něhož můžeme např. zjišťovat, že
někdo změnil konfigurační soubor.
Adresářové informace
Předchozí metody se týkaly všech souborů bez rozdílu, tedy včetně
adresářů. Pro adresáře samotné máme k dispozici speciální sadu
metod, které využijeme pro přístup k souborům v těchto adresářích:
-
list()
- nejjednodušší varianta. Prostě vrátí pole obsahující
seznam všech souborů v daném adresáři - položky tohoto pole
budou textové řetězce s názvy souborů. Pořadí souborů není
definováno, může být libovolné (závisí na implementaci;
v Linuxu budou soubory pravděpodobně uspořádány tak, jak
jsou zaznamenány v adresáři). To samozřejmě není problém,
protože si soubory můžeme (ať už podle názvu nebo jinak)
seřadit podle potřeby.
-
listFiles()
- dělá přesně totéž co list()
, ale místo pole
textových řetězců vrací pole objektů File
, tedy
abstraktních cest. Zda použijeme tuto nebo přechozí metodu,
záleží na konkrétní situaci.
-
list(FilenameFilter f)
- modifikace metody list()
s tím, že
předem vybíráme jen některé soubory. Které to budou, to
určí implementace rozhraní FilenameFilter
. O filtraci ještě
bude řeč.
-
listFiles(FilenameFilter f)
- opět metoda vracející pole
objektů File
, tentokrát s filtrací (viz výše).
-
listFiles(FileFilter f)
- další modifikace, ale s jiným
typem filtru.
-
listRoots()
- v souborových systémech unixovského typu je
hierarchie přísně stromová, vždy máme jediný kořen. V jiných systémech to
ale platit nemusí (a také neplatí), proto je třeba mít možnost dostat se
ke všem dostupným kořenům - a to zajišťuje právě tato statická metoda. Vrací pole
všech kořenů adresářových stromů, které jsou v danou chvíli k dispozici.
O filtraci souborů
Výše uvedené metody provádějí filtraci souborů podle
poskytnutého rozhraní. To je typická ukázka toho, jak se v Javě
podobné věci řeší - existuje mnoho a mnoho objektových metod,
které jako mají parametr nějaké jednoduché rozhraní, obsahující
třeba jen jedinou metodu. Chování je pak plně v režii
implementace tohoto rozhraní.
Pokud budeme implementaci potřebovat jen v jednom jediném
případě, s výhodou využijeme možnosti vytvořit anonymní
třídu přímo na daném místě. Viz příklad:
File f = new File("/home/username/docs"); // vybereme adresář
String list[] = f.list(new FilenameFilter() {
boolean accept(File dir, String name) {
return name.endsWith(".pdf"); // jen názvy *.pdf
}
});
Arrays.sort(list); // abecední seřazení
for (int i=0; i<list.length; i++) {
System.out.println(list[i]);
}
Uvedený příklad vypíše v abecedním pořadí všechny soubory z daného adresáře,
jejichž název končí na .pdf
(jsou to tedy dokumenty formátu PDF).
Manipulační operace
Zatím jsme o souborech pouze zjišťovali různé informace.
S tím si rozhodně nelze vystačit, občas musíme také někde
něco změnit. Třída File
nabízí několik manipulačních operací,
tak se na ně podívejme:
-
renameTo(File f)
- metoda přejmenuje soubor podle zadání. Všimněte si,
že se jako parametr zadává jiná instance objektu File
. Jak jsme si již
řekli, instance File
je neměnná, proto i po úspěšném přejmenování souboru
zůstane tak, jak je (bude obsahovat původní cestu k souboru). Naopak
nové jméno souboru bude odpovídat zadané instanci, o čemž se můžeme
přesvědčit tak, že zavoláme metodu exists()
. Chování je silně platformově
závislé, nemůžeme spoléhat, že metoda bude dělat vždy to, co dělala
na některé platformě. Návratovou hodnotu je třeba vždy testovat.
-
delete()
- pokusí se smazat soubor. Pokud to jde, smaže ho.
Neprázdné adresáře mazat nelze, musí se nejdřív explicitně
vyprázdnit.
-
deleteOnExit()
- zajímavá metoda, naplánuje smazání souboru při
ukončování programu. Zafunguje pouze při čistém ukončení programu,
tedy ne při "sestřelení" (na Linuxu signálem SIGKILL) nebo
při zavolání metody System.halt()
. Metoda se používá pro
automatické mazání dočasných souborů (viz níže). Pozor
- naplánované smazání už nejde zrušit!
-
createNewFile()
- vytvoří nový prázdný soubor. Metoda nemá
příliš velké využití, ale někdy se hodí.
-
mkdir(), mkdirs()
- dvojice metod pro vytváření adresářů. Liší
se pouze tím, že ta první vytvoří pouze ten jediný adresář, na
který odkazuje instance File
, kdežto ta druhá vytvoří, pokud
je třeba, i všechny nadřazené adresáře.
-
setLastModified(long time)
- změní časový údaj o poslední změně
souboru. Někdy se to může hodit.
-
setReadOnly()
- nastaví, že soubor bude pouze ke čtení. Nepříjemné
je, že to je pouze jednosměrná operace a v Javě nemáme prostředky,
jak to vrátit zpět.
Následující příklad ukáže několik operací provedených na souboru
identifikovaném abstraktní cestou. Nejdříve se adresářová cesta převede
do kanonického tvaru, potom se daný adresář vytvoří, v něm se založí nový
soubor a ten se nakonec přejmenuje. Všimněte si, že některé operace vyžadují
ošetření výjimky IOException
.
File dir = new File("../user2/texty");
try {
dir = dir.getCanonicalFile();
} catch (IOException e) {
System.err.println("Nelze ziskat kanonicky tvar: " + dir);
System.exit(1);
}
if (!dir.makedir()) {
System.err.println("Nelze vytvorit adresar " + dir);
System.exit(2);
}
File f = new File(dir, "test.txt");
try {
if (!f.createNewFile()) {
System.err.println("Soubor " + f + " jiz existuje");
}
} catch (IOException e) {
System.err.println("Nelze vytvorit soubor " + f);
System.exit(3);
}
File f2 = new File(f.getParent(), "test2.txt");
if (!f.renameTo(f2)) {
System.err.println("Nelze prejmenovat soubor na " + f2);
System.exit(4);
}
Dočasné soubory
Občas potřebujeme uložit nějaká data do dočasného souboru, abychom
je použili později. Lze to samozřejmě udělat ručně, tedy zvolením
nějakého umístění souboru, to ale nebude přenositelné. Ve třídě
File
máme k dispozici dvě statické metody, kterými tento problém
snadno vyřešíme:
-
createTempFile(String prefix, String suffix)
- vytvoří dočasný soubor
s daným prefixem (min. 3 znaky dlouhým) a danou koncovkou (může být null
,
pak se použije .tmp
). Soubor vytvoří v adresáři pro dočasné soubory
(např. /tmp
), vrací instanci třídy File
.
-
createTempFile(String prefix, String suffix, File directory)
- od předchozí
metody se liší tím, že umožňuje specifikovat adresář pro uložení souboru
(pokud je null
, chování této metody je shodné s chováním té předchozí).
Vytvořený soubor se nemaže automaticky při skončení programu. Pokud to
potřebujeme (což je skoro vždy), použijeme metodu deleteOnExit()
.
Od souborů k síti
Kapitola (pravda, poněkud "výčtově" orientovaná) o práci se soubory tímto
dospěla ke svému konci. Ke konci ale nedospěly I/O operace, protože Java
v této oblasti nabízí velmi mnoho. Příště přijde řada na komunikaci po síti,
která je v dnešní době stejně důležitá jako práce se soubory.