Ders 12 / 15
Dosya Sistemi API'si
Bir yolun ayrıştırılmış bir değer olduğu, normalleştirme ve çözümlemenin dosya sistemine hiç dokunmadan yanıt verdiği iki ayrı sağlayıcıda koşturularak gösterilir. Dizin gezinme sırasının belirtilmemiş olduğu ve varlık denetimiyle açmanın iki ayrı işlem olduğu ölçülür.
İçindekiler
Önceki ders bir girdi-çıktı akışının içinden geçen içerikle ilgiliydi. Ama akış açılmadan önce bir soru daha var: akışın ucundaki şey gerçekten var mı, ve ona giden yol ne anlatıyor? Bu ders dosya sistemi API’sine bakar — bir yolun kendisinin ayrıştırılmış bir değer olduğunu, bu değer üzerindeki hangi işlemlerin dosya sistemine hiç dokunmadan yanıt verdiğini ve hangilerinin gerçekten diske indiğini ölçer.
Path bir dizgi değildir. "a/../b" yazısı içinde tutulan bir metin gibi görünür, ama
üzerinde çağrılan yöntemler bir metin işleme kütüphanesinin değil, bir dosya sistemi
sağlayıcısının kurallarını izler. Bu dersin sorusu kursun sorusuyla aynı biçimde sorulur:
hangi davranış sağlayıcı seçiminden bağımsızdır — yani arayüzün sözüdür; hangisi sağlayıcının
kendi deposuna sorulmadan yanıtlanamaz — yani depo sorgusudur; ve hangi güvence hiçbir
yöntemin vermediği, yalnız çağrıları doğru sırada yapmakla elde edilen bir kuraldır.
Dört bölüm sırayla ilerliyor. İlk ikisi aynı yol nesnesi üzerinde çağrılan iki tür yöntemi ayırıyor: depoya hiç dokunmayanlar ve depoya gerçekten sorulanlar. Üçüncüsü bir dizinin listelenme sırasının nereden geldiğini ve neyi garanti etmediğini ölçüyor. Dördüncüsü, tek bir yöntemin verdiği sözü iki ayrı çağrıya bölmenin bedelini gösteriyor. Dördü de aynı ilkeye çıkıyor: bir yöntemin adı değil, ne zaman ve hangi kaynağa sorduğu, o yöntemin verdiği sözü belirliyor.
Yol Cebiri: Dosya Sistemine Hiç Dokunmadan
- IO10 — Aynı üç işlem — normalleştirme, çözümleme, dosya adı ayırma — iki ayrı dosya sistemi sağlayıcısında çalıştırılır: varsayılan yerel sağlayıcı ve standart kitaplığın kendi zip sağlayıcısı (bir zip arşivini bir dosya sistemi gibi açan, standart kitaplığın parçası olan sağlayıcı).
- IO11 — Karşılaştırılan şey iki sonucun eşitliğidir; hiçbir yol dizgisinin kendisi
yorum gerektirmez, çünkü sonuçlar zaten insan tarafından okunabilir kısa dizgilerdir.
Aynı ölçüm
equalsvecompareToiçin de tekrarlanır, ve ayrıca yazı olarak aynı iki yol farklı sağlayıcılardan geldiğindeequals’ın ne dediğine bakılır.
Bu ölçüm için iki ayrı sınıf değil, iki ayrı sağlayıcı karşılaştırılıyor: varsayılan
yerel sağlayıcı ve standart kitaplığın kendi zip sağlayıcısı, bir zip arşivini bir dosya sistemi gibi
açan, standart kitaplığın parçası olan FileSystemProvider gerçekleştirimi. Kursun önceki
derslerinde “gerçekleştirim” bir List ya da Map seçimiydi; burada aynı rolü sağlayıcı
seçimi oynuyor — hangi dosya sisteminin arkasında çalıştığın, Path arayüzünün üzerinde
çağırdığın yöntemleri değiştirmiyor.
// SaglayiciDeneme.java — yol cebiri iki dosya sistemi saglayicisinda da ayni mi
import java.net.URI;
import java.nio.file.*;
import java.util.Map;
public class SaglayiciDeneme {
public static void main(String[] args) throws Exception {
Path arsiv = Path.of("arsiv.zip");
Files.deleteIfExists(arsiv);
try (FileSystem zip = FileSystems.newFileSystem(
URI.create("jar:" + arsiv.toUri()), Map.of("create", "true"))) {
System.out.printf("%-14s%-20s%-20s%s%n", "islem", "yerel", "zip", "ayni mi");
String yerelNorm = Path.of("a/./b/../c").normalize().toString();
String zipNorm = zip.getPath("a/./b/../c").normalize().toString();
System.out.printf("%-14s%-20s%-20s%s%n", "normalize", yerelNorm, zipNorm,
yerelNorm.equals(zipNorm));
String yerelCoz = Path.of("kok").resolve("alt/dosya.txt").toString();
String zipCoz = zip.getPath("kok").resolve("alt/dosya.txt").toString();
System.out.printf("%-14s%-20s%-20s%s%n", "resolve", yerelCoz, zipCoz,
yerelCoz.equals(zipCoz));
String yerelAd = Path.of("dizin/dosya.txt").getFileName().toString();
String zipAd = zip.getPath("dizin/dosya.txt").getFileName().toString();
System.out.printf("%-14s%-20s%-20s%s%n", "getFileName", yerelAd, zipAd,
yerelAd.equals(zipAd));
String yerelGoreli = Path.of("a/b/c").relativize(Path.of("a/x")).toString();
String zipGoreli = zip.getPath("a/b/c").relativize(zip.getPath("a/x")).toString();
System.out.printf("%-14s%-20s%-20s%s%n", "relativize", yerelGoreli, zipGoreli,
yerelGoreli.equals(zipGoreli));
boolean yerelEsit = Path.of("a/b").equals(Path.of("a/b"));
boolean zipEsit = zip.getPath("a/b").equals(zip.getPath("a/b"));
System.out.printf("%-14s%-20s%-20s%s%n", "equals (kendi)", yerelEsit, zipEsit,
yerelEsit == zipEsit);
boolean yerelSira = Path.of("a/b").compareTo(Path.of("a/c")) < 0;
boolean zipSira = zip.getPath("a/b").compareTo(zip.getPath("a/c")) < 0;
System.out.printf("%-14s%-20s%-20s%s%n", "compareTo<0", yerelSira, zipSira,
yerelSira == zipSira);
System.out.println("yerel a/b, zip a/b'ye esit mi (ayni yazi, farkli saglayici): "
+ Path.of("a/b").equals(zip.getPath("a/b")));
}
Files.deleteIfExists(arsiv);
}
}
islem yerel zip ayni mi normalize a/c a/c true resolve kok/alt/dosya.txt kok/alt/dosya.txt true getFileName dosya.txt dosya.txt true relativize ../../x ../../x true equals (kendi)true true true compareTo<0 true true true yerel a/b, zip a/b'ye esit mi (ayni yazi, farkli saglayici): false
Altı işlemin altısı da iki sağlayıcıda birebir aynı sonucu veriyor. normalize . ve ..
segmentlerini aynı kuralla eritiyor, resolve bir yolu bir başkasının üzerine aynı biçimde
ekliyor, relativize bir yoldan diğerine giden göreli yolu aynı biçimde hesaplıyor,
equals ve compareTo da segment listesini karşılaştırıyor. Bu altı işlemin hiçbiri
çalışırken sağlayıcının deposuna bakmıyor — zip arşivi henüz boş, yerel dizinde de bu adlarda
hiçbir şey yok, ve yine de altısı da sonuç üretiyor. Sonuç sağlayıcıdan bağımsız olduğu için
bu davranış Path’in kendi arayüz sözüdür: yol cebiri bir segment listesi üzerinde çalışan
saf bir hesaptır, deponun içeriğine hiç sormaz.
Son satır bu sözün sınırını gösteriyor. Yazı olarak birebir aynı iki yol — biri yerel
sağlayıcıdan, biri zip sağlayıcısından — equals ile karşılaştırıldığında eşit değil.
Path.equals yalnız segmentlere değil, hangi sağlayıcıya ait olduğuna da bakıyor; bu da
sağlayıcıdan bağımsız bir kuraldır, çünkü iki sağlayıcı da bu kısıtlamayı aynı biçimde
uyguluyor. Yol cebirinin verdiği söz “aynı segmentler aynı sonucu üretir” değil, “aynı
sağlayıcıdaki aynı segmentler aynı sonucu üretir” biçimindedir.
Bu saflığın ölçülebilir bir sonucu var: yol cebiri üzerine yazılan bir kural — bir uzantının doğrulanması, bir üst dizine çıkışın engellenmesi, bir göreli yolun hesaplanması — hiçbir dosya oluşturmadan, hiçbir dizin kurmadan sınanabilir. Yukarıdaki ölçümün kendisi de bunu gösteriyor: zip arşivi boş açıldı, yerel dizinde ilgili adlarda hiçbir şey yoktu, ve altı karşılaştırmanın altısı da hatasız tamamlandı. Depoya dokunan bir işlem olsaydı, aynı ölçüm önce bir kurulum gerektirirdi.
Aynı Sorunun Depoya Dokunan Yanıtı
- IO12 — Aynı iki sağlayıcıda bu kez
Files.existsçağrılır. Zip sağlayıcısındaki yola önce bir kayıt yazılır, yerel sağlayıcıdaki aynı adlı dosya ise hiç oluşturulmaz.
// DepoyaDokunma.java — depoya dokunan islemler saglayiciya bakar, yol cebiri hicbir zaman bakmaz
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.Map;
public class DepoyaDokunma {
public static void main(String[] args) throws Exception {
Path arsiv = Path.of("arsiv.zip");
Files.deleteIfExists(arsiv);
try (FileSystem zip = FileSystems.newFileSystem(
URI.create("jar:" + arsiv.toUri()), Map.of("create", "true"))) {
Path zipDosya = zip.getPath("kayit.txt");
Files.writeString(zipDosya, "veri", StandardCharsets.UTF_8);
Path yerelDosya = Path.of("kayit.txt");
Files.deleteIfExists(yerelDosya);
System.out.printf("%-14s%s%n", "saglayici", "Files.exists sonucu");
System.out.printf("%-14s%s%n", "yerel", Files.exists(yerelDosya));
System.out.printf("%-14s%s%n", "zip", Files.exists(zipDosya));
}
Files.deleteIfExists(arsiv);
}
}
saglayici Files.exists sonucu yerel false zip true
Aynı ad, iki ayrı sağlayıcıda iki ayrı yanıt veriyor — ve bu bir kusur değil, Files.exists
gerçekten ne yaptığının kanıtı. Bir önceki bölümdeki altı işlemden farklı olarak exists
sağlayıcının deposuna gerçekten sorar; yanıt “hayır” ya da “evet” olabilir ve bu yanıt
depoda o an ne olduğuna bağlıdır. Files.newInputStream, Files.size, Files.readAllBytes,
Files.isDirectory aynı ailededir: hepsi çağrıldıkları anda sağlayıcının deposuna gider. Bir
yol nesnesi üzerinde çağrılan yöntemin depoya dokunup dokunmadığını anlamanın kestirme yolu
şudur: sonuç depo boşken de üretilebiliyorsa cebirdir; depo dolu olmayı gerektiriyorsa depo
sorgusudur.
Bu ayrımın pratik bir sonucu var: bir yol nesnesi, gösterdiği dosya hiç yokken bile güvenle
kurulabilir, saklanabilir, başka yollarla birleştirilebilir. Henüz yazılmamış bir çıktı
dizinini resolve ile önceden adlandırmak hiçbir işlemi patlatmaz, çünkü cebir hiçbir zaman
depoya sormaz. Sorun ancak Files.createDirectories ya da Files.newOutputStream gibi
depoya dokunan bir çağrı geldiğinde ortaya çıkabilir — ve o an hatanın nerede arandığı
bellidir, çünkü depoya dokunan yöntemler kısıtlı ve adı bilinen bir kümedir.
Depoya dokunan her yanıtın bir de zaman boyutu var: exists çağrıldığı anda doğru olan
bir cevap veriyor, o cevabın çağrının bittiği andan sonra da doğru kalacağına dair hiçbir söz
vermiyor. Yol cebiri bu bakımdan daha güçlü bir garanti taşıyor — normalize bir kere
hesaplandıktan sonra bir daha hiç geçersiz olmaz, çünkü depoya hiç bakmadı. Depo sorgusunun
sonucu ise bir anlık görüntüdür ve bu dersin son bölümü tam olarak bu anlık görüntünün ne
kadar kısa ömürlü olduğunu ölçüyor.
Dizin Gezinme Sırası Belirtilmemiştir
- IO13 — Bir dizine dört dosya belli bir sırayla oluşturulur.
Files.listile elde edilen gezinme sırası hem oluşturma sırasıyla hem alfabetik sırayla karşılaştırılır. - IO14 — Gezinme sırasının kendisi basılmaz; yalnız iki karşılaştırmanın sonucu ve iki
ayrı gezinme yönteminin birbiriyle tutarlı olup olmadığı basılır.
Files.list’in döndürdüğü şey de bir kaynaktır — arkasında bir dizin tanıtıcısı tutar — ve kaynakla deneme (M08/K02 Nesneye Dayalı Java,java-genellikler/04) ile açılır.
// GezinmeSirasi.java — dizin gezinme sirasi belirtilmemistir, siralamayi cagiran yapar
import java.nio.file.*;
import java.util.*;
import java.util.stream.Collectors;
public class GezinmeSirasi {
public static void main(String[] args) throws Exception {
Path dizin = Path.of("gezinme");
Files.createDirectories(dizin);
List<String> olusturmaSirasi = List.of("cok.txt", "az.txt", "orta.txt", "bir.txt");
for (String ad : olusturmaSirasi) Files.createFile(dizin.resolve(ad));
List<String> gezinmeListe;
try (var akis = Files.list(dizin)) {
gezinmeListe = akis.map(p -> p.getFileName().toString()).collect(Collectors.toList());
}
List<String> gezinmeDizinAkisi = new ArrayList<>();
try (DirectoryStream<Path> dizinAkisi = Files.newDirectoryStream(dizin)) {
for (Path p : dizinAkisi) gezinmeDizinAkisi.add(p.getFileName().toString());
}
List<String> alfabetik = gezinmeListe.stream().sorted().collect(Collectors.toList());
System.out.println("gezinme sirasi olusturma sirasiyla ayni mi: "
+ gezinmeListe.equals(olusturmaSirasi));
System.out.println("gezinme sirasi alfabetik siralamayla ayni mi: "
+ gezinmeListe.equals(alfabetik));
System.out.println("Files.list ile dizin akisi ayni sirayi mi veriyor: "
+ gezinmeListe.equals(gezinmeDizinAkisi));
for (String ad : olusturmaSirasi) Files.deleteIfExists(dizin.resolve(ad));
Files.deleteIfExists(dizin);
}
}
gezinme sirasi olusturma sirasiyla ayni mi: false gezinme sirasi alfabetik siralamayla ayni mi: false Files.list ile dizin akisi ayni sirayi mi veriyor: true
Gezinme sırası ne oluşturma sırasıyla ne de alfabetik sırayla örtüşüyor; sağlayıcı kendi
deposunu kendi iç düzenine göre listeliyor ve bu düzen Files.list‘in belgelenmiş bir sözü
değil. Buna karşılık Files.list (bir akış döndüren yöntem) ile Files.newDirectoryStream
(bir dizin akışı döndüren, girdi-çıktı akışı duyusunda bir yineleyici arayüzü) aynı gezinmede
aynı sırayı veriyor — çünkü ikisi de aynı sağlayıcının aynı iç listeleme çağrısını
sarmalıyor. İki API’nin birbirleriyle tutarlı olması, gezinmenin belgelenmiş bir sırası
olduğu anlamına gelmiyor; yalnız aynı sağlayıcı çağrısına dayandıklarını gösteriyor. Sıralı
bir sonuç isteyen çağıran listeyi kendisi sıralamak zorundadır; API bu sözü hiçbir yerde
vermiyor.
Belirsizliğin kaynağı sağlayıcının kendi depolama yapısıdır. Bir dizin girdisi çoğunlukla
adına göre değil, dosya sisteminin kendi iç dizinleme düzenine göre tutulur ve listeleme o
iç düzeni olduğu gibi dolaşır. Files.list‘in belgesi bu düzeni hiç taahhüt etmez; taahhüt
ettiği tek şey her girdinin listede bir kez görüneceğidir. Bu, önceki dersteki tamponlama
ölçümüyle aynı biçimdedir: API’nin vermediği bir sözü kendiliğinden var saymak, kod çalıştığı
sürece fark edilmeyen ama taşındığında ortaya çıkan bir bağımlılık kurar.
Varlık Denetimi ile Açma İki Ayrı İşlemdir
Bu bölüm önceki bölümün son cümlesini sayısal bir ölçüme çeviriyor. Files.exists ile
Files.newInputStream iki ayrı depo sorgusudur ve aralarında geçen zaman sıfır değildir;
o boşlukta çalışan kod deponun durumunu değiştirebilir. Ölçüm bunu gerçek bir silme ile
kuruyor, ama kaynağı önemli değil — aynı boşluğu başka bir işlem, başka bir iş parçacığı ya
da kullanıcının kendisi de doldurabilir.
- IO15 — Bir dosya yazılır,
Files.existsile varlığı denetlenir, hemen ardından dosya silinir — başka bir kod yolunun araya girdiği kabulü. Denetimden sonra dosya açılmaya çalışılır. - IO16 — Karşılaştırma için aynı açma çağrısı hiç var olmamış bir dosya üzerinde de doğrudan denenir; iki hatanın aynı sınıftan geldiği görülür.
// IkiAdim.java — varlik denetimi ile acma iki ayri islemdir, aralarinda bosluk vardir
import java.nio.file.*;
public class IkiAdim {
public static void main(String[] args) throws Exception {
Path dosya = Path.of("gecici-kayit.txt");
Files.writeString(dosya, "kayit");
boolean denetimSonucu = Files.exists(dosya);
Files.delete(dosya);
String acmaSonucu;
try {
Files.newInputStream(dosya).close();
acmaSonucu = "acildi";
} catch (NoSuchFileException e) {
acmaSonucu = "acilamadi: " + e.getClass().getSimpleName();
}
System.out.printf("%-18s%s%n", "denetim sonucu", denetimSonucu);
System.out.printf("%-18s%s%n", "acma sonucu", acmaSonucu);
String dogrudanSonuc;
try {
Files.newInputStream(Path.of("hic-olmayan.txt")).close();
dogrudanSonuc = "acildi";
} catch (NoSuchFileException e) {
dogrudanSonuc = "acilamadi: " + e.getClass().getSimpleName();
}
System.out.printf("%-18s%s%n", "dogrudan acma", dogrudanSonuc);
}
}
denetim sonucu true acma sonucu acilamadi: NoSuchFileException dogrudan acma acilamadi: NoSuchFileException
Denetim true döndürdükten hemen sonra açma başarısız oluyor, çünkü ikisi arasında dosya
silindi. Bu kusur Files.exists’in yalan söylemesinden gelmiyor — denetim çağrıldığı anda
doğru cevabı verdi. Kusur, denetim ile açmanın iki ayrı çağrı olmasından, aralarında
geçen zamanda depoyu başka bir şeyin değiştirebilmesinden geliyor. Son satır bunu doğruluyor:
hiç var olmamış bir dosyayı doğrudan açmaya çalıştığımda da aynı istisna, aynı sınıftan
düşüyor. API zaten “aç ya da hata ver” sözünü tek çağrıda sunuyor; denetim adımını önce
ekleyen çağırandır, ve o ek adım güvenlik kazandırmıyor, yalnız aradaki boşluğu büyütüyor.
Doğru düzen denetlemeden açmak ve açmanın kendi istisnasını yakalamaktır.
Bu, kütüphanenin bir eksikliği değil — arayüz zaten tek bir çağrıda hem sonucu hem hatayı taşıyacak biçimde tasarlanmış. Aradaki boşluk yalnız çağıran iki çağrıyı birbirinden ayırıp arasına başka kod koyduğunda ortaya çıkıyor; ne kadar kısa görünürse görünsün, iki adımın arasında geçen her satır o boşluğu genişletiyor. Bu, kursun “çağıranın yükümlülüğü” dediği şeyin en yalın biçimidir: kütüphane yanlış hiçbir şey söylemedi, çağıran doğru bilgiyi yanlış zamanda kullandı.
Özet
Pathbir dizgi değil ayrıştırılmış bir değerdir;normalize,resolve,getFileName,relativize,equalsvecompareTodosya sistemine hiç dokunmadan çalışır ve bu davranış iki ayrı sağlayıcıda da aynıdır.- Yazı olarak aynı iki yol farklı sağlayıcılardan geldiğinde
equalsile eşit çıkmaz; eşitlik segmentlere değil, segmentler artı sağlayıcıya bakar. Files.exists,Files.size,Files.readAllBytesgibi işlemler sağlayıcının deposuna gerçekten sorar; yanıt depodaki gerçek duruma bağlıdır ve sağlayıcı değişince değişebilir.- Dizin gezinme sırası ne oluşturma sırasıyla ne alfabetik sırayla örtüşür ve API bu sırayı hiçbir yerde belgelemez; sıralı bir sonuç isteyen çağıran listeyi kendisi sıralar.
- İki ayrı gezinme yöntemi aynı sırayı vermesi, sıranın belgelendiği anlamına gelmez — yalnız aynı sağlayıcı çağrısına dayandıklarını gösterir.
- Sınırlayıcı ölçüm: varlık denetimi ile açma iki ayrı işlemdir. Aralarında dosya silinirse denetim geçer, açma düşer; API zaten tek çağrıda “aç ya da hata ver” sunar, ek denetim adımı çağıranın kendi eklediği bir boşluktur.
Sonraki Adım
Bu derste bir dosyanın var olup olmadığı, hangi baytları taşıdığı ölçüldü — ama bir nesnenin kendisi bayta çevrildiğinde ne olduğu hiç sorulmadı. Sıradaki ders dizileştirmeye bakar: bir nesne bayt dizisine döküldüğünde ve geri okunduğunda, o nesnenin yapıcısının hiç çalışmadığını ve sınıfın kurduğu değişmezlerin bu yüzden dışarıdan verilen baytlarla kırılabildiğini ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.