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
- IO1 —
OutputStream.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
& 0xFFile işaretsiz tamsayıya çevrilir; imzalıbytedeğ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
- IO3 —
InputStream.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ı
byteolarak 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
BufferedOutputStreamyazıldıktan hemen sonra,flushçağrılmadan, alttaki akışın içeriğine doğrudan bakılır. İkinci ölçümde aynı adımlarflushile 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.