İçeriğe geç
academia.sh

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 equals ve compareTo için de tekrarlanır, ve ayrıca yazı olarak aynı iki yol farklı sağlayıcılardan geldiğinde equals’ı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.list ile 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.exists ile 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

  • Path bir dizgi değil ayrıştırılmış bir değerdir; normalize, resolve, getFileName, relativize, equals ve compareTo dosya 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 equals ile eşit çıkmaz; eşitlik segmentlere değil, segmentler artı sağlayıcıya bakar.
  • Files.exists, Files.size, Files.readAllBytes gibi 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat