İçeriğe geç
academia.sh

Ders 11 / 15

Akış Tabanlı Girdi/Çıktı

Bayt akışının write(int) için düşük sekiz bitten başka söz vermediği, karakter akışının bir kodlamaya yaslandığı ve tamponlamanın sözü değil yalnız alt akışa varan çağrı sayısını değiştirdiği ölçülür. Sınırlayıcı ölçüm, boşaltılmamış bir tamponun veriyi eksik bıraktığını gösterir.

İçindekiler

B payının son dersi yokluğu bir tipe taşıdı: Optional boş olduğunda okuma yine çalışma zamanında düşüyordu, denetim ortadan kalkmadı, yalnız yeri değişti. Bu konu aynı soruyu dış dünyaya sorar. Bir bayt dizisini ya da bir metni okurken kütüphanenin sana verdiği söz nedir, ve o sözü kimin taşıdığını — arayüzün mü, seçtiğin sınıfın mı, yoksa senin mi — çağrı yazımına bakarak ayırt edebilir misin?

Bu derste iki ayrı aile karşılaştırılır: bayt akışı ve karakter akışı. İkisi de java.io girdi-çıktı akışıdır; Java’nın Stream veri akışı ayrı bir API’dir ve bu konu boyunca hep niteleyiciyle anılacaktır. Soru hep aynı biçimde sorulur: bir davranış iki ayrı gerçekleştirimde de görülüyorsa arayüzün sözüdür; yalnız birinde görülüyorsa seçilen sınıfa aittir; hiç görülmüyor ama kod hâlâ çalışıyorsa çağıranın uyduğu bir kuraldır.

Beş bölüm sırayla ilerliyor. İlk ikisi bayt seviyesinde neyin garanti edildiğini ölçüyor — yazmada ve okumada. Üçüncüsü tamponlamanın bu iki sözü hiç bozmadan yalnız çağrı sayısını değiştirdiğini gösteriyor. Dördüncüsü karakter akışının bayt akışına eklediği tek şeyin, yani kodlamanın, çağırana yüklediği kararı ölçüyor. Beşincisi ise hiçbir sınıfın vermediği, yalnız çağıranın uyması gereken bir kuralı: bir tamponun boşaltılması.

Bayt Akışının Verdiği Tek Söz

  • IO1OutputStream.write(int) bir tamsayı alır ama yalnız düşük sekiz bitini yazar; kalan yirmi dört bit yok sayılır. Bu ölçüm, iki ayrı gerçekleştirimin aynı değeri yazıp aynı düşük sekiz biti üretip üretmediğine bakar.
  • IO2 — Yazılan bayt geri okunurken & 0xFF ile işaretsiz tamsayıya çevrilir; imzalı byte değeri doğrudan karşılaştırılmaz.
// TekSoz.java — bayt akisinin write(int) icin verdigi tek soz: dusuk sekiz bit
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class TekSoz {
    public static void main(String[] args) throws Exception {
        int deger = 321;
        int beklenen = deger & 0xFF;
        System.out.printf("%-24s%-10s%s%n", "gerceklestirim", "yazilan", "beklenenle ayni mi");

        ByteArrayOutputStream dizi = new ByteArrayOutputStream();
        dizi.write(deger);
        int diziSonuc = dizi.toByteArray()[0] & 0xFF;
        System.out.printf("%-24s%-10d%s%n", "ByteArrayOutputStream", diziSonuc, diziSonuc == beklenen);

        Path dosya = Path.of("tek-soz.bin");
        try (OutputStream dosyaAkisi = new FileOutputStream(dosya.toFile())) {
            dosyaAkisi.write(deger);
        }
        int dosyaSonuc = Files.readAllBytes(dosya)[0] & 0xFF;
        System.out.printf("%-24s%-10d%s%n", "FileOutputStream", dosyaSonuc, dosyaSonuc == beklenen);
        Files.delete(dosya);
    }
}
gerceklestirim          yazilan   beklenenle ayni mi
ByteArrayOutputStream   65        true
FileOutputStream        65        true

Yazılan değer 321; ikilik tabanda alt sekiz biti 01000001, yani 65. Bellekte tutan bir gerçekleştirim ile bir dosyaya yazan gerçekleştirim, aralarında hiçbir ortak kod satırı olmamasına rağmen aynı düşük sekiz biti üretiyor. Bu, OutputStream soyut sınıfının belgelediği bir sözdür — gerçekleştirim seçimi burada hiçbir şeyi değiştirmiyor, çünkü davranış zaten üst sınıfın kendisinde tanımlı.

Bayt akışının verdiği söz tam olarak bu kadardır: sekiz bitlik bir birimi olduğu gibi taşımak. Değerin ne anlama geldiği — bir karakter mi, bir sayı mı, bir görüntünün parçası mı — akışın hiç bilmediği bir şeydir. Bayt akışı hiçbir yorum sözü vermez; yalnız birim taşır. OutputStream bir Java interface’i değil soyut sınıftır, ama ölçünün sorduğu soruyu değiştirmiyor: davranış üst tipte tanımlıysa, o tip bir arayüz gibi konuşuyor demektir — onu konuşan her alt sınıf sözü aynen taşımak zorunda.

Okuma Ucunda Aynı Söz

  • IO3InputStream.read() de bir sözdür: okunan bayt işaretsiz bir tamsayı olarak, 0 ile 255 arasında döner; girdi-çıktı akışı bittiğinde dönen değer bir bayt değil, akış dışı bir işaretçidir: -1.
  • IO4 — Ölçülen dizi üç bayt taşıyor ve üçüncüsü, imzalı byte olarak okunsaydı negatif çıkacak bir değer (255). read()’in işaretsize çevirmesi bu değeri de doğru taşıyıp taşımadığı burada görülüyor.
// Okuma.java — read() icin verdigi tek soz: 0-255 arasi deger, sonda -1
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class Okuma {
    static List<Integer> oku(InputStream a) throws Exception {
        List<Integer> degerler = new ArrayList<>();
        int d;
        while ((d = a.read()) != -1) degerler.add(d);
        degerler.add(a.read());
        return degerler;
    }

    public static void main(String[] args) throws Exception {
        byte[] veri = { 10, (byte) 200, (byte) 255 };

        List<Integer> diziSonuc = oku(new ByteArrayInputStream(veri));

        Path dosya = Path.of("okuma.bin");
        Files.write(dosya, veri);
        List<Integer> dosyaSonuc;
        try (InputStream dosyaAkisi = new FileInputStream(dosya.toFile())) {
            dosyaSonuc = oku(dosyaAkisi);
        }
        Files.delete(dosya);

        System.out.printf("%-24s%s%n", "gerceklestirim", "okunan degerler (son ikisi: son bayt, dosya sonu)");
        System.out.printf("%-24s%s%n", "ByteArrayInputStream", diziSonuc);
        System.out.printf("%-24s%s%n", "FileInputStream", dosyaSonuc);
        System.out.println("iki dizi ayni mi: " + diziSonuc.equals(dosyaSonuc));
    }
}
gerceklestirim          okunan degerler (son ikisi: son bayt, dosya sonu)
ByteArrayInputStream    [10, 200, 255, -1]
FileInputStream         [10, 200, 255, -1]
iki dizi ayni mi: true

Üç bayttan üçüncüsü, imzalı byte olarak yorumlansaydı -1 çıkardı; read() onu işaretsiz bir değere çevirip 255 olarak döndürüyor. Bellekteki dizi de dosya da aynı üç değeri ve ardından aynı işaretçiyi veriyor. Bu -1 okunmuş bir bayt değildir — hiçbir gerçek bayt bu değeri taşıyamaz, çünkü işaretsize çevrilmiş bir bayt her zaman 0 ile 255 arasındadır. -1, akışın bittiğini söyleyen, veri uzayının dışında duran bir işarettir.

Yazmada düşük sekiz bitin korunması ile okumada işaretsize çevirme ve dosya sonu işareti, birlikte bayt akışının bütün sözleşmesini kuruyor. İkisi de gerçekleştirimden bağımsız, çünkü ikisi de InputStream ve OutputStream’in kendisinde tanımlı; bir alt sınıf bu davranışı değiştiremez, yalnız nasıl sağladığını — bellekte mi, diskte mi — seçebilir.

Tamponlamanın Sözü Değil Çağrı Sayısını Değiştirmesi

  • IO5 — Alt akışa varan her yazma çağrısını sayan bir sarmalayıcı yazılır; bu sarmalayıcı yalnız sayaç tutar, veriyi değiştirmez.
  • IO6 — Aynı beş parça, önce doğrudan bu sarmalayıcıya, sonra BufferedOutputStream üzerinden aynı sarmalayıcıya yazılır. Karşılaştırılan iki şey ayrıdır: alt akışa varan çağrı sayısı ve alt akışın topladığı içerik.
// SayacAkis.java — alttaki akisa varan yazma cagrisini sayan sarmalayici (yardimci sinif, main yok)
import java.io.IOException;
import java.io.OutputStream;

class SayacAkis extends OutputStream {
    private final OutputStream alt;
    int cagriSayisi = 0;

    SayacAkis(OutputStream alt) { this.alt = alt; }

    @Override public void write(int b) throws IOException {
        cagriSayisi++;
        alt.write(b);
    }

    @Override public void write(byte[] veri, int bas, int uzunluk) throws IOException {
        cagriSayisi++;
        alt.write(veri, bas, uzunluk);
    }

    @Override public void flush() throws IOException { alt.flush(); }
    @Override public void close() throws IOException { alt.close(); }
}
// Tamponlama.java — tamponlamanin sozu degil, alt akisa varan cagri sayisini degistirdigi
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.util.List;

public class Tamponlama {
    static final List<byte[]> PARCALAR = List.of(
            "veri".getBytes(), "-".getBytes(), "kaydi".getBytes(),
            "-".getBytes(), "01".getBytes());

    public static void main(String[] args) throws Exception {
        ByteArrayOutputStream altDogrudan = new ByteArrayOutputStream();
        SayacAkis sayacDogrudan = new SayacAkis(altDogrudan);
        for (byte[] parca : PARCALAR) sayacDogrudan.write(parca, 0, parca.length);
        sayacDogrudan.close();

        ByteArrayOutputStream altTamponlu = new ByteArrayOutputStream();
        SayacAkis sayacTamponlu = new SayacAkis(altTamponlu);
        try (BufferedOutputStream tamponlu = new BufferedOutputStream(sayacTamponlu)) {
            for (byte[] parca : PARCALAR) tamponlu.write(parca, 0, parca.length);
        }

        System.out.printf("%-14s%-18s%s%n", "yazim", "alt cagri sayisi", "teslim edilen icerik");
        System.out.printf("%-14s%-18d%s%n", "dogrudan", sayacDogrudan.cagriSayisi,
                altDogrudan.toString());
        System.out.printf("%-14s%-18d%s%n", "tamponlu", sayacTamponlu.cagriSayisi,
                altTamponlu.toString());
        System.out.println("icerik ayni mi: " + altDogrudan.toString().equals(altTamponlu.toString()));
    }
}
yazim         alt cagri sayisi  teslim edilen icerik
dogrudan      5                 veri-kaydi-01
tamponlu      1                 veri-kaydi-01
icerik ayni mi: true

Doğrudan yazımda beş parça, sayaç akışına beş ayrı çağrı olarak ulaşıyor: her write alt akışa hemen düşüyor. Tamponlu yazımda aynı beş parça sayaç akışına bir çağrı olarak ulaşıyor, çünkü BufferedOutputStream parçaları kendi belleğinde biriktirip yalnız boşalttığında alt akışı çağırıyor. İki yazımın da teslim ettiği içerik birebir aynı.

Bu ölçüm tamponlamanın ne değiştirip ne değiştirmediğini ayırıyor. OutputStream’in verdiği söz “yazdığın baytlar sonunda alt akışa ulaşacak” biçimindedir; “her write çağrısı alt akışa anında dokunacak” biçiminde değildir. Tamponlama bu ikinci cümleyi hiç ihlal etmiyor, çünkü o cümle zaten sözün bir parçası değildi. Değişen şey yalnız çağıranın göremeyeceği bir sayı: alt akışa varan çağrı sayısı.

Bu yüzden tamponlama bir garanti değil bir seçimdir. Alt akış gerçekten pahalıysa — bir dosya, bir ağ bağlantısı — beş ayrı çağrı beş ayrı sistem çağrısına dönüşebilir; bir tampon araya girdiğinde aynı iş tek çağrıya iner. Seçim çağırana ait olduğu için BufferedOutputStream hiçbir zaman kendiliğinden boşalmaz: tamponu ne zaman dolduracağına, ne zaman erken boşaltacağına yalnız çağıran karar verir. Bu dersin son bölümündeki sınırlayıcı ölçüm tam olarak bu kararın atlandığı durumu gösteriyor.

Karakter Akışının Kodlamaya Yaslanması

  • IO7 — Bir metin tek bir kodlamayla bayta çevrilip yazılır. Aynı bayt dizisi iki ayrı kodlamayla geri okunur: yazıldığı kodlamayla ve başka bir kodlamayla.
  • IO8 — Karşılaştırılan üç şey: bayt sayısı (sabit kalır, çünkü kaynak aynıdır), karakter sayısı ve okunan metnin yazılanla eşitliği. Metnin kendisi değil, uzunluk ve eşitlik basılır.
// Kodlama.java — karakter akisinin verdigi soz kodlamaya baglidir, bayt akisininki degildir
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class Kodlama {
    public static void main(String[] args) throws Exception {
        String turkce = "ağırlık";
        Path dosya = Path.of("kodlama.bin");

        byte[] bayt = turkce.getBytes(StandardCharsets.UTF_8);
        Files.write(dosya, bayt);

        String ayniKodlama = new String(Files.readAllBytes(dosya), StandardCharsets.UTF_8);
        String farkliKodlama = new String(Files.readAllBytes(dosya), StandardCharsets.ISO_8859_1);

        System.out.printf("%-26s%-12s%-16s%s%n", "okuma kodlamasi", "bayt sayisi", "karakter sayisi", "yazilanla ayni mi");
        System.out.printf("%-26s%-12d%-16d%s%n", "UTF_8 (yazildigi gibi)", bayt.length,
                ayniKodlama.length(), ayniKodlama.equals(turkce));
        System.out.printf("%-26s%-12d%-16d%s%n", "ISO_8859_1 (baska kodlama)", bayt.length,
                farkliKodlama.length(), farkliKodlama.equals(turkce));

        Files.delete(dosya);
    }
}
okuma kodlamasi           bayt sayisi karakter sayisi yazilanla ayni mi
UTF_8 (yazildigi gibi)    10          7               true
ISO_8859_1 (baska kodlama)10          10              false

Yedi harflik metin UTF-8 ile on bayt üretiyor, çünkü ğ ve ı bu kodlamada iki baytla gösteriliyor. Aynı on bayt UTF-8 ile geri okunduğunda yedi karaktere dönüyor ve yazılanla birebir eşleşiyor. Aynı on bayt ISO-8859-1 ile okunduğunda her bayt kendi başına bir karakter sayılıyor ve sonuç on karakter — yazılan metinle eşleşmiyor.

Bu ölçüm çağıranın bir kuralını gerçekten kırıyor: yazma ve okuma kodlamasının aynı olması. Kırılmanın sonucu ne bir istisnadır ne de bir uyarı; sessiz bir sonuçtur. Program hatasız çalışır, bir dizgi üretir, ve o dizgi yanlıştır. Bayt akışının hiçbir yorum sözü vermediği önceki bölümde kuruldu; karakter akışı tam olarak bu boşluğu dolduruyor, ama doldurduğu kodlamayı sen vermek zorundasın. Kodlama verilmediğinde derleyici bir varsayılana düşer ve karar ortama kalır — bu derste her okuma ve yazmada kodlamanın açıkça verilmesinin nedeni tam olarak yukarıdaki tablodur: iki taraf farklı kodlama kullandığında hata hiçbir yerde durmaz, yalnız sayı değişir.

Bu yükün nerede taşındığı da ölçülebilir bir seçimdir. OutputStreamWriter ve InputStreamReader — bayt akışını karakter akışına çeviren iki sınıf — kurucularında bir Charset parametresi ister; Files.write ve Files.readAllBytes ise kodlamadan tamamen habersizdir, çünkü ikisi de yalnız bayt taşır ve kodlama kararını çağırana, yukarıdaki gibi getBytes ve String kurucusuna bırakır. Hangi katmanda verilirse verilsin, karar sözün bir parçası değildir; her okuma ve her yazma kendi kodlamasını açıkça taşımak zorundadır.

Boşaltılmamış Tampon

  • IO9 — Bir BufferedOutputStream yazıldıktan hemen sonra, flush çağrılmadan, alttaki akışın içeriğine doğrudan bakılır. İkinci ölçümde aynı adımlar flush ile tamamlanır.
// Bosaltma.java — bosaltilmamis tampon veriyi tasimaz, bosaltma karari cagirana aittir
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;

public class Bosaltma {
    public static void main(String[] args) throws Exception {
        ByteArrayOutputStream altKapatilmadan = new ByteArrayOutputStream();
        BufferedOutputStream tamponluKapatilmadan = new BufferedOutputStream(altKapatilmadan);
        tamponluKapatilmadan.write("teslim edilmeyecek".getBytes());

        ByteArrayOutputStream altBosaltilan = new ByteArrayOutputStream();
        BufferedOutputStream tamponluBosaltilan = new BufferedOutputStream(altBosaltilan);
        tamponluBosaltilan.write("teslim edilecek".getBytes());
        tamponluBosaltilan.flush();

        System.out.printf("%-26s%-12s%s%n", "olcum", "alt uzunluk", "alt akista veri var mi");
        System.out.printf("%-26s%-12d%s%n", "flush cagrilmadan",
                altKapatilmadan.size(), altKapatilmadan.size() > 0);
        System.out.printf("%-26s%-12d%s%n", "flush cagrildiktan sonra",
                altBosaltilan.size(), altBosaltilan.size() > 0);
    }
}
olcum                     alt uzunluk alt akista veri var mi
flush cagrilmadan         0           false
flush cagrildiktan sonra  15          true

flush çağrılmadan alttaki akışın uzunluğu sıfır: on dokuz baytlık metin hâlâ BufferedOutputStream’in kendi belleğinde bekliyor, alt akışa hiç dokunulmamış. flush çağrıldıktan sonra aynı düzenek on beş baytı eksiksiz teslim ediyor. Aradaki fark kodun doğruluğunda değil, bir çağrının yapılıp yapılmadığındadır — ve o çağrıyı yapmak tamamen çağıranın kararıdır.

Kaynakla deneme (M08/K02 Nesneye Dayalı Java, java-genellikler/04) bu çağrıyı otomatikleştirir: bir BufferedOutputStream try (...) içinde açılırsa, bloktan çıkarken close çalışır ve close içeriden flush’ı çağırır. Ama bu otomasyonun devreye girmesi için tamponlu akışın kaynak olarak bildirilmiş olması gerekir; yukarıdaki ilk ölçümde tamponluKapatilmadan hiçbir try (...) bloğuna girmediği için kapatma hiç çalışmadı ve veri kayboldu. Kaynakla deneme kaybı önler, ama önleyecek satırı yazmak yine çağırana kalır.

Bazı sınıflar bu kararı kendi üzerlerine alır: PrintWriter’ın bir kurucusu her satır sonunda otomatik boşaltma isteyip istemediğini bir boolean parametreyle sorar. BufferedOutputStream’in böyle bir seçeneği yok — o hep manuel kalır. Bu fark bile aynı kuralı doğruluyor: otomatik boşaltma bir sınıfın tercih ettiği bir davranıştır, girdi-çıktı akışı sınıflarının hepsinin uyduğu bir arayüz sözü değildir. Hangi sınıfı seçtiğin, boşaltma kararını kendine mi bırakacağını yoksa sınıfa mı devredeceğini de belirliyor.

Özet

  • Bayt akışının write(int) için verdiği tek söz düşük sekiz biti taşımaktır; bu söz farklı gerçekleştirimlerde aynı çıkıyor, çünkü üst sınıfın kendisinde tanımlıdır.
  • Tamponlama alt akışa varan çağrı sayısını değiştirir, teslim edilen içeriği değiştirmez; beş çağrı bire iniyor ama sonuç birebir aynı kalıyor.
  • Karakter akışı bir kodlamaya yaslanır; bayt akışı hiçbir yorum sözü vermez. Kodlama verilmediğinde karar ortama düşer.
  • Yazma ve okuma kodlaması ayrıldığında hata hiçbir yerde durmaz: yedi karakterlik bir metin sessizce on karaktere dönüşür.
  • Sınırlayıcı ölçüm: boşaltılmamış bir tampon veriyi eksik bırakır. Kaynakla deneme bu kaybı önler, ama tamponlu akışı kaynak olarak bildirme kararı çağırana aittir.

Sonraki Adım

Bu derste ölçülen her şey bir akışın içinden geçen içerikle ilgiliydi: hangi bayt yazıldı, hangi karakter okundu. Ama bir akış açılmadan önce bir soru daha var: akışın ucundaki şey gerçekten var mı, ve ona giden yol ne anlatıyor? Sıradaki 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.

İ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