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 | přečteno 69227×
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ů:
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.
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
.
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).
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ů.
File
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).
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.
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.
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).
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); }
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()
.
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.