Java ile Nesne Yönelimli Programlama · Ders Notu
Bölüm 8: Giriş Çıkış
Bir programın değişkenleri bellekte yaşar ve program kapanınca kaybolur. Kalıcı olan her şey, yani ölçüm kayıtları, ayar dosyaları, resimler, öğrenci listeleri, programın dışında bir yerde durur: diskte, ağda ya da bir cihazda. Programın bu dış dünyayla veri alışverişine giriş çıkış (G/Ç) denir. Java bu alışverişi tek bir fikir üzerine kurar: veri, bir kaynaktan bir hedefe sırayla akan bir akıştır. Not önce akış kavramını ve klavye ile ekranın standart akışlarını anlatır. Ardından dosyaların içindeki baytlara iner: metin ile ikili dosyanın farkı, karakter kodlamaları ve bir sayının dosyada kaç bayt tuttuğu. Sonraki kesimler File sınıfını, metin okuma ve yazma sınıflarını, tamponlamayı, öteki bayt akışlarını ve dosya kopyalamanın yollarını işler. Son kesimler rastgele erişimi, nesnelerin serileştirilmesini ve Java'nın tampon ile kanal üzerine kurulu NIO kütüphanesini anlatır.
Öğrenme çıktıları
- Akışı açıklamak
- Girdi ve çıktı akışını,
System.in,System.outveSystem.errakışlarını anlatmak; klavyedenScannerile okumak. - Metin ile ikiliyi ayırmak
- Metin ve ikili dosya farkını, ASCII, ISO-8859-9 ve UTF-8 kodlamalarını açıklamak; bir sayının dosyada kaç bayt tuttuğunu hesaplamak.
- Dosya okumak ve yazmak
File,Scanner,BufferedReader,PrintWriterveFileWriterile metin dosyası işlemek; kaynakları try-with-resources ile kapatmak.- Doğru akışı seçmek
- Bayt (
InputStream/OutputStream) ve karakter (Reader/Writer) akışlarını, tamponlu ve sarmalayıcı akışları ayırt etmek. - Kopyalamak, konumlanmak
- Bayt, blok ve satır düzeyinde kopyalamak;
RandomAccessFileile istenen konuma atlamak. - Nesne saklamak ve NIO
- Nesneleri serileştirmek (
Serializable,transient); NIO tamponunun, kanalının ve seçicisinin ne işe yaradığını açıklamak.
8.1Akışlar
Bir program veriyi dışarıdan alır, işler ve sonucu dışarıya verir. Dışarısı her seferinde başka bir yer olabilir: klavye, ekran, diskteki bir dosya, ağdaki bir sunucu ya da bellekteki bir dizi. Bu kaynakların her biri için ayrı bir okuma yöntemi öğrenmek hem zor hem de gereksiz olurdu. Java hepsini aynı soyutlamayla ele alır: akış.
8.1.1Akış nedir?
Akış (stream), veriyi bir kaynaktan bir hedefe sırayla, parça parça taşıyan yapıdır. Verinin programa girdiği akışa girdi akışı (input stream), programdan çıktığı akışa çıktı akışı (output stream) denir. Taşınan parçalar karakter, sayı ya da ham bayt olabilir.
Akışı bir su borusuna benzetmek yerinde olur. Borunun bir ucundan giren su öbür ucundan aynı sırayla çıkar. Borunun öbür ucunda musluk mu, depo mu olduğu suyu içen kişiyi ilgilendirmez. Akış da böyledir. Program yalnız akışın kendi ucunu görür. Akışın öbür ucunda klavye, dosya ya da ağ bağlantısı olabilir; program her durumda aynı read() ve write() metotlarını çağırır. Java G/Ç (giriş/çıkış, I/O) kütüphanesinin gücü bu ortak arayüzden gelir. Şekil 8.1 bu yapıyı gösterir.
InputStream ve OutputStream, karakter akışlarınınki Reader ve Writer'dır.Akışın iki özelliği bütün bölüm boyunca önem taşır. Birincisi sıradır: akıştan okunan veri yazıldığı sırayla gelir, bir veri atlanıp sonrakine geçilemez. Bu yüzden akışlarla dosyanın ortasındaki bir kayda doğrudan gidilemez; bunun için rastgele erişim gerekir. İkincisi yöndür: bir akış ya girdi ya çıktı akışıdır, ikisi birden olamaz. Aynı dosyayı hem okumak hem yazmak isteyen program iki ayrı akış açar.
8.1.2Standart akışlar
Klavye ve ekran en basit G/Ç aygıtlarıdır. Bunlarla yapılan veri işleme geçicidir: klavyeden girilen ve ekrana yazılan veri saklanmaz, program kapanınca kaybolur. Ayrıca anındadır: kullanıcı yazdıkça girdi gelir, program yazdıkça çıktı ekranda görünür. Her Java programı çalışmaya başlarken işletim sisteminden üç hazır akış alır. Bunlara standart akışlar denir (Tablo 8.1).
| Akış | Java nesnesi | Tipi | Varsayılan uç |
|---|---|---|---|
| standart girdi | System.in | InputStream | klavye |
| standart çıktı | System.out | PrintStream | ekran |
| standart hata | System.err | PrintStream | ekran |
System.in ile bağlanır; program ekrana System.out ve System.err ile yazar. Numaralar işletim sistemindeki kanal numaralarıdır (0, 1, 2).System.out ile System.err ikisi de ekrana yazar. Peki neden iki ayrı akış vardır? Fark, çıktı yönlendirildiğinde ortaya çıkar. Komut satırında java Uygulama > sonuc.txt yazılırsa > işareti yalnız standart çıktıyı sonuc.txt dosyasına yönlendirir. System.err'e yazılanlar ekranda kalır. Böylece hata iletileri hem çıktı dosyasını kirletmez hem de gözden kaçmaz. Programın asıl sonucu System.out'a, uyarılar ve hata iletileri System.err'e yazılır.
System.in'in tipi InputStream'dir; yani ham bayt verir. Kullanıcı klavyede 42 yazıp Enter'a bastığında akışa üç bayt girer: '4', '2' ve satır sonu. Bu baytları bir int değerine çevirmek programcıya düşer. Bu işi java.util.Scanner sınıfı yapar.
8.1.3İlk örnek: okuma ve yazma
Aşağıdaki program üç standart akışı birlikte kullanır. Klavyeden bir sayı okur, iki satırı standart çıktıya, bir satırı standart hataya yazar.
import java.util.Scanner;
public class AkisOrnek {
public static void main(String[] args) {
// Giriş akışından veri okuma
Scanner scanner = new Scanner(System.in);
int kullaniciGirdisi = scanner.nextInt();
// Çıkış akışına veri yazma
System.out.println("Merhaba, Dünya!");
System.out.println("Girdiğiniz sayı: " + kullaniciGirdisi);
System.err.println("Bu satır hata akışına yazıldı.");
}
}Kullanıcı 42 yazarsa konsolda şu satırlar görünür (ilk satır kullanıcının yazdığı girdidir):
42
Merhaba, Dünya!
Girdiğiniz sayı: 42
Bu satır hata akışına yazıldı.Program satır satır şöyle çalışır:
new Scanner(System.in)klavye akışını okuyan bir tarayıcı oluşturur.Scannerjava.utilpaketindedir; bu yüzden dosyanın başındakiimportsatırı şarttır.scanner.nextInt()kullanıcı bir sayı yazıp Enter'a basana kadar bekler. Bu beklemeye bloklanma (blocking) denir: program o satırda durur ve girdi gelince devam eder.System.out.printlnçıktı akışına bir satır yazar ve satırı bitirir.System.err.printlnhata akışına yazar. Çoğu geliştirme ortamı bu satırı kırmızı gösterir.
Kullanıcı sayı yerine abc yazarsa nextInt() bu metni sayıya çeviremez ve java.util.InputMismatchException fırlatır. Program yakalanmamış istisnayla sona erer:
Exception in thread "main" java.util.InputMismatchException
at java.base/java.util.Scanner.throwFor(Scanner.java:947)
at java.base/java.util.Scanner.next(Scanner.java:1602)
at java.base/java.util.Scanner.nextInt(Scanner.java:2267)
at java.base/java.util.Scanner.nextInt(Scanner.java:2221)
at AkisOrnek.main(AkisOrnek.java:7)Bu istisna kontrol edilmeyen bir istisnadır; derleyici onu yakalamayı zorunlu tutmaz. Kullanıcıdan gelen girdiye güvenilmeyen programlarda hasNextInt() ile önce denetim yapılır ya da istisna yakalanıp kullanıcıdan yeniden girdi istenir.
8.1.4Kod izleme: klavyeden okuma
Aşağıdaki program kullanıcının adını ve yaşını sorar, ertesi yılki yaşını hesaplar. Adı okumak için nextLine(), yaşı okumak için nextInt() kullanılır. İki metodun satır sonunu farklı ele alması bu bölümün en sık görülen hatalarından birine yol açar; izleme bu farkı görünür kılar.
import java.util.Scanner;
public class KonsolOkuma {
public static void main(String[] args) {
Scanner klavye = new Scanner(System.in);
System.out.print("Adınız: ");
String ad = klavye.nextLine();
System.out.print("Yaşınız: ");
int yas = klavye.nextInt();
int seneye = yas + 1;
System.out.println("Merhaba " + ad + "!");
System.out.println("Seneye " + seneye + " yaşındasın.");
klavye.close();
}
}Kullanıcı ad olarak Ayşe Yılmaz, yaş olarak 19 yazsın. Tablo 8.2 programın her adımını verir. Satır numaraları yukarıdaki kodun satırlarıdır.
| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 5: Scanner klavye = new Scanner(System.in); | klavye=Scanner(System.in) | — |
| 2 | 6: System.out.print("Adınız: "); | klavye=Scanner(System.in) | Adınız: |
| 3 | 7: String ad = klavye.nextLine(); | ad="Ayşe Yılmaz" | Ayşe Yılmaz |
| 4 | 8: System.out.print("Yaşınız: "); | ad="Ayşe Yılmaz" | Yaşınız: |
| 5 | 9: int yas = klavye.nextInt(); | ad="Ayşe Yılmaz", yas=19 | 19 |
| 6 | 10: int seneye = yas + 1; | ad="Ayşe Yılmaz", yas=19, seneye=20 | — |
| 7 | 11: System.out.println("Merhaba " + ad + "!"); | ad="Ayşe Yılmaz", yas=19, seneye=20 | Merhaba Ayşe Yılmaz! |
| 8 | 12: System.out.println("Seneye " + seneye + " yaşındasın."); | ad="Ayşe Yılmaz", yas=19, seneye=20 | Seneye 20 yaşındasın. |
| 9 | 13: klavye.close(); | ad="Ayşe Yılmaz", yas=19, seneye=20 | — |
Birinci adımda Scanner nesnesi System.in akışına bağlanır. Henüz hiçbir şey okunmamıştır ve klavye tamponu boştur. İkinci adımda print (println değil) Adınız: sorusunu yazar. print satırı bitirmediği için kullanıcının cevabı sorunun hemen yanına yazılır.
Üçüncü adım önemlidir. Kullanıcı Ayşe Yılmaz yazıp Enter'a basar. Klavye tamponunda artık Ayşe Yılmaz⏎ vardır. nextLine() satır sonuna kadar olan her şeyi, boşluk dahil alır ve satır sonunu da tampondan atar. ad değişkenine "Ayşe Yılmaz" atanır; satır sonu bu metnin parçası değildir.
Beşinci adımda kullanıcı 19 yazıp Enter'a basar. nextInt() baştaki boşlukları atlar, rakamları okur ve int değeri 19'a çevirir. Satır sonuna ise dokunmaz: ⏎ tamponda kalır. Bu programda sorun çıkmaz, çünkü ardından bir okuma daha yapılmıyor. Sonraki adımlarda yas artık sıradan bir int değeridir: 19 + 1 = 20. Çıktı satırları dizgi birleştirmesiyle kurulur: "Seneye " + 20 + " yaşındasın.".
nextLine() satırın tamamını alır ve satır sonunu atar. (b) nextInt() rakamları alır; satır sonu tamponda kalır.Son satırdaki klavye.close() yalnız tarayıcıyı değil, onun sardığı System.in akışını da kapatır. Kapanan standart girdi yeniden açılamaz. Program içinde daha sonra yeni bir Scanner(System.in) oluşturulsa bile klavyeden okuma yapılamaz. Aşağıdaki küçük deneme bunu gösterir: ilk tarayıcı kapatıldıktan sonra ikinci tarayıcı, girdide bekleyen bir satır olduğu hâlde NoSuchElementException fırlatır.
okunan: bir
java.util.NoSuchElementException: No line foundBu yüzden klavyeyi okuyan tarayıcı programda bir kez oluşturulur ve gerekiyorsa yalnız programın sonunda kapatılır. Dosyayı okuyan tarayıcılar ise iş bitince hemen kapatılmalıdır; bu fark dosya kesimlerinde yeniden ele alınır.
8.1.5nextInt'ten sonra nextLine
Tampondaki satır sonunun etkisi, sıra tersine çevrildiğinde ortaya çıkar. Önce yaş, sonra ad okunsun:
import java.util.Scanner;
public class SonraSatir {
public static void main(String[] args) {
Scanner klavye = new Scanner(System.in);
System.out.print("Yaş: ");
int yas = klavye.nextInt();
System.out.print("Ad: ");
String ad = klavye.nextLine();
System.out.println("[" + ad + "] " + yas);
}
}Kullanıcı 19 yazıp Enter'a basar. nextInt() 19'u okur ve satır sonunu tamponda bırakır. Ardından nextLine() çağrılır. Bu metot satır sonuna kadar olan metni ister. Tamponda zaten bir satır sonu bekler; önündeki metin boştur. nextLine() hiç beklemeden boş dizgiyi döndürür. Kullanıcı adını yazmaya fırsat bulamadan program biter:
Yaş: Ad: [] 19nextInt(), nextDouble() ya da next() çağrısından hemen sonra nextLine() ile metin okumak. İlk nextLine() önceki satırdan kalan satır sonunu okur ve boş dizgi döndürür. İki çözüm vardır: araya satır sonunu atan fazladan bir klavye.nextLine(); konur ya da sayı da satır olarak okunup çevrilir: int yas = Integer.parseInt(klavye.nextLine());.
İkinci çözüm genellikle daha temizdir. Her okuma bir satırın tamamını tükettiği için tamponda artık bir şey kalmaz. Kullanıcı sayının yanına fazladan boşluk yazarsa parseInt hata verir; bu durumda klavye.nextLine().trim() kullanılır.
8.2Dosyalar ve kodlama
Klavye ve ekranla yapılan alışveriş geçicidir. Kalıcı veri için dosya gerekir. Bu kesim bir dosyanın aslında ne olduğunu, metin ve ikili dosyaların farkını ve karakterlerin baytlara nasıl çevrildiğini anlatır. Bu bilgiler sonraki kesimlerde hangi akış sınıfının seçileceğini belirler.
8.2.1Dosya: diskteki bayt dizisi
Dosya, veriyi diskte kalıcı olarak saklar. Program kapansa ya da bilgisayar yeniden başlasa da veri yerinde durur. RAM'deki değişkenler ise programla birlikte kaybolur. Her dosyadaki veri sonuçta 0'lar ve 1'lerden, yani bir bayt dizisinden oluşur. Dosyalar arasındaki fark bu baytların nasıl yorumlandığından doğar.
Metin dosyasında (text file) baytlar bir karakter kodlamasına göre harflere çevrilir; insan dosyayı bir metin düzenleyicisiyle okuyabilir. İkili dosyada (binary file) baytlar sayı, piksel, ses örneği ya da makine komutu gibi verilerdir; onları ancak dosyanın biçimini bilen bir program anlamlandırır.
Bir dosyanın baytlarını görmek için onaltılık (hexadecimal, hex) düzenleyici kullanılır. Bu araç her baytı iki onaltılık rakamla gösterir. İçinde Merhaba, dünya! yazan dosya.txt onaltılık düzenleyicide şöyle görünür:
0000 4D 65 72 68 61 62 61 2C Merhaba,
0008 20 64 C3 BC 6E 79 61 21 d..nya!Soldaki sütun satırın ilk baytının konumudur: 0000 ve 0008. Ortada her baytın onaltılık değeri, sağda ASCII karşılığı vardır. İlk bayt 4D'dir: 4 · 16 + 13 = 77, ASCII'de büyük M harfi. Metin 15 karakterdir ama dosya 16 bayt tutar. Fark ü harfinden gelir: UTF-8 kodlamasında ü iki bayttır, C3 BC. Sağ sütundaki iki nokta bu iki baytın ASCII'de karşılığı olmadığını gösterir. Bu dosya Java ile üretilip baytları yazdırıldığında aynı 16 değer elde edilir:
16 bayt: 4D 65 72 68 61 62 61 2C 20 64 C3 BC 6E 79 61 21
1 bayt: 7F
1382 bayt: CA FE BA BE 00 00 00 41 00 5C 0A 00 02 00 03 07 00 04 0C 00 05 00 06 01 00 10 6A 61 76 61 2F 6C
·······A·\················java/lang/Object···<init>···()V······java/io/FileWriter······dosya.txt·············(Ljava/lang
major = 65Çıktının ilk satırı bu dosyanın, ikinci satırı veri.dat dosyasının baytlarını verir. Kalan satırlar bir .class dosyasının başını gösterir; bunlar ikili dosyalar ele alınırken açıklanır.
8.2.2Dosya işleme süreci
Hangi dil kullanılırsa kullanılsın, bir dosyayla çalışmak aynı adımlardan geçer. Şekil 8.4 adımları, Tablo 8.3 her adımın Java'daki karşılığını verir.
| Adım | Ne olur? | Java'da |
|---|---|---|
| Oluşturma | Diskte yeni, boş bir dosya oluşur. | file.createNewFile(); yazmak için açmak da dosyayı oluşturur |
| Açma | Var olan dosyaya bir akış bağlanır. | new FileInputStream(ad), new FileReader(ad), new Scanner(file) |
| Yazma | Veri dosyaya aktarılır. | write(), print(), println() |
| Okuma | Veri dosyadan programa alınır. | read(), readLine(), nextLine() |
| Kapatma | Tamponda kalan veri diske yazılır, işletim sistemi kaynağı bırakılır. | close() ya da try-with-resources |
Son adım en sık unutulan adımdır. Kapatılmayan bir dosya iki soruna yol açar. Birincisi veri kaybıdır: yazılan baytların bir kısmı bellekteki bir tamponda bekler ve dosya kapatılmadan diske gitmeyebilir. İkincisi kaynak sızıntısıdır: işletim sistemi her açık dosya için bir tanıtıcı (file handle) ayırır. Bir sürecin aynı anda açık tutabileceği dosya sayısı sınırlıdır. Uzun süre çalışan ve dosyalarını kapatmayan bir program bir süre sonra yeni dosya açamaz hâle gelir. Windows'ta açık kalan bir dosya ayrıca silinemez ve taşınamaz.
8.2.3İlk dosya programı
Aşağıdaki program bir metin dosyasına bir cümle, bir ikili dosyaya tek bir bayt yazar. Sonra ikili dosyadaki baytı geri okur ve metin dosyasının boyutunu yazdırır.
import java.io.*;
public class IlkDosya {
public static void main(String[] args) throws IOException {
// Metin dosyasına yazma
FileWriter writer = new FileWriter("dosya.txt");
writer.write("Merhaba, dünya!");
writer.close();
// İkili dosyaya bir byte yazma
FileOutputStream cikis = new FileOutputStream("veri.dat");
cikis.write(127);
cikis.close();
// İkili dosyadan okuma
FileInputStream stream = new FileInputStream("veri.dat");
int veri = stream.read();
stream.close();
System.out.println("Okunan byte: " + veri);
File metin = new File("dosya.txt");
System.out.println("dosya.txt: " + metin.length() + " byte");
}
}Okunan byte: 127
dosya.txt: 16 byteProgram çalıştıktan sonra dosya.txt'nin içeriği şudur:
Merhaba, dünya!Programda üç farklı akış sınıfı vardır:
FileWritermetin yazar. Kendisine verilen karakterleri bir karakter kodlamasıyla baytlara çevirip dosyaya gönderir. Java 18'den beri varsayılan kodlama UTF-8'dir;übu yüzden iki bayt olur ve 15 karakterlik metin 16 bayt tutar.FileOutputStreamham bayt yazar.write(127)çağrısı dosyaya tek bir bayt koyar: onaltılık7F, ikilik0111 1111. Bir karakter dönüşümü yapılmaz.FileInputStreamham bayt okur.read()bir baytı 0–255 arasında birintolarak döndürür. Okunacak bayt kalmamışsa −1 döndürür; bu ayrıntı kopyalama kesiminde önem kazanır.
main metodunun başlığındaki throws IOException, dosya işlemlerinin kontrol edilen istisna fırlatabileceğini bildirir. Dosya bulunamayabilir, disk dolabilir, izin olmayabilir. Derleyici bu durumların ele alınmasını zorunlu tutar: ya try-catch ile yakalanır ya da metot başlığında throws ile bildirilir. Bu kısa örnekler ikinci yolu seçer. Gerçek programlarda hata iletisini kullanıcıya anlaşılır biçimde vermek için yakalamak daha uygundur.
8.2.4Metin dosyaları ve karakter kodlaması
Metin dosyaları yazdırılabilir karakterler içerir: harfler, rakamlar, noktalama işaretleri, boşluk ve satır sonu. Not Defteri ya da VS Code gibi metin düzenleyicileriyle açılıp düzenlenebilirler. Kaynak kodlar (.java, .c), .txt, .csv ve .html dosyaları metin dosyasıdır.
Bilgisayar harf saklayamaz, yalnız sayı saklar. Bu yüzden her karaktere bir sayı karşılık getirilir. Bu eşleme kuralına karakter kodlaması denir.
Karakter kodlaması (character encoding) ya da karakter kümesi (character set), her karakterin hangi sayıyla, yani hangi bayt ya da baytlarla saklanacağını belirleyen kuraldır. Bir metin dosyasını yazan ve okuyan program aynı kodlamayı kullanmalıdır.
Tarih içinde birçok kodlama ortaya çıkmıştır. Bu derste karşılaşılan dördü Tablo 8.4'da özetlenir.
| Kodlama | Karakter başına | Kapsam |
|---|---|---|
| ASCII | 7 bit (1 bayt) | 128 karakter: İngilizce harfler, rakamlar, noktalama ve kontrol karakterleri. |
| ISO-8859-1 (Latin-1) | 1 bayt | 256 karakter, Batı Avrupa dilleri. é, ü, ö, ç vardır; ş, ğ, ı yoktur. |
| ISO-8859-9 (Latin-5) | 1 bayt | Latin-1'in Türkçe sürümü. Latin-1'deki altı harfin yerine ğ Ğ ı İ ş Ş gelir. |
| UTF-8 | 1–4 bayt | Unicode'un evrensel kodlaması; bütün dünya dilleri. Web'in ve Java 18'den beri Java'nın varsayılan kodlaması. |
UTF-8 değişken uzunluklu bir kodlamadır. ASCII'deki 128 karakter UTF-8'de de aynı tek baytla yazılır; bu yüzden yalnız İngilizce harf içeren bir ASCII dosyası aynı zamanda geçerli bir UTF-8 dosyasıdır. Türkçe harfler ve öteki Latin harfleri iki bayt, avro işareti € gibi semboller üç bayt, emojiler dört bayt tutar. Aşağıdaki deneme getBytes metoduyla bu sayıları ölçer:
32 karakter, UTF-8 41, ISO-8859-9 32, UTF-16BE 64
IÅ<9f>ık, Å<9f>imÅ<9f>ek, çiÄ<9f>, öÄ<9f>renci, Ã<9c>nye
ş: ASCII 3F, UTF-8 C5 9F, ISO-8859-9 FE, cp1254 FE
þimdi
şimdi
Çağrı UTF-8: 8, ISO-8859-9: 5, UTF-16BE: 10
€ UTF-8: 3, 😀 UTF-8: 4, 😀 length(): 2
varsayılan: UTF-8 / native.encoding=Cp1254
4D 65 72 68 61 62 61 2C 20 64 C3 BC 6E 79 61 21İlk satır, 32 karakterlik Türkçe bir cümlenin UTF-8'de 41, ISO-8859-9'da 32, UTF-16'da 64 bayt tuttuğunu gösterir. Cümlede iki baytlık dokuz Türkçe harf vardır: 32 + 9 = 41. İkinci satır aynı baytların yanlış kodlamayla çözülmesidir; bu bozulma kodlama belirtme kesiminde ayrıntılı ele alınır. ş satırı ise bir harfin dört kodlamadaki baytlarını verir. ASCII'de ş yoktur; Java onun yerine soru işaretini (3F) yazar. “Çağrı” satırı da ilginç bir ayrıntı verir: "Çağrı" beş karakterdir ama UTF-8'de 8 bayt tutar. Ç, ğ ve ı ikişer, a ve r birer bayttır: 2 + 1 + 2 + 1 + 2 = 8. Aynı metin ISO-8859-9 ile 5, UTF-16 ile 10 bayttır. 😀 için length() 2 döndürür. Java'nın char tipi 16 bittir ve 65 536'dan büyük kodlu karakterler iki char ile temsil edilir. Dolayısıyla bir dizginin uzunluğu, ne karakter sayısına ne de bayt sayısına her zaman eşittir.
Çıktıdaki “varsayılan” satırı da dikkat ister. Java 18 ve sonrasında varsayılan kodlama işletim sisteminden bağımsız olarak UTF-8'dir. İşletim sisteminin kendi kodlaması ise native.encoding özelliğinde durur; Türkçe Windows'ta bu Windows-1254'tür (Cp1254). Java 17 ve öncesinde varsayılan kodlama doğrudan bu işletim sistemi ayarıydı. Aynı program Windows'ta ve Linux'ta farklı baytlar yazabiliyordu.
8.2.5İkili dosyalar
İkili dosyalar baytları karakter olarak değil, programın belirlediği bir biçimde (format) saklar: sayılar, pikseller, ses örnekleri, makine kodu. Bu baytlar ancak biçimi bilen program için anlamlıdır; insan onları doğrudan okuyamaz. Çalıştırılabilir dosyalar (.exe, Java'nın .class dosyaları), görüntüler (.png, .jpg), müzik (.mp3), video (.mp4) ve veri dosyaları (.dat) ikili dosyadır. İkili dosyaların avantajı daha küçük olmaları ve daha hızlı işlenmeleridir: sayıyı metne, metni sayıya çevirmek gerekmez.
Önceki programın derlenmesiyle oluşan IlkDosya.class'ın ilk 32 baytı onaltılık düzenleyicide şöyle görünür:
0000 CA FE BA BE 00 00 00 41 .......A
0008 00 5C 0A 00 02 00 03 07 .\......
0010 00 04 0C 00 05 00 06 01 ........
0018 00 10 6A 61 76 61 2F 6C ..java/lHer .class dosyası CA FE BA BE baytlarıyla başlar. Bu dört bayta sihirli sayı (magic number) denir; JVM dosyanın gerçekten bir sınıf dosyası olduğunu bu sayıdan anlar. Sonraki dört bayt sürüm bilgisidir. 00 41 = 65, Java 21'in sınıf dosyası sürümüdür; bu sayı JDK 21 ile derlenen dosyada Java ile de okunmuştur (önceki çıktıdaki major = 65). Ardından sabitler havuzu gelir. Okunabilir parçalar da görünür: java/lang/Object gibi sınıf adları dosyada metin olarak saklanır. Aynı dosya Not Defteri ile açıldığında anlamsız bir karakter dizisi çıkar:
Êþº¾···A·\················java/lang/Object···<init>···()V······java/io/FileWriter······dosya.txt·············(Not Defteri baytları bir metin kodlamasıyla çevirmeye çalışır. CA, FE, BA ve BE Latin-1'de sırasıyla Ê, þ, º ve ¾ karakterleridir. Yazdırılamayan baytlar ise nokta ya da boşluk gibi görünür. Bir ikili dosyayı metin düzenleyicisinde açıp kaydetmek onu bozabilir: düzenleyici bazı baytları değiştirebilir ya da satır sonlarını dönüştürebilir.
8.2.6Genişletilmiş ASCII ve Türkçe karakterler
ASCII 7 bittir ve 128 karakteri kapsar. Bir bayt ise 8 bittir. Kalan bir bit, 128 yeni karakter için yer açar.
Genişletilmiş ASCII (extended ASCII), 8 bitin tamamını kullanarak 28 = 256 karakter temsil eden kodlamalara verilen genel addır. 0–127 arası standart ASCII karakterleridir ve her tabloda aynıdır. 128–255 arası özel karakterlere, sembollere ve ulusal harflere ayrılmıştır.
Tek bir “genişletilmiş ASCII” yoktur. Üst yarının hangi karakterleri tuttuğu kod sayfasına (code page) göre değişir. Türkçe için iki kod sayfası kullanılır: ISO-8859-9 (Latin-5) ve Windows-1254. İkisi Türkçe harflerde aynı baytları kullanır. Tablo 8.5 aynı baytların Latin-1 ve Latin-5'teki karşılığını yan yana verir.
| Bayt | D0 | DD | DE | F0 | FD | FE | FC | E7 |
|---|---|---|---|---|---|---|---|---|
| ISO-8859-1 | Ð | Ý | Þ | ð | ý | þ | ü | ç |
| ISO-8859-9 | Ğ | İ | Ş | ğ | ı | ş | ü | ç |
FC ve E7 iki tabloda da ü ve ç'dir; bu harfler Batı Avrupa dillerinde de vardır. İlk altı bayt ise tablodan tabloya değişir. Aynı FE baytı bir tabloda ş, ötekinde İzlandaca þ harfidir. Türkçe bir dosya yanlış kod sayfasıyla açıldığında “şimdi” yerine “þimdi” gibi bozuk metinler çıkar (Şekil 8.5). Önceki çıktıdaki þimdi ve şimdi satırları aynı beş bayttan iki kod sayfasıyla elde edilmiştir.
FE baytı ISO-8859-9'da ş, ISO-8859-1'de þ'dir. Dosyayı yanlış kod sayfasıyla açmak metni bozar.Bu sorunun kalıcı çözümü bütün dilleri tek tabloda toplayan Unicode ve onun bayt kodlaması UTF-8'dir. UTF-8'de her karakterin tek bir bayt dizisi vardır; okuyanın hangi kod sayfasını seçeceğini tahmin etmesi gerekmez. Bugün yeni yazılan her metin dosyası için UTF-8 seçilir.
8.2.7Bir dosyaya 127 yazmak
Bir sayının dosyada nasıl saklanacağı veri tipine, kullanım amacına ve dosya biçimine bağlıdır. 127 sayısı iki temelden farklı yolla yazılabilir.
- Metin olarak yazıldığında sayı önce karakterlere çevrilir:
"127". Her rakam bir ASCII baytı tutar. Burada sık karıştırılan bir nokta vardır:'1'karakterinin kodu 1 değil, 49'dur.'2'50,'7'55'tir. Dosyada31 32 37baytları durur. - İkili olarak yazıldığında sayının ikilik değeri,
0111 1111, tipin boyutu kadar bayta doğrudan yazılır.byteiçin 1,shortiçin 2,intiçin 4 bayt kullanılır. Sayısal veri için bu yol daha verimlidir.
Aşağıdaki program bu beş gösterimi gerçekten dosyaya yazar ve dosyaların boyutunu ölçer. Metin yazmak için OutputStreamWriter, ikili yazmak için DataOutputStream kullanılır. Kısalık için import java.io.*; ve import java.nio.charset.*; satırları gösterilmemiştir.
public class Sayi127 {
static void metinYaz(String ad, Charset kod) throws IOException {
Writer w = new OutputStreamWriter(new FileOutputStream(ad), kod);
w.write(String.valueOf(127)); // "127": 3 karakter
w.close();
}
static void ikiliYaz(String ad, int boyut) throws IOException {
FileOutputStream fos = new FileOutputStream(ad);
DataOutputStream d = new DataOutputStream(fos);
if (boyut == 1) d.writeByte(127); // 01111111
else if (boyut == 2) d.writeShort(127); // 00000000 01111111
else d.writeInt(127); // 4 byte
d.close();
}
public static void main(String[] args) throws IOException {
metinYaz("metin.txt", StandardCharsets.US_ASCII);
metinYaz("metin16.txt", StandardCharsets.UTF_16BE);
ikiliYaz("b.dat", 1); ikiliYaz("s.dat", 2); ikiliYaz("i.dat", 4);
String[] adlar = {"metin.txt", "metin16.txt",
"b.dat", "s.dat", "i.dat"};
for (String ad : adlar)
System.out.println(ad + ": " + new File(ad).length());
}
}metin.txt: 3
metin16.txt: 6
b.dat: 1
s.dat: 2
i.dat: 4metinYaz metodu bir FileOutputStream'i OutputStreamWriter ile sarar. OutputStreamWriter karakterleri, kendisine verilen kodlamayla baytlara çeviren köprü sınıftır. ASCII ile "127" üç bayt tutar. UTF_16BE ise her karakteri iki baytla yazar ve 6 bayt tutar. Bu kodlamanın adındaki BE (big-endian) baytların sırasını sabitler; bu yüzden dosyanın başına sıra işareti (byte order mark, BOM) yazılmaz. Sıra işaretli UTF_16 kullanılsaydı başa iki bayt FE FF eklenir ve dosya 8 bayt tutardı.
ikiliYaz metodu DataOutputStream kullanır. Bu sınıf ilkel tipleri sabit boyutlu ikili gösterimleriyle yazar: writeByte 1, writeShort 2, writeInt 4 bayt. Baytlar en anlamlı bayt önce gelecek biçimde yazılır. Dosyaların gerçek içerikleri ölçümü doğrular:
metin.txt: 3
metin16.txt: 6
b.dat: 1
s.dat: 2
i.dat: 4
metin.txt -> 31 32 37
metin16.txt -> 00 31 00 32 00 37
b.dat -> 7F
s.dat -> 00 7F
i.dat -> 00 00 00 7F
UTF_16 (BOM'lu): 8
iki int: 8
7 2000000000
yanlış sıra/tip: 0 7
EOFException: nullÇıktının son satırları DataInputStream ile ilgili iki önemli kuralı gösterir. İkili dosyadaki değerler yalnız yazıldıkları sırayla ve tiple doğru geri okunur. İki int yazılan dosya iki readShort() ile okunursa 0 ve 7 gelir; yani ilk int'in dört baytı iki parçaya bölünür. Dosyada artık veri yokken readInt() çağrılırsa EOFException (end of file, dosya sonu) fırlatılır. Metin dosyası insana biçimini kendisi anlatır; ikili dosyanın biçimini ise yazan program ile okuyan programın önceden bilmesi gerekir.
8.2.8Metin mi, ikili mi?
Tablo 8.6 127 sayısının beş gösterimini boyutlarıyla karşılaştırır.
| 127 nasıl yazıldı? | Baytlar | Boyut |
|---|---|---|
| ASCII / UTF-8 metin dosyası | 31 32 37 | 3 bayt |
| UTF-16 metin dosyası (BOM'suz) | 00 31 00 32 00 37 | 6 bayt |
İkili, byte | 7F | 1 bayt |
İkili, short | 00 7F | 2 bayt |
İkili, int | 00 00 00 7F | 4 bayt |
writeInt ile yazılan dört baytlık ikili gösterimi.Tabloda ikili gösterim her zaman daha küçük görünüyor, ama bu yalnız 127 için doğrudur. İkili gösterimde boyut tipe bağlıdır: bir int her zaman 4 bayttır, değeri 7 de olsa 2 000 000 000 de olsa. Metin gösteriminde boyut basamak sayısına bağlıdır: 7 bir bayt, 2 000 000 000 ise on bayt tutar. Küçük sayılarda metin daha kısa olabilir; büyük sayılarda ikili gösterim açıkça kazanır. Ayrıca ikili okuma sayıyı çözümlemek zorunda kalmadığı için hızlıdır.
Seçim kullanım amacına göre yapılır:
- Metin insanların okuyacağı veriler içindir. Her düzenleyiciyle açılır, platformlar arasında kolay taşınır, bozulduğunda gözle onarılabilir. Kayıt dosyaları, ayar dosyaları ve CSV tabloları metindir.
- İkili sayısal verinin verimli saklanması içindir. Daha küçük ve hızlıdır, ama yazan programın biçimini bilmek gerekir. Resim, ses ve veri tabanı dosyaları ikilidir.
Java'nın bu iki yol için araçları bellidir. DataOutputStream ilkel tipleri ikili yazar (writeByte, writeShort, writeInt …). DataInputStream onları aynı sırayla geri okur. OutputStreamWriter ise karakterleri verilen kodlamayla baytlara çevirir.
8.3Dosya sistemi: File sınıfı
Bir dosyanın içeriğini okumadan önce çoğu zaman dosyanın kendisi hakkında sorular sorulur. Dosya var mı? Bir dosya mı, klasör mü? Boyutu ne? Okunabilir mi? Bu soruların cevabını java.io.File sınıfı verir.
8.3.1File bir yoldur
File nesnesi diskteki bir yolu (path) temsil eder: bir dosyanın ya da klasörün adı ve konumu. File dosyanın içeriğini okumaz ve yazmaz; bu iş akışların işidir. File yalnız dosya sistemine erişip yol hakkındaki soruları cevaplar ve dosya ya da klasör oluşturma, silme, listeleme gibi işlemleri yapar.
Bu ayrımın en önemli sonucu şudur: new File("ornek.txt") diskte hiçbir şey oluşturmaz. Yapıcı yalnız yolu bir nesnede saklar. Dosya diskte olmasa bile nesne oluşur ve istisna fırlatılmaz. Olmayan bir dosya için exists() false, length() 0 döndürür. Dosya bulunamadığında istisna ancak dosyayı okumak için bir akış açılırken gelir; örneğin new FileReader(f) FileNotFoundException fırlatır.
false 0 yok.txt false
mkdir: false, mkdirs: true
ikinci createNewFile: false
dolu klasör delete: false
lastModified(yok) = 0
1 a/bBu çıktının ilk satırı olmayan yok.txt için exists(), length(), getName() ve isFile() değerlerini verir. Kalan satırlar aşağıda açıklanan metotların ince noktalarını gösterir. Tablo 8.7 sık kullanılan metotları özetler.
| Metot | Döndürdüğü |
|---|---|
exists() | yol diskte var mı (boolean) |
isFile() / isDirectory() | dosya mı / klasör mü |
canRead(), canWrite(), canExecute() | okuma, yazma, çalıştırma izni |
createNewFile() | dosya oluşturuldu mu (zaten varsa false) |
mkdir() / mkdirs() | klasör oluşturuldu mu (mkdirs ara klasörleri de oluşturur) |
delete() | silindi mi (içi dolu klasör silinmez) |
list() / listFiles() | klasörün içeriği: String[] / File[] |
length() | boyut, bayt cinsinden (long) |
lastModified() | son değiştirilme zamanı, 1 Ocak 1970'ten beri milisaniye |
getName(), getAbsolutePath() | ad, tam yol |
Bu metotların çoğu hata durumunda istisna fırlatmaz, false döndürür. mkdir() ara klasörler yoksa başarısız olur; yukarıdaki çıktıda a/b/c için mkdir false, mkdirs ise true döndürür. createNewFile() dosya zaten varsa false döndürür, var olan dosyaya dokunmaz. delete() içi dolu bir klasörü silmez ve yine false döndürür. Olmayan bir dosyanın lastModified() değeri 0'dır. Sonuç olarak bu metotların dönüş değeri denetlenmelidir. Değer yok sayılırsa işlem sessizce başarısız olur ve hata ancak çok sonra fark edilir.
8.3.2Dosya ve klasör işlemleri
Aşağıdaki program bir klasör ve içinde boş bir dosya oluşturur, dosyanın özelliklerini sorgular, sonra her şeyi siler.
public class DosyaBilgi {
public static void main(String[] args) throws IOException {
File klasor = new File("ornek");
File dosya = new File("ornek/dosya.txt");
System.out.println("var mı: " + dosya.exists());
System.out.println("mkdir: " + klasor.mkdir());
System.out.println("oluştu: " + dosya.createNewFile());
System.out.println("dosya mı: " + dosya.isFile());
System.out.println("klasör mü: " + dosya.isDirectory());
System.out.println("okunur: " + dosya.canRead());
System.out.println("yazılır: " + dosya.canWrite());
System.out.println("boyut: " + dosya.length());
String[] icerik = klasor.list();
System.out.println("klasörde: " + icerik.length + " öğe");
long zaman = dosya.lastModified();
System.out.println("zaman > 0: " + (zaman > 0));
System.out.println("silindi: " + dosya.delete());
System.out.println("var mı: " + dosya.exists());
klasor.delete();
}
}var mı: false
mkdir: true
oluştu: true
dosya mı: true
klasör mü: false
okunur: true
yazılır: true
boyut: 0
klasörde: 1 öğe
zaman > 0: true
silindi: true
var mı: falseİlk satırda dosya henüz yoktur. mkdir() ornek klasörünü, createNewFile() içindeki boş dosyayı oluşturur; ikisi de true döndürür. Dosya yeni oluştuğu için boyutu 0'dır. klasor.list() klasördeki adları bir dizi olarak verir; dizinin uzunluğu 1'dir. lastModified() değeri program her çalıştığında farklıdır, bu yüzden program yalnız sıfırdan büyük olup olmadığını yazdırır. Son olarak dosya silinir ve exists() yeniden false olur. En son satır klasörü siler; klasör artık boş olduğu için silme başarılıdır.
Programdaki yolların ikisi de göreli yoldur: "ornek/dosya.txt" programın çalıştığı klasöre göre yorumlanır. Bu klasör bir geliştirme ortamında genellikle projenin kök klasörüdür. getAbsolutePath() göreli yolu tam yola çevirerek dosyanın gerçekte nerede arandığını gösterir. “Dosya bulunamadı” hatası alındığında ilk bakılacak yer burasıdır.
Yol ayırıcı Windows'ta ters bölü (\), Linux ve macOS'ta bölü (/) işaretidir. Java, Windows'ta da /'yu kabul eder; bu yüzden "ornek/dosya.txt" her sistemde çalışır. Sistemin kendi ayırıcısı File.separator sabitinde durur. Windows yollarında ters bölü kullanılacaksa Java dizgisinde iki kez yazılmalıdır: "C:\\dersler\\notlar.txt". Tek ters bölü bir kaçış dizisi başlatır. "C:\dersler" derlenmez, çünkü \d geçerli bir kaçış dizisi değildir:
Ana.java:5: error: illegal escape character
java.io.File f = new java.io.File("C:\dersler\notlar.txt");
^
1 errorDaha sinsi durum, kaçış dizisinin geçerli olduğu durumdur. "C:\notlar.txt" derlenir, çünkü \n satır sonudur. Yol sessizce C:, satır sonu ve otlar.txt'ye dönüşür:
DERLENDİ; çalıştı: C:
otlar.txt8.4Metin okuma ve yazma
Dosya işlemlerinin büyük kısmı metin dosyalarıyla yapılır. Bu kesim metin okumanın ve yazmanın yollarını, bunların arkasındaki sınıf yapısını ve kaynakların güvenle kapatılmasını anlatır.
8.4.1Scanner ile dosya okuma
Scanner yalnız klavyeyi değil, bir dosyayı ya da bir String'i de okuyabilir. Kaynağı değiştirmek için yapıcıya System.in yerine bir File nesnesi verilir. Okuma metotları aynıdır. Aşağıdaki program bir dosyayı satır satır okuyup ekrana yazar. Gereken importlar java.io.File, java.io.FileNotFoundException ve java.util.Scanner'dır.
public class ScannerDosya {
public static void main(String[] args) {
Scanner scanner = null;
try {
scanner = new Scanner(new File("ornekDosya.txt"));
// Dosyanın içeriğini okuma ve ekrana yazdırma
while (scanner.hasNextLine()) {
String satir = scanner.nextLine();
System.out.println(satir);
}
} catch (FileNotFoundException e) {
System.err.println("Dosya bulunamadı: " + e.getMessage());
} finally {
if (scanner != null)
scanner.close();
}
}
}ornekDosya.txt üç satır içersin: Java, Giriş Çıkış ve Akışlar. Program bunları aynen yazar:
Java
Giriş Çıkış
AkışlarProgramın üç noktası ayrıca açıklanmalıdır:
- Kontrol edilen istisna.
new Scanner(File)dosya yoksaFileNotFoundExceptionfırlatır. Bu kontrol edilen bir istisnadır: ya yakalanır ya da metot başlığında bildirilir. Program onu yakalar ve hata akışına bir ileti yazar. - Döngü.
hasNextLine()okunacak satır olup olmadığını sorar,nextLine()satırı alır. Dosya sonundahasNextLine()falsedöndürür ve döngü biter. finallyvenulldenetimi. Dosya açılamazsascannerdeğişkeninullkalır.finallybloğu her durumda çalışır; denetim olmasaydınullbir referans üzerindenclose()çağrılırdı.
Son nokta göründüğünden önemlidir. if (scanner != null) satırı kaldırılıp olmayan bir dosya açılmaya çalışılırsa program önce “Dosya bulunamadı” iletisini yazar, sonra finally'de çöker:
Dosya bulunamadı: yokDosya.txt (The system cannot find the file specified)
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.Scanner.close()" because "<local1>" is null
at Yok17.main(Yok17.java:12)Bu kalıp hem uzun hem de hataya açıktır. Java 7 ile gelen try-with-resources aynı işi kısaca ve güvenle yapar. Scanner AutoCloseable arayüzünü uygular; bu yüzden try'ın parantezinde açılabilir ve blok bitince kendiliğinden kapanır. finally ve null denetimi gerekmez:
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class Twr17 {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(new File("ornekDosya.txt"))) {
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
System.err.println("Dosya bulunamadı: " + e.getMessage());
}
Scanner s = new Scanner("3 4,5 elma\nikinci satır");
System.out.println(s.nextInt() + " | " + s.next() + " | " + s.next() + " | " + s.nextLine() + " | " + s.nextLine());
}
}Java
Giriş Çıkış
Akışlar
3 | 4,5 | elma | | ikinci satırProgramın sonundaki iki satır Scanner'ın bir String'i de okuyabildiğini gösterir. nextInt() 3'ü, iki next() çağrısı boşluğa kadar olan 4,5 ve elma kelimelerini alır. Ardından gelen ilk nextLine() satırın kalanını, yani boş dizgiyi döndürür; ikinci nextLine() asıl ikinci satırı verir. Bu, klavyede görülen nextInt–nextLine tuzağının aynısıdır.
8.4.2Kod izleme: try-with-resources ve kapanış sırası
try-with-resources, try anahtar kelimesinden sonraki parantezde açılan kaynakları blok bittiğinde kendiliğinden kapatan try biçimidir. Parantezde yalnız AutoCloseable arayüzünü uygulayan nesneler açılabilir. Kaynaklar açıldıkları sıranın tersiyle kapatılır; kapatma, blok normal bitse de istisnayla bitse de yapılır.
Kapanış sırasını görmek için açılırken ve kapanırken ileti yazan küçük bir kaynak sınıfı yazılır. Kaynak, AutoCloseable arayüzünü uygular; tek metodu close()'dur.
class Kaynak implements AutoCloseable {
String ad;
Kaynak(String ad) { this.ad = ad; System.out.println(ad + " açıldı"); }
public void close() { System.out.println(ad + " kapandı"); }
}
public class KapanisSirasi {
public static void main(String[] args) {
try (Kaynak a = new Kaynak("A"); Kaynak b = new Kaynak("B")) {
System.out.println("blok çalışıyor");
throw new IllegalStateException("hata!");
} catch (IllegalStateException e) {
System.out.println("yakalandı: " + e.getMessage());
} finally { System.out.println("finally"); }
}
}A açıldı
B açıldı
blok çalışıyor
B kapandı
A kapandı
yakalandı: hata!
finallyTablo 8.8 programın sekiz adımını verir.
| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 8: try (Kaynak a = new Kaynak("A"); Kaynak b = new Kaynak("B")) { | a=Kaynak("A") | A açıldı |
| 2 | 8: try (Kaynak a = new Kaynak("A"); Kaynak b = new Kaynak("B")) { | a=Kaynak("A"), b=Kaynak("B") | B açıldı |
| 3 | 9: System.out.println("blok çalışıyor"); | a=Kaynak("A"), b=Kaynak("B") | blok çalışıyor |
| 4 | 10: throw new IllegalStateException("hata!"); | a=Kaynak("A"), b=Kaynak("B") | — |
| 5 | 4: public void close() { System.out.println(ad + " kapandı"); } | this.ad="B" | B kapandı |
| 6 | 4: public void close() { System.out.println(ad + " kapandı"); } | this.ad="A" | A kapandı |
| 7 | 11: } catch (IllegalStateException e) {12: System.out.println("yakalandı: " + e.getMessage()); | e=IllegalStateException("hata!") | yakalandı: hata! |
| 8 | 13: } finally { System.out.println("finally"); } | — | finally |
İlk iki adımda parantezdeki kaynaklar soldan sağa sırayla oluşturulur: önce a, sonra b. Üçüncü adımda blok gövdesi çalışır; iki kaynak da açıktır. Dördüncü adımda gövde bir istisna fırlatır.
Beşinci ve altıncı adımlar bu yapının özüdür. İstisna fırlatıldığı hâlde catch'e hemen geçilmez. Önce kaynaklar kapatılır ve kapatma ters sırayla yapılır: en son açılan b ilk kapanır, sonra a. Ters sıranın bir gerekçesi vardır. Sonra açılan kaynak öncekine bağlı olabilir; örneğin bir yazıcı bir dosya akışını sarıyor olabilir. Önce dıştakinin kapatılması, içteki akış henüz açıkken dıştakinin tamponunu boşaltabilmesini sağlar.
Yedinci adımda catch istisnayı yakalar. Bu noktada a ve b kapsam dışıdır: parantezde tanımlanan değişkenler yalnız try bloğunun içinde görünür. catch içinde ya da bloktan sonra r.close() gibi bir kullanım derlenmez. Sekizinci adımda finally çalışır. Tam sıra şudur: aç A, aç B, gövde, kapat B, kapat A, catch, finally.
b kapanır. (c) İki kaynak da kapandıktan sonra catch istisnayı yakalar.8.4.3java.io kütüphanesi
java.io paketi giriş ve çıkış işlemleri için güçlü ve esnek bir araç takımı sunar. Dosya işlemlerinden ağ programlamaya kadar geniş bir alanda kullanılır. Paket üç eksende esneklik sağlar:
- Giriş ve çıkış: dosya, ağ ve bellek gibi kaynaklardan veri alır ve onlara veri gönderir.
- İkili ve metin: hem bayt tabanlı (ikili) hem karakter tabanlı (metin) işlemleri destekler.
- Sıralı ve rastgele: dosyadaki verilere hem sırayla hem de istenen konuma atlayarak erişir.
Paketteki öğeler üç gruba ayrılır (Tablo 8.9).
| Öğe tipi | Örnekler | Görevi |
|---|---|---|
| Sınıflar | File | dosya ve klasör (yol) işlemleri |
InputStream, OutputStream | temel bayt girişi ve çıkışı | |
Reader, Writer | metin (karakter) girişi ve çıkışı | |
| Arayüzler | Closeable, AutoCloseable | kaynağı serbest bırakma: close() |
Flushable | tamponu boşaltma: flush() | |
| İstisnalar | IOException | giriş çıkış hatalarının üst sınıfı |
FileNotFoundException, EOFException | dosya yok / beklenmedik dosya sonu |
8.4.4Bayt akışları ve karakter akışları
java.io'daki akışlar iki büyük aileye ayrılır. Ayrım, akışın taşıdığı birime göre yapılır.
Bayt akışları (byte streams) veriyi 8 bitlik baytlar olarak işler; üst sınıfları InputStream ve OutputStream'dir. Karakter akışları (character streams) veriyi 16 bitlik Unicode char değerleri olarak işler; üst sınıfları Reader ve Writer'dır. Karakter akışları baytları bir karakter kodlamasıyla karakterlere ve karakterleri baytlara çevirir.
Bayt akışları resim, ses, video ve .class gibi ikili verilerle çalışır. read() 0–255 arası bir bayt döndürür ve baytın ne anlama geldiğiyle ilgilenmez. Karakter akışları ise metin tabanlı veriler için tasarlanmıştır. Kodlamayı çözdükleri için farklı dilleri ve alfabeleri doğru işlerler. Tablo 8.10 iki aileyi karşılaştırır.
| Bayt akışı | Karakter akışı | |
|---|---|---|
| Okuma birimi | byte (8 bit) | char (16 bit) |
| Üst sınıflar | InputStream, OutputStream | Reader, Writer |
| Dosya sınıfları | FileInputStream, FileOutputStream | FileReader, FileWriter |
| Tamponlu | BufferedInputStream, BufferedOutputStream | BufferedReader, BufferedWriter |
| Kullanım | her tür dosya, kopyalama | yalnız metin dosyaları |
Metni bayt akışıyla okumak mümkündür ama zordur. Aşağıdaki deneme Işık kelimesini bayt bayt okuyup her baytı (char) ile karaktere çevirir:
IÅık (6 char)Dört harflik kelime altı “karakter” olarak çıkar. ş ve ı UTF-8'de ikişer bayttır ve bayt akışı bu baytları ayrı ayrı verir. Her baytın tek başına char'a çevrilmesi yanlış harfler üretir. Karakter akışı ise iki baytı birlikte çözer ve doğru harfi verir.
İki aile arasında köprü sınıflar vardır. InputStreamReader bir bayt akışını karakter akışına, OutputStreamWriter karakterleri bayt akışına çevirir. Kodlama bu köprülerde uygulanır. Bir sayıyı UTF-16 ile yazan metinYaz metodu bu köprüyü kullanmıştı.
8.4.5Sınıf hiyerarşileri
İki ailenin her biri bir soyut üst sınıftan türeyen sınıflardan oluşur. Şekil 8.9 bayt akışlarını, Şekil 8.10 karakter akışlarını gösterir.
System.out bir PrintStream'dir.Hiyerarşinin tepesindeki InputStream ve OutputStream soyut sınıflardır. Onlardan doğrudan nesne oluşturulmaz. Alt sınıflar iki tür işi üstlenir. Bir kısmı bir veri kaynağına bağlanır: FileInputStream dosyaya, ByteArrayInputStream bellekteki bir diziye, PipedInputStream bir boruya bağlanır. Öbür kısmı FilterInputStream ve FilterOutputStream'den türer. Bunlar kendi başlarına bir kaynağa bağlanmaz; başka bir akışı sarmalayıp ona yeni bir yetenek ekler. BufferedInputStream tampon, DataInputStream ilkel tip okuma yeteneği ekler.
FileReader bir InputStreamReader'dır; FileWriter da OutputStreamWriter'dan türer.Karakter akışlarında da aynı düzen vardır. Burada dikkat çeken ilişki FileReader'ın yeridir. FileReader aslında bir InputStreamReader'dır: içeride bir FileInputStream açar ve ondan gelen baytları seçilen kodlamayla karakterlere çevirir. FileWriter da aynı biçimde OutputStreamWriter'dan türer. Yani her karakter akışının altında, sonunda bir bayt akışı vardır. Disk yalnız bayt tanır.
8.4.6Sarmalayıcı akışlar
Java G/Ç kütüphanesindeki her sınıf tek bir işi iyi yapar. FileReader dosyadan karakter okur ama satır tanımaz. BufferedReader satır okur ama bir dosyaya bağlanamaz. İhtiyaç duyulan yetenekler, akışlar iç içe sarmalanarak birleştirilir.
Sarmalayıcı akış (decorator, wrapper stream), yapıcısında başka bir akışı alan ve ona tampon, satır okuma ya da ilkel tip okuma gibi yeni bir yetenek ekleyen akıştır. Sarmalayıcının metotları işin bir kısmını kendisi yapar, kalanını içteki akışa iletir. Bu düzenin adı Dekoratör (Decorator) tasarım desenidir.
BufferedReader okuyucu = new BufferedReader(new FileReader("ornekDosya.txt"));
PrintWriter yazici = new PrintWriter(new BufferedWriter(new FileWriter("dosya2.txt")));Birinci satırda iki katman vardır: FileReader dosyayı açar ve baytları karakterlere çevirir; BufferedReader onu sarar, tampon ve readLine() ekler. İkinci satırda üç katman vardır: FileWriter karakterleri baytlara çevirip dosyaya yazar, BufferedWriter tampon ekler, PrintWriter ise print, println ve printf gibi rahat yazma metotlarını ekler (Şekil 8.11).
Bunu bir bahçe hortumuna benzetmek mümkündür: hortumun ucuna önce bir filtre, sonra bir püskürtme başlığı takılır. Su, yani veri, hepsinden sırayla geçer. Her parça kendi işini yapar ve parçalar istenen sırayla birleştirilebilir. Bir yeteneği eklemek için yeni bir sınıf yazmak gerekmez; var olan sarmalayıcılar birleştirilir.
Sarmalamanın iki kuralı vardır. Birincisi, en dıştaki akışı kapatmak içtekilerin hepsini kapatır. yazici.close() önce kendi tamponunu, sonra BufferedWriter'ın tamponunu boşaltır, en sonunda FileWriter'ı ve dosyayı kapatır. İçteki akışlar ayrıca kapatılmaz. İkincisi, sarmalayıcı veriyi yorumlar ama doğrulamaz. Bir metin dosyası DataInputStream ile sarılıp readInt() çağrılırsa ilk dört bayt, yani J, a, v, a harflerinin kodları, 1 247 901 281 tam sayısı olarak okunur. Akış hata vermez; yanlış sarmalayıcı yanlış ama geçerli görünen bir sonuç üretir.
8.4.7Kod izleme: BufferedReader
Metin dosyasını satır satır okumanın en yaygın yolu BufferedReader'ın readLine() metodudur. Metot bir sonraki satırı satır sonu olmadan döndürür; dosya bittiğinde null döndürür.
import java.io.*;
public class SatirOku {
public static void main(String[] args) {
File dosya = new File("ornekDosya.txt");
try (BufferedReader okuyucu =
new BufferedReader(new FileReader(dosya))) {
String satir;
while ((satir = okuyucu.readLine()) != null) {
System.out.println(satir);
}
} catch (IOException e) {
System.err.println("Okuma hatası: " + e.getMessage());
e.printStackTrace();
}
}
}Döngü koşulundaki (satir = okuyucu.readLine()) != null ifadesi Java'nın G/Ç kodunda sık görülen bir kalıptır. Önce parantez içindeki atama yapılır: okunan satır satir değişkenine konur. Atama ifadesinin değeri, atanan değerin kendisidir. Bu değer null ile karşılaştırılır. Böylece okuma ve dosya sonu denetimi tek satırda yapılır. Dış parantezler şarttır. Onlar olmadan != önce çalışır ve bir boolean değer String değişkenine atanmaya çalışılır; kod derlenmez. Dosyada Java, Giriş Çıkış ve Akışlar satırları varken izleme Tablo 8.11'teki gibidir.
| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 5: File dosya = new File("ornekDosya.txt"); | dosya="ornekDosya.txt" | — |
| 2 | 6: try (BufferedReader okuyucu =7: new BufferedReader(new FileReader(dosya))) { | okuyucu=BufferedReader | — |
| 3 | 9: while ((satir = okuyucu.readLine()) != null) { | satir="Java" | — |
| 4 | 10: System.out.println(satir); | satir="Java" | Java |
| 5 | 9: while ((satir = okuyucu.readLine()) != null) { | satir="Giriş Çıkış" | — |
| 6 | 10: System.out.println(satir); | satir="Giriş Çıkış" | Giriş Çıkış |
| 7 | 9: while ((satir = okuyucu.readLine()) != null) { | satir="Akışlar" | — |
| 8 | 10: System.out.println(satir); | satir="Akışlar" | Akışlar |
| 9 | 9: while ((satir = okuyucu.readLine()) != null) { | satir=null | — |
| 10 | 12: } catch (IOException e) { | — | — |
Birinci adımda yalnız bir File nesnesi oluşur; dosya henüz açılmamıştır. İkinci adımda FileReader dosyayı açar ve BufferedReader onu sarmalar. Okuma konumu dosyanın başındadır. Üçüncü adımda ilk readLine() çağrısı "Java"'yı döndürür. Bu sırada BufferedReader dosyadan tek seferde büyük bir parça okuyup tamponuna koymuştur; küçük bir dosyada tamponun içine dosyanın tamamı girer. Sonraki satırlar diske gitmeden tampondan verilir.
readLine() izlemesinden iki an. (a) İlk satır okunur, satır sonu atılır. (b) Dosya sonunda readLine() null döndürür ve döngü biter.Dokuzuncu adımda dosya biter ve readLine() null döndürür. Döngü sona erer. Onuncu adımda try bloğu biter; okuyucu kendiliğinden kapanır. Hata olmadığı için catch çalışmaz. Okunan her satır bir kez yazıldığı için programın süresi dosyanın boyutuyla doğru orantılıdır. Bellekte ise aynı anda yalnız bir satır ve tampon durur; dosya ne kadar büyük olursa olsun bellek kullanımı sabittir. Bu özellik, gigabaytlarca büyüklükteki kayıt dosyalarının bile satır satır işlenebilmesini sağlar.
Döngü içinde readLine()'ı iki kez çağırmak: while (okuyucu.readLine() != null) System.out.println(okuyucu.readLine());. Koşuldaki çağrı bir satırı okuyup atar, gövdedeki çağrı sonrakini yazar. Satırların yarısı kaybolur ve tek sayıda satırlı dosyanın sonunda null yazdırılır. Her satır bir kez okunur ve bir değişkende tutulur.
8.4.8PrintWriter
PrintWriter metin dosyalarına yazmanın en rahat yoludur. System.out'tan tanıdık olan print, println ve printf metotlarının hepsi vardır. Sayı, karakter ve boolean gibi öteki tipleri kendisi metne çevirerek yazar.
public class PrintWriterOrnek {
public static void main(String[] args) {
PrintWriter yazici = null;
try {
yazici = new PrintWriter("yeniDosya.txt");
yazici.println("Merhaba, dünya!");
yazici.println("Bu bir örnek dosyadır.");
yazici.printf(Locale.ROOT, "Pi yaklaşık %.2f%n", Math.PI);
yazici.println(42);
} catch (IOException e) {
System.err.println("Dosya hatası: " + e.getMessage());
e.printStackTrace();
} finally {
if (yazici != null)
yazici.close();
}
}
}Program çalıştıktan sonra yeniDosya.txt'nin içeriği şudur:
Merhaba, dünya!
Bu bir örnek dosyadır.
Pi yaklaşık 3.14
42new PrintWriter("yeniDosya.txt") dosya yoksa onu oluşturur. Dosya varsa içini siler ve baştan yazar. Dosya oluşturulamazsa yapıcı FileNotFoundException fırlatır; program bunu IOException olarak yakalar. printf'teki %.2f sayıyı iki ondalık basamakla, %n ise işletim sisteminin satır sonuyla yazar. İlk argüman olan Locale.ROOT (java.util.Locale) biçimlendirmenin yerel ayardan bağımsız olmasını sağlar.
Locale.ROOT'un neden verildiği şöyle açıklanır. printf yerel ayar verilmeden çağrılırsa ondalık ayırıcıyı bilgisayarın yerel ayarına (locale) göre seçer. Türkçe bir sistemde 3,14, İngilizce bir sistemde 3.14 yazılır. Aynı program iki makinede farklı dosya üretir. Dosyayı daha sonra bir program okuyacaksa bu fark sorun çıkarır: Double.parseDouble("3,14") hata verir. Locale.ROOT ile çıktı her makinede 3.14'tür. println(3.5) ise zaten yerel ayardan bağımsızdır ve her zaman noktayla yazar. Aşağıdaki deneme Türkçe yerel ayarda çalıştırılmıştır:
import java.io.*;
import java.util.Locale;
public class Locale25 {
public static void main(String[] args) throws Exception {
try (PrintWriter p = new PrintWriter("loc.txt")) {
p.printf("Pi yaklaşık %.2f%n", Math.PI);
p.printf(Locale.ROOT, "Pi yaklaşık %.2f%n", Math.PI);
p.println(3.5);
p.println('A');
p.println(true);
}
Goster.main(new String[] {"loc.txt"});
}
}Pi yaklaşık 3,14
Pi yaklaşık 3.14
3.5
A
true
[54 bayt]PrintWriter'ın önemli bir özelliği daha vardır: yazma metotları istisna fırlatmaz. Yazma sırasında bir G/Ç hatası olursa PrintWriter bunu bir iç bayrağa kaydeder ve sessizce devam eder. Hata ancak checkError() çağrılarak görülür. Tamponlama kesimindeki izleme bu davranışı gösterir. Son olarak close() unutulmamalıdır: PrintWriter içinde bir tampon vardır ve kapatılmazsa son yazılanlar diske gitmeyebilir.
8.4.9FileWriter: yazma ve ekleme
FileWriter dosyalara karakter yazmak için kullanılan temel sınıftır; Writer sınıfının yazma işlevini bir dosyaya bağlar. Yalnız write(String), write(char[]) ve write(int c) metotları vardır. Metin biçimlendirme yeteneği sınırlıdır: satır sonu (\n) ayrıca yazılır, sayılar önce dizgiye çevrilir. Daha düşük düzeyli ve esnek bir yaklaşımdır; bu yüzden çoğunlukla BufferedWriter ya da PrintWriter ile sarmalanır.
write(int c) sık karıştırılır. Bu metot sayıyı değil, kodu o sayı olan karakteri yazar. w.write(65) dosyaya A yazar; 65 yazmak için w.write(String.valueOf(65)) gerekir. Aşağıdaki programın son çıktı satırındaki A65 bu iki çağrının sonucudur.
FileWriter'ın en kullanışlı yeteneği ekleme (append) kipidir. new FileWriter(ad, true) dosyayı silmeden sonuna ekler. false verilirse ya da ikinci parametre hiç verilmezse dosya sıfırlanır.
public class FileWriterOrnek {
public static void main(String[] args) {
try (FileWriter yazici = new FileWriter("yeniDosya.txt")) {
yazici.write("Merhaba, dünya!\n");
yazici.write("Bu bir FileWriter örneğidir.\n");
System.out.println("Dosya oluşturuldu ve veri yazıldı.");
} catch (IOException e) {
System.err.println("Dosya hatası: " + e.getMessage());
e.printStackTrace();
}
// true: dosyanın sonuna ekle (append)
try (FileWriter ekle = new FileWriter("yeniDosya.txt", true)) {
ekle.write("Sona eklenen satır.\n");
} catch (IOException e) {
System.err.println("Dosya hatası: " + e.getMessage());
}
}
}Dosya oluşturuldu ve veri yazıldı.İlk try bloğu dosyayı oluşturur ve iki satır yazar. İkinci blok dosyayı true ile açtığı için mevcut iki satırı korur ve üçüncü satırı sona ekler. Program bittiğinde yeniDosya.txt şudur:
Merhaba, dünya!
Bu bir FileWriter örneğidir.
Sona eklenen satır.Merhaba, dünya!
Bu bir FileWriter örneğidir.
Sona eklenen satır.
[69 bayt]
A65Dosya 69 bayttır: üç satırdaki 62 karakter ve üç satır sonu 65 eder. ü, ö, ğ ve ı UTF-8'de ikişer bayt olduğu için dört bayt daha eklenir. Ekleme kipi kayıt dosyalarında vazgeçilmezdir: program her çalıştığında yeni kayıtlar eskilerin sonuna eklenir. İkinci try bloğunda true unutulsaydı ilk iki satır silinir ve dosyada yalnız “Sona eklenen satır.” kalırdı. Tablo 8.12 üç yazıcı sınıfını karşılaştırır.
FileWriter | BufferedWriter | PrintWriter | |
|---|---|---|---|
| Yazma metotları | write | write, newLine() | print, println, printf |
| Sayı yazmak | önce String'e çevrilir | önce String'e çevrilir | doğrudan: println(42) |
| Tampon | küçük iç tampon | 8192 karakterlik tampon | sardığı akışa bağlı |
| Hata bildirimi | IOException | IOException | checkError() |
Seçim yapılacak işe göre değişir. Biçimli rapor ya da tablo yazılacaksa PrintWriter, çok sayıda kısa yazma yapılacaksa araya BufferedWriter konur. Hataların istisna olarak hemen bildirilmesi isteniyorsa BufferedWriter tercih edilir; PrintWriter'da hata ancak checkError() sorulunca görülür.
8.4.10Karakter kodlamasını belirtmek
Kodlama verilmeyen karakter akışları varsayılan kodlamayı kullanır. Java 18 ve sonrasında bu UTF-8'dir. Daha eski sürümlerde ise işletim sisteminin ayarıydı; Türkçe Windows'ta Windows-1254. Aynı dosya iki farklı makinede farklı okunabiliyordu. Taşınabilir kodda kodlama açıkça verilir. Java 11'den beri FileReader ve FileWriter'ın kodlama alan yapıcıları vardır. Java 10 ve öncesinde aynı iş InputStreamReader ve OutputStreamWriter köprüleriyle yapılırdı.
public class KodlamaOku {
public static void main(String[] args) throws IOException {
File dosya = new File("turkce.txt");
try (BufferedWriter yazici = new BufferedWriter(
new FileWriter(dosya, StandardCharsets.UTF_8))) { // Java 11+
yazici.write("Işık, şimşek, çiğ, öğrenci, Ünye");
}
try (BufferedReader dogru = new BufferedReader(
new FileReader(dosya, StandardCharsets.UTF_8))) {
System.out.println(dogru.readLine());
}
try (BufferedReader yanlis = new BufferedReader(
new FileReader(dosya, StandardCharsets.ISO_8859_1))) {
System.out.println(yanlis.readLine());
}
}
}Işık, şimşek, çiğ, öğrenci, Ünye
IÅık, ÅimÅek, çiÄ, öÄrenci, ÃnyeDosya UTF-8 ile yazılıp UTF-8 ile okunduğunda metin doğru gelir. Aynı baytlar ISO-8859-1 ile okunduğunda her Türkçe harf iki ayrı karaktere dönüşür. Örneğin ş'nin UTF-8 baytları C5 9F'dur. Latin-1'de C5 Å harfidir. 9F ise yazdırılamayan bir kontrol karakteridir ve ekranda görünmez. Bu yüzden şimşek, ÅimÅek olarak görünür. Bu tür bozulmaya Türkçede “karakter bozulması”, programcı jargonunda mojibake denir. Kodlama hatası istisna üretmez; program çalışır, ama veri bozuk gelir.
Karşılığı olmayan karakter yazılırken de bilgi kaybolur. ASCII ile yazan bir akışa ş verilirse bu harfin ASCII'de karşılığı olmadığı için soru işareti (?, 3F) yazılır. Aynı harf UTF-8'de C5 9F olarak saklanırdı. Bu kayıp geri alınamaz: dosyada artık yalnız bir soru işareti vardır.
Dosyayı bir kodlamayla yazıp başka bir kodlamayla okumak. Eski bir Windows programının Windows-1254 ile ürettiği dosya Java 18 ve sonrasında kodlama verilmeden okunursa Türkçe harfler � işaretine dönüşür. Dosyanın kodlaması biliniyorsa okurken açıkça verilir: new FileReader(dosya, Charset.forName("windows-1254")).
8.5Tamponlama
Akışların temel metotları küçük parçalarla çalışır. read() bir bayt ya da bir bayt dizisi okur, write() bir bayt ya da bir bayt dizisi yazar. Her bayt için diske ayrı ayrı gidilirse uygulama ciddi biçimde yavaşlar. Bu kesim sorunun nedenini ve çözümünü anlatır.
8.5.1Neden tampon?
Disk, bellekten binlerce kat yavaştır. Üstelik her disk erişimi yalnız aktarım süresinden ibaret değildir. Program işletim sistemine bir sistem çağrısı yapar, işletim sistemi aygıt sürücüsünü çalıştırır, sonuç programa geri döner. Bu sabit maliyet, bir bayt okunsa da sekiz bin bayt okunsa da aşağı yukarı aynıdır. Dolayısıyla bin baytı bin ayrı çağrıyla okumak, bir çağrıyla okumaktan çok daha pahalıdır.
Tampon (buffer), verinin bellekte geçici olarak biriktirildiği alandır. Tamponlama (buffering), okuma ve yazmayı bu alan üzerinden toplu yaparak fiziksel disk erişimlerinin sayısını azaltma tekniğidir. Okurken tampon bir seferde büyük bir parçayla doldurulur ve program küçük okumalarını tampondan karşılar. Yazarken küçük yazmalar tamponda birikir ve tampon dolunca tek seferde diske gönderilir.
Gündelik bir benzetme farkı açıklar. Marketten on ürün almak için her ürüne ayrı bir yolculuk yapmak yerine hepsi bir sepete konur ve tek seferde taşınır. Yolculuğun kendisi, yani sabit maliyet, bir kez ödenir. Şekil 8.13 tamponsuz ve tamponlu yazmayı karşılaştırır.
write() doğrudan diske gider. (b) Tamponlu akışta write() baytları bellekteki tampona koyar. (c) Tampon dolunca ya da flush() veya close() çağrılınca tamponun tamamı tek seferde diske yazılır.8.5.2Tamponlu akış sınıfları ve flush
Java'da tamponlama, sarmalayıcı akışlarla eklenir. Dört tamponlu sınıf vardır (Tablo 8.13).
| Sınıf | Sardığı akış | Ne yapar? |
|---|---|---|
BufferedInputStream | InputStream | Okumayı hızlandırır: tampon boşalınca diskten tek seferde 8192 bayt okur. |
BufferedReader | Reader | Aynı işi karakterler için yapar; ayrıca readLine() ekler. |
BufferedOutputStream | OutputStream | Yazmayı hızlandırır: write() verisini tamponda biriktirir. |
BufferedWriter | Writer | Aynı işi karakterler için yapar; ayrıca newLine() ekler. |
Yazma tamponunun bir yan etkisi vardır: write() döndüğünde veri henüz diskte olmayabilir. Verinin diske gittiği üç an vardır. Tampon dolduğunda kendiliğinden boşaltılır. flush() çağrıldığında tampon hemen diske boşaltılır. close() çağrıldığında önce flush() yapılır, sonra akış kapatılır. Aşağıdaki program bu davranışı dosya boyutunu ölçerek gösterir:
public class FlushOrnek {
public static void main(String[] args) throws IOException {
File log = new File("log.txt");
BufferedWriter bw = new BufferedWriter(new FileWriter(log));
bw.write("Kayıt 1");
System.out.println("önce: " + log.length());
bw.flush();
System.out.println("sonra: " + log.length());
bw.close();
}
}önce: 0
sonra: 8bw.write("Kayıt 1") döndükten sonra dosya hâlâ 0 bayttır; yedi karakter BufferedWriter'ın tamponunda bekler. flush()'tan sonra dosya 8 bayt olur. Yedi karakter sekiz bayt eder, çünkü ı UTF-8'de iki bayttır.
Tamponlu bir yazıcıyı kapatmamak. bw.write("kayit"); yazılıp akış kapatılmazsa veri tamponda kalır. Program bittiğinde de tampon boşaltılmaz: JVM kapanırken açık akışların tamponlarını diske yazmaz. Dosya boş görünür. close() ya da flush() çağrılır; en güvenli yol try-with-resources'tır.
Bu hata gerçekten ölçülebilir. Aşağıdaki iki program arka arkaya çalıştırılmıştır. Birincisi yazıp kapatmadan biter, ikincisi dosyanın boyutuna bakar:
not.txt boyutu: 0program bitti, not.txt boyutu: 0Program sona erdikten sonra bile dosya 0 bayttır. Beş karakter hiçbir zaman diske ulaşmamıştır.
8.5.3Kod izleme: tampon ve flush
Aşağıdaki kod parçası bir PrintWriter'ın altındaki tamponu adım adım izler. Her yazmadan sonra dosyanın boyutu ölçülür. Son iki satır kapatılmış bir akışa yazmanın ne olduğunu gösterir.
File f = new File("log.txt");
PrintWriter pw = new PrintWriter(new FileWriter(f));
System.out.println("0: " + f.length());
pw.print("kayit1;");
System.out.println("1: " + f.length());
pw.print("kayit2;");
System.out.println("2: " + f.length());
pw.flush();
System.out.println("3: " + f.length());
pw.print("kayit3;");
pw.close();
System.out.println("4: " + f.length());
pw.print("kayit4;"); // kapalı: sessizce yok sayılır
System.out.println("hata? " + pw.checkError());0: 0
1: 0
2: 0
3: 14
4: 21
hata? true| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 1: File f = new File("log.txt");2: PrintWriter pw = new PrintWriter(new FileWriter(f)); | f=log.txt, pw=PrintWriter | — |
| 2 | 3: System.out.println("0: " + f.length()); | f.length()=0 | 0: 0 |
| 3 | 4: pw.print("kayit1;"); | tampon="kayit1;" | — |
| 4 | 5: System.out.println("1: " + f.length()); | f.length()=0 | 1: 0 |
| 5 | 6: pw.print("kayit2;");7: System.out.println("2: " + f.length()); | tampon="kayit1;kayit2;" | 2: 0 |
| 6 | 8: pw.flush(); | tampon="" | — |
| 7 | 9: System.out.println("3: " + f.length()); | f.length()=14 | 3: 14 |
| 8 | 10: pw.print("kayit3;"); | tampon="kayit3;" | — |
| 9 | 11: pw.close(); | pw=kapalı | — |
| 10 | 12: System.out.println("4: " + f.length()); | f.length()=21 | 4: 21 |
| 11 | 13: pw.print("kayit4;"); // kapalı: sessizce yok sayılır14: System.out.println("hata? " + pw.checkError()); | pw.checkError()=true | hata? true |
Tablo 8.14'in ilk adımında FileWriter dosyayı oluşturur; dosya boştur. Bu zincirde tampon FileWriter'ın içindedir. FileWriter karakterleri baytlara çeviren bir kodlayıcı kullanır ve bu kodlayıcının bir bayt tamponu vardır. PrintWriter kendi başına tampon eklemez; yazdıklarını bu tampona iletir.
Üçüncü ve beşinci adımlarda print çağrıları yedişer karakteri tampona koyar ama diske dokunmaz. Dosya 0 bayt kalır. Altıncı adımda flush() tamponu diske boşaltır ve 14 bayt yazılır. Sekizinci adımda üçüncü kayıt tampona girer. Dokuzuncu adımdaki close() önce flush() yapar, sonra dosyayı kapatır. Onuncu adımda dosya 21 bayttır. close() unutulsaydı üçüncü kayıt kaybolabilirdi.
flush()'tan sonra dosya 14 bayt. (c) close()'tan sonra dosya 21 bayt.On birinci adım PrintWriter'ın istisna fırlatmama özelliğini gösterir. Kapatılmış bir akışa print yapmak normalde bir IOException gerektirir. PrintWriter ise hata vermez; yazma sessizce başarısız olur ve iç bayrak kurulur. checkError() true döndürür. Bu davranış System.out için uygundur, çünkü her println'i try içine yazmak zahmetli olurdu. Önemli bir dosyaya yazılırken ise sonda checkError() denetlenmeli ya da istisna fırlatan bir yazıcı seçilmelidir.
8.5.4Tamponlu okuma
Okuma tarafında tamponlama iki biçimde kullanılır: bayt bayt ve satır satır. dosya.txt iki satır içersin: Java I/O ve Tampon.
try {
FileInputStream fis = new FileInputStream("dosya.txt");
BufferedInputStream bis = new BufferedInputStream(fis);
int veri;
while ((veri = bis.read()) != -1) {
System.out.print((char) veri);
}
bis.close(); // içteki fis'i de kapatır
} catch (IOException e) {
System.err.println("Dosya hatası: " + e.getMessage());
}Çıktı: bayt bayt okuma
Java I/O
TamponBu döngü bis.read()'i her bayt için bir kez çağırır. Ama diskten her seferinde bir bayt okunmaz. İlk çağrıda tampon boştur; BufferedInputStream içteki FileInputStream'den tek seferde 8192 bayta kadar okur. Sonraki çağrılar tampondan karşılanır ve diske gidilmez. Satır sonu da bir bayttır ve (char) ile yazdırıldığında satırı bitirir. Bu yüzden çıktı iki satırdır. Bu yöntem yalnız ASCII metinde doğru sonuç verir; Türkçe harfler ikişer bayt olduğu için bozulur.
try {
FileReader fr = new FileReader("dosya.txt");
BufferedReader br = new BufferedReader(fr);
String satir;
while ((satir = br.readLine()) != null) {
System.out.println(satir);
}
br.close(); // içteki fr'yi de kapatır
} catch (IOException e) {
System.err.println("Dosya hatası: " + e.getMessage());
}İkinci parça aynı dosyayı BufferedReader ile satır satır okur ve aynı iki satırı yazar. Karakter akışı kodlamayı çözdüğü için Türkçe harfler de doğru gelir. Her iki parçada da close() yalnız en dıştaki akışa uygulanır: bis.close() içteki fis'i, br.close() içteki fr'yi kapatır.
Bu iki parça bir zayıflık taşır. Okuma sırasında bir istisna olursa close() satırına ulaşılmaz ve dosya açık kalır. Bölümün geri kalanındaki programların çoğu bu yüzden try-with-resources kullanır.
8.5.5Tamponlama ne kadar hızlandırır?
Aşağıdaki program 2 MB'lık bir dosya oluşturur ve onu iki kez bayt bayt okur: önce tamponsuz, sonra tamponlu. Süre System.nanoTime() ile milisaniye cinsinden ölçülür.
public class TamponOlcum {
static long oku(InputStream in) throws IOException {
long bas = System.nanoTime();
while (in.read() != -1) { } // her seferinde 1 byte
in.close();
return (System.nanoTime() - bas) / 1_000_000;
}
public static void main(String[] args) throws IOException {
File f = new File("buyuk.dat");
try (FileOutputStream fos = new FileOutputStream(f)) {
fos.write(new byte[2 * 1024 * 1024]); // 2 MB
}
long t1 = oku(new FileInputStream(f));
long t2 = oku(new BufferedInputStream(new FileInputStream(f)));
System.out.println("Tamponsuz: " + t1 + " ms");
System.out.println("Tamponlu : " + t2 + " ms");
f.delete();
}
}Sunumdaki örnek çalıştırmanın çıktısı şudur:
Tamponsuz: 3151 ms
Tamponlu : 94 msBu not hazırlanırken aynı program üç kez daha çalıştırıldı. Süreler 3194/58, 3241/62 ve 3250/63 milisaniye oldu. Süreler makineye, diske ve işletim sisteminin önbelleğine göre değişir; oran ise benzer kalır. Bu makinede tamponlu okuma elli kattan fazla hızlıdır.
Farkın nedeni çağrı sayısındadır. Dosya 2 MB, yani 2 · 1024 · 1024 = 2 097 152 bayttır. Tamponsuz akışta her read() bir sistem çağrısıdır: 2 097 152 çağrı, üstüne dosya sonunu bildiren bir çağrı daha. Tamponlu akışta sistem çağrısı 8192 baytta birdir: 2 097 152 / 8192 = 256 veri taşıyan çağrı ve dosya sonu için bir çağrı. Programın kendi read() çağrılarının sayısı iki durumda da aynıdır; ama tamponlu durumda bunlar bellekteki bir diziden okunan sıradan metot çağrılarıdır. Sistem çağrısı sayısı yaklaşık 8000 kat azalır. Süre ise yalnız elli kat kadar azalır, çünkü her bayt için bir metot çağrısı hâlâ yapılır. Blok blok okuma bu maliyeti de ortadan kaldırır.
8.6Diğer bayt akışları
InputStream ve OutputStream ailesinde dosya akışlarından başka sınıflar da vardır. Bunlar veri kaynağı olarak bellekteki bir diziyi, başka bir iş parçacığını ya da birden çok akışı kullanır. Hepsi aynı read() ve write() metotlarını taşıdığı için bir dosya akışının beklendiği her yerde kullanılabilirler.
8.6.1FileInputStream ve parça parça okuma
FileInputStream InputStream'den türer ve dosyadan doğrudan bayt okur. Düşük düzeyli bir sınıftır; resim ve ses gibi metin dışı veriler için uygundur. İki okuma metodu vardır:
read()tek bir bayt okur ve 0–255 arası birintdöndürür. Dosya sonunda −1 döndürür.read(byte[] b)en fazlab.lengthbayt okuyup diziye koyar ve okuduğu bayt sayısını döndürür. Dosya sonunda −1 döndürür. Dönen sayı dizinin boyundan küçük olabilir.
public class ParcaOku {
public static void main(String[] args) throws IOException {
try (FileInputStream fis = new FileInputStream("dosya.txt")) {
byte[] dizi = new byte[4];
int adet;
while ((adet = fis.read(dizi)) != -1) {
System.out.println(adet + ": " + new String(dizi, 0, adet));
}
}
}
}dosya.txt'de Merhaba Java yazsın: 12 bayt. Dizi 4 baytlık olduğu için dosya üç parçada okunur:
4: Merh
4: aba
4: Javaİkinci satırdaki aba 'nın sonunda bir boşluk vardır. new String(dizi, 0, adet) dizinin yalnız okunan kısmını, yani 0'dan başlayarak adet baytı metne çevirir. Bu örnekte dosya tam 4'ün katı olduğu için her parça doludur. Dosya 13 bayt olsaydı dördüncü okuma 1 döndürürdü ve dizinin son üç hücresinde önceki parçadan kalan baytlar dururdu. new String(dizi) yazılsaydı bu eski baytlar da metne katılırdı. Çalışma sorularındaki JAVA-IO örneği bu hatayı adım adım gösterir.
Metin için FileInputStream kullanmak zordur. Türkçe harfler gibi çok baytlı karakterler parça sınırında bölünebilir. Gündüz kelimesi 2 baytlık parçalarla okunup her parça ayrı ayrı UTF-8 olarak çözülürse ü'nün iki baytı iki ayrı parçaya düşer:
[G�][�n][d�][�z]Yarım kalan her bayt geçersiz bir UTF-8 dizisidir ve � işaretine dönüşür. Metin dosyaları için FileReader gibi karakter tabanlı sınıflar daha uygundur; onlar parça sınırındaki yarım karakteri bir sonraki okumaya saklar.
8.6.2Bellekteki akışlar
ByteArrayInputStream de InputStream'den türer, ama bir dosyadan değil, bellekteki bir bayt dizisinden okur. Fiziksel bir kaynağa erişmediği için hızlıdır. Yalnız okunabilir; belleğe yazmak için eşi ByteArrayOutputStream kullanılır. ByteArrayOutputStream yazılan baytları büyüyen bir dizide toplar; toByteArray() bu diziyi, toString() onun metin karşılığını verir.
public class ByteDiziOku {
public static void main(String[] args) throws IOException {
byte[] dizi = {74, 97, 118, 97}; // "Java"
ByteArrayInputStream bais = new ByteArrayInputStream(dizi);
int b;
while ((b = bais.read()) != -1) {
System.out.print(b + "=" + (char) b + " ");
}
System.out.println();
// Belleğe yazma: ByteArrayOutputStream
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(dizi);
baos.write('!');
byte[] sonuc = baos.toByteArray();
System.out.println(sonuc.length + " byte: " + baos);
}
}74=J 97=a 118=v 97=a
5 byte: Java!İlk döngü dizinin dört baytını okur. Her baytı sayı ve karakter olarak yazar: 74 = J, 97 = a, 118 = v. İkinci kısımda dört bayt ve '!' karakteri belleğe yazılır. Sonuç 5 baytlık bir dizidir.
Bu sınıflar iki durumda işe yarar. Birincisi, akış bekleyen bir metodu dosyasız denemektir: bir sınama, bir ByteArrayInputStream ile istenen girdiyi verip metodun çıktısını bir ByteArrayOutputStream'de toplayabilir. İkincisi, verinin tamamı bellekte hazırlanıp sonra tek seferde gönderilecekse, örneğin ağ üzerinden. Bütün veri bellekte durduğu için çok büyük verilerde bellek tüketimi sorun olur.
8.6.3Kullanımdan kaldırılmış bir sınıf
java.io.StringBufferInputStream bir dizgiden bayt okumak için yazılmış eski bir sınıftır. Java 1.1'den beri kullanımdan kaldırılmıştır (deprecated). Sınıf hâlâ derlenir, ama derleyici uyarı verir ve kullanılması önerilmez. Sorunu, karakterleri bayta doğru çevirmemesidir: her karakterin yalnız alt 8 bitini alır.
public class EskiAkis {
@SuppressWarnings("deprecation")
public static void main(String[] args) throws IOException {
InputStream eski = new StringBufferInputStream("ş€");
int b;
while ((b = eski.read()) != -1) {
System.out.print(b + " ");
}
System.out.println();
byte[] dogru = "ş€".getBytes("UTF-8");
InputStream yeni = new ByteArrayInputStream(dogru);
while ((b = yeni.read()) != -1) {
System.out.print(b + " ");
}
System.out.println();
}
}95 172
197 159 226 130 172 ş'nin Unicode kodu 351'dir, onaltılık 015F. Alt 8 biti 5F = 95'tir; bu da _ karakteridir. €'nun kodu 20AC'dir ve alt 8 biti AC = 172'dir. İki karakter iki anlamsız bayta dönüşür ve asıl bilgi kaybolur. Doğrusu UTF-8 baytlarıdır: ş için 197 159, € için 226 130 172. Bu baytlar "ş€".getBytes("UTF-8") ile elde edilip ByteArrayInputStream ile okunur. Dizgiyi karakter olarak okumak gerekiyorsa StringReader kullanılır. @SuppressWarnings("deprecation") etiketi, eski sınıfı bilerek kullanan bu örnekte derleyici uyarısını susturur.
8.6.4Borular
PipedInputStream ve PipedOutputStream, iki iş parçacığı (thread) arasında güvenli veri iletişimi sağlar. İş parçacığı, aynı programın içinde bağımsız ilerleyen bir yürütme akışıdır. Bir iş parçacığı PipedOutputStream ile veri yazar, öbürü PipedInputStream ile bu veriyi okur. İki uç new PipedOutputStream(pis) ile birbirine bağlanır (Şekil 8.15).
PipedOutputStream'e yazar; baytlar 1024 baytlık dairesel bir tamponda bekler; okuyucu iş parçacığı onları PipedInputStream'den okur.Borunun dört özelliği vardır:
- Okuma ve yazma eş zamanlıdır (synchronized): boru boşsa okuyucu bekler, doluysa yazıcı bekler.
- Veri tek yönde akar. Çift yönlü iletişim için iki boru gerekir.
- Borunun tamponu varsayılan olarak 1024 bayttır.
- İki ucu aynı iş parçacığında kullanmak kilitlenmeye (deadlock) yol açabilir. Tampon dolunca yazıcı okuyucuyu bekler; okuyucu da aynı iş parçacığı olduğu için hiçbir zaman çalışmaz.
Aşağıdaki program bir okuyucu ve bir yazıcı iş parçacığı oluşturur. İş parçacıklarının gövdeleri lambda ifadeleriyle yazılmıştır.
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream(pis);// Okuyucu iş parçacığı
Thread okuyucu = new Thread(() -> {
try {
int b;
while ((b = pis.read()) != -1)
System.out.print((char) b);
System.out.println();
pis.close();
} catch (IOException e) {
e.printStackTrace();
}
});// Yazıcı iş parçacığı
Thread yazici = new Thread(() -> {
try {
pos.write("Merhaba, boru!".getBytes());
pos.close();
} catch (IOException e) {
e.printStackTrace();
}
});
okuyucu.start();
yazici.start();
yazici.join();
okuyucu.join();Merhaba, boru!start() iş parçacığını başlatır; join() onun bitmesini bekler. İki iş parçacığı aynı anda çalışır ve hangisinin önce ilerleyeceği her çalıştırmada değişebilir. Okuyucu önce başlarsa pis.read()'te bekler, çünkü boru boştur. Yazıcı 14 baytı boruya yazar ve pos.close() der. Okuyucunun bekleyen read() çağrısı baytları sırayla alır. Boru boşalıp yazıcı ucu kapalı olduğunda read() −1 döndürür ve döngü biter. Çıktı her çalıştırmada aynıdır, çünkü boru baytların sırasını korur. Program bu not için iki kez çalıştırıldı ve iki çalıştırmada da aynı satırı yazdı.
Kilitlenme uyarısını görmek için borunun tamponu 4 bayta küçültülür ve yazıcı 6 bayt yazmaya çalışır. Okuyan kimse yoktur:
yazıcı 1,5 s sonra hâlâ çalışıyor mu? true, borudaki bayt: 4Yazıcı ilk 4 baytı yazar, sonra boru dolduğu için bekler. 1,5 saniye sonra da beklemeyi sürdürür. Okuyucu aynı iş parçacığı olsaydı program sonsuza dek dururdu. Bu deneme iş parçacığını arka plan iş parçacığı yaparak programın kapanabilmesini sağlar.
8.6.5SequenceInputStream
SequenceInputStream, birden fazla InputStream'i uç uca birleştirerek tek bir ardışık akış oluşturur (Şekil 8.16). Birinci akış bitip −1 döndürdüğünde okuma kendiliğinden ikinci akışa geçer. Böylece farklı kaynaklardan gelen veri tek bir akış gibi işlenir.
SequenceInputStream iki dosya akışını uç uca ekler. Okuyan program tek, ardışık bir akış görür.try {
FileInputStream is1 = new FileInputStream("dosya1.txt");
FileInputStream is2 = new FileInputStream("dosya2.txt");
SequenceInputStream sis = new SequenceInputStream(is1, is2);
int okunanByte;
while ((okunanByte = sis.read()) != -1)
System.out.print((char) okunanByte);
sis.close(); // is1 ve is2'yi de kapatır
} catch (IOException e) {
System.err.println("Okuma hatası: " + e.getMessage());
}Birinci dosya.
Ikinci dosya.dosya1.txt Birinci dosya. satırını, dosya2.txt Ikinci dosya. satırını içerir. Döngü yalnız sis.read() çağırır ve dosyalar arasındaki geçişi fark etmez. sis.close() içteki iki akışı da kapatır. Okuma sırasında akışlardan birinde hata olursa IOException fırlatılır ve öbür akışa geçilmez. İkinci dosyanın metninde İ yerine I yazılması bilinçlidir: bayt bayt okunup (char) ile yazdırılan metinde Türkçe harfler bozulurdu.
8.7Dosya kopyalama
Dosya kopyalamak, akışları öğrenmek için iyi bir alıştırmadır: bir girdi akışından okunan her şey bir çıktı akışına yazılır. Aynı iş farklı birimlerle yapılabilir: bayt bayt, blok blok ya da satır satır. Bu kesim yolları karşılaştırır ve her birinin tuzaklarını gösterir.
8.7.1Bayt bayt kopyalama
En basit kopyalama, kaynak dosyadan okunan her baytı hedef dosyaya yazmaktır. Bayt akışları baytların anlamıyla ilgilenmediği için bu yöntem her tür dosyayı, yani metni, resmi ya da .exe'yi bozmadan kopyalar. Okuma ve yazmayı hızlandırmak için iki akış tamponlu akışlarla sarılır (Şekil 8.17).
dosya.txt'den FileInputStream ve BufferedInputStream üzerinden veri değişkenine, oradan BufferedOutputStream ve FileOutputStream üzerinden dosya2.txt'ye gider.import java.io.*;
public class BaytKopya {
public static void main(String[] args) throws IOException {
File dosya = new File("dosya.txt");
File dosya2 = new File("dosya2.txt");
FileInputStream fis = new FileInputStream(dosya);
FileOutputStream fos = new FileOutputStream(dosya2);
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
int veri;
// Bir byte oku. Dosyanın sonunda -1 dönecektir.
while ((veri = bis.read()) != -1)
bos.write(veri); // Okunan byte'ı çıkışa yaz
bos.close();
bis.close();
System.out.println(dosya2.length() + " byte kopyalandı");
}
}Kopyalama döngüsü tek satırdır: while ((veri = bis.read()) != -1) bos.write(veri);. bis.read() bir bayt okur ve dosya sonunda −1 döndürür. bos.write(veri) okunan baytı çıkış akışına yazar. Okunan değer bir int değişkende tutulmalıdır. Bunun nedeni −1 ile 255 değerli baytı ayırt etmektir. read() 0–255 aralığındaki her değeri geçerli bir bayt olarak döndürebilir; −1 bu aralığın dışında kaldığı için dosya sonunu belirsizlik olmadan bildirir.
read()'in sonucunu byte tipinde tutmak: byte b; while ((b = (byte) in.read()) != -1). Java'da byte işaretlidir ve −128 ile 127 arasında değer alır. Değeri 255 olan bir bayt, yani FF, (byte) dönüşümüyle −1'e dönüşür. Döngü bu baytı dosya sonu sanır ve durur; dosyanın geri kalanı kopyalanmaz. Doğru tip int'tir.
Hatanın etkisi küçük bir denemeyle görülür. Üç baytlık {10, (byte) 255, 20} dizisi bu döngüyle okunursa yalnız ilk bayt işlenir:
işlenen: 1Metin dosyalarında FF baytı nadiren görüldüğü için bu hata metin dosyalarıyla yapılan denemelerde fark edilmez. Resim ya da sıkıştırılmış dosyalarda ise FF sık görülür ve kopya yarıda kesilir.
8.7.2Kod izleme: bayt bayt kopyalama
dosya.txt dört bayt içersin: Java. Tablo 8.15 programın her adımında iki tamponun ve hedef dosyanın durumunu verir.
| Adım | Satır | veri | bis tamponu | bos tamponu | dosya2.txt | Ne oldu? |
|---|---|---|---|---|---|---|
| 1 | 5–6 | — | — | — | yok | İki File nesnesi yalnız yolları tutar. |
| 2 | 7–8 | — | — | — | 0 bayt | FileOutputStream hedef dosyayı oluşturur; varsa içini siler. |
| 3 | 9–10 | ? | boş | boş | 0 bayt | Akışlar tamponlu akışlarla sarılır. |
| 4 | 13 | 74 | Java | boş | 0 bayt | Tampon boş; diskten tek okumayla 4 bayt gelir. İlk bayt 'J' döner. |
| 5 | 14 | 74 | Java | J | 0 bayt | write baytı çıkış tamponuna koyar. |
| 6–7 | 13–14 | 97 | Java | Ja | 0 bayt | Bayt tampondan gelir; diske gidilmez. |
| 8–9 | 13–14 | 118 | Java | Jav | 0 bayt | Bayt tampondan gelir. |
| 10–11 | 13–14 | 97 | Java | Java | 0 bayt | Çıkış tamponunda 4 bayt var. |
| 12 | 13 | −1 | boş | Java | 0 bayt | Tampon tükendi; ikinci disk okuması dosya sonunu bildirir. |
| 13 | 15 | −1 | boş | boş | 4 bayt | bos.close() önce flush() yapar: 4 bayt tek yazmayla diske gider. |
| 14 | 16 | −1 | — | — | 4 bayt | Giriş akışları kapatılır. |
| 15 | 17 | −1 | — | — | 4 bayt | Çıktı: 4 byte kopyalandı |
read() dört baytın tamamını giriş tamponuna alır. (b) Dört bayt çıkış tamponunda bekler; hedef dosya hâlâ 0 bayt. (c) close()'tan sonra dört bayt diskte.İzlemenin en önemli sonucu, programın dört read() ve dört write() çağrısı yapmasına karşın diske çok daha az gidilmesidir. Bunu doğrulamak için FileInputStream ve FileOutputStream'in alt sınıfları yazıldı. Bu alt sınıflar diske giden her çağrıyı sayar ve yazdırır:
disk read(dizi, 0, 8192) -> 4
disk read(dizi, 0, 8192) -> -1
disk write(dizi, 0, 4)
tamponlu: 2 okuma, 1 yazma
disk read() -> 74
disk read() -> 97
disk read() -> 118
disk read() -> 97
disk read() -> -1
tamponsuz: 5 okuma, 4 yazmaTamponlu kopyada diskten iki okuma yapılır. Birincisi 8192 bayt ister ve dört bayt alır. İkincisi tampon tükendikten sonra yapılır ve dosya sonunu (−1) bildirir. Diske yalnız bir yazma yapılır. Tamponsuz kopyada ise her bayt için bir okuma ve bir yazma, dosya sonu için de bir okuma daha gerekir: beş okuma, dört yazma. Dört baytlık bir dosyada fark küçüktür. Bir megabaytlık dosyada ise tamponsuz kopya bir milyondan fazla okuma ve yazma çağrısı yapar. Tamponlu kopya 129 okuma ve 128 yazmayla, yani yaklaşık 260 çağrıyla işi bitirir.
8.7.3Blok blok kopyalama
Tamponlu akışlar disk erişimini azaltır, ama programın kendisi hâlâ her bayt için bir read() ve bir write() çağırır. Daha hızlı yol, veriyi programın kendi dizisine bloklar hâlinde okumaktır. read(byte[] parca) diziyi doldurmaya çalışır ve okuduğu bayt sayısını döndürür. write(parca, 0, boyut) dizinin yalnız ilk boyut baytını yazar.
public class ParcaKopya {
public static void main(String[] args) throws IOException {
// Veri, 16K'lık parçalar hâlinde okunacak
byte[] parca = new byte[1024 * 16];
File dosya = new File("dosya.txt");
File dosya2 = new File("dosya2.txt");
FileInputStream fis = new FileInputStream(dosya);
FileOutputStream fos = new FileOutputStream(dosya2);
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
int boyut;
// 16K'ya kadar oku. Dosyanın sonunda -1 dönecektir.
while ((boyut = bis.read(parca)) > -1)
bos.write(parca, 0, boyut); // Okunan parçayı çıkışa yaz
bos.close();
bis.close();
System.out.println(dosya2.length() + " byte kopyalandı");
}
}40 000 baytlık bir dosya için çıktı şudur:
40000 byte kopyalandınew byte[1024 * 16] 16 KB'lık bir dizi oluşturur. Veri bu boyuttaki parçalar hâlinde işlenir: dosya ne kadar büyük olursa olsun programın bellek kullanımı sabit kalır. 40 000 baytlık dosya üç okumayla gelir: 16 384, 16 384 ve 7 232 bayt. Son parça dizinin tamamını doldurmaz. Bu not için 40 000 rastgele baytlık bir dosyayla yapılan deneme parça boylarını ve kopyanın kaynakla aynı olduğunu doğrular:
okunan parçalar: 16384 16384 7232
aynı mı: trueDöngü koşulu > -1 biçimindedir; != -1 ile aynı anlama gelir, çünkü read −1'den küçük bir değer döndürmez. read(byte[])'in 0 döndürmesi ise yalnız dizinin boyu 0 olduğunda mümkündür.
8.7.4Kod izleme: blok kopyalama
Blok kopyalamada son parçanın ele alınışı kritiktir. İzlemeyi küçük tutmak için 16 baytlık bir dizi ve 39 baytlık bir dosya kullanılır; gerçek programlarda dizi 8–16 KB olur. Bu kez tamponlu akışlar kullanılmaz, kaynaklar try-with-resources ile açılır.
public class ParcaIzle {
public static void main(String[] args) throws IOException {
byte[] parca = new byte[16];
try (InputStream in = new FileInputStream("dosya.txt");
OutputStream out = new FileOutputStream("dosya2.txt")) {
int boyut;
while ((boyut = in.read(parca)) > -1) {
out.write(parca, 0, boyut);
System.out.println("boyut = " + boyut);
}
}
long n = new File("dosya2.txt").length();
System.out.println("Bitti: " + n + " byte");
}
}| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 3: byte[] parca = new byte[16]; | parca=byte[16] | — |
| 2 | 4: try (InputStream in = new FileInputStream("dosya.txt");5: OutputStream out = new FileOutputStream("dosya2.txt")) { | parca=byte[16] | — |
| 3 | 7: while ((boyut = in.read(parca)) > -1) { | boyut=16 | — |
| 4 | 8: out.write(parca, 0, boyut); | boyut=16 | — |
| 5 | 9: System.out.println("boyut = " + boyut); | boyut=16 | boyut = 16 |
| 6 | 7: while ((boyut = in.read(parca)) > -1) { | boyut=16 | — |
| 7 | 8: out.write(parca, 0, boyut); | boyut=16 | — |
| 8 | 9: System.out.println("boyut = " + boyut); | boyut=16 | boyut = 16 |
| 9 | 7: while ((boyut = in.read(parca)) > -1) { | boyut=7 | — |
| 10 | 8: out.write(parca, 0, boyut); | boyut=7 | — |
| 11 | 9: System.out.println("boyut = " + boyut); | boyut=7 | boyut = 7 |
| 12 | 7: while ((boyut = in.read(parca)) > -1) { | boyut=-1 | — |
| 13 | 13: System.out.println("Bitti: " + n + " byte"); | boyut=-1, n=39 | Bitti: 39 byte |
Tablo 8.16'un ilk iki turunda read(parca) diziyi doldurur ve 16 döndürür. write(parca, 0, 16) okunan 16 baytı yazar. İkinci turda yeni 16 bayt aynı dizinin üzerine yazılır; dizi her turda yeniden kullanılır. Üçüncü turda dosyada yalnız 7 bayt kalmıştır. read dizinin ilk 7 hücresini yeniler ve 7 döndürür. Kalan 9 hücrede bir önceki bloktan kalan baytlar durur.
write(parca, 0, 7) yalnız okunan 7 baytı yazar.Burada write(parca, 0, boyut) yerine write(parca) yazılmış olsaydı dizinin 16 baytının tamamı yazılırdı. Eski 9 bayt da kopyaya girer ve kopya 16 + 16 + 16 = 48 bayt olurdu. Aynı dosyayla yapılan deneme bunu doğrular. Kopyanın son 9 baytı, ikinci bloktan kalan kislar ve parçasıdır:
write(parca) ile: 48 byte
son 9 bayt: kislar ve | gerçek son blok: tamponDördüncü turda read −1 döndürür, > -1 koşulu yanlış olur ve döngü biter. Kaynaklar kapanır ve kopya kaynakla aynı boydadır: 16 + 16 + 7 = 39 bayt. Okuma ve yazma sayısı dosya boyunun dizi boyuna bölümünün tavanıdır: ⌈39/16⌉ = 3. Bir megabaytlık bir dosya 16 KB'lık diziyle 64 turda kopyalanır.
Blok kopyalamada out.write(parca); yazmak. Son blokta dizi tam dolmaz; dizinin sonundaki eski veri de kopyaya gider ve kopya kaynaktan büyük olur. Doğrusu her zaman write(parca, 0, boyut)'tur; boyut, read'in son döndürdüğü değerdir. Aynı kural metne çevirmede de geçerlidir: new String(parca, 0, boyut).
8.7.5Satır satır kopyalama
Metin dosyaları satır satır da kopyalanabilir. Bu yol karakter akışlarıyla çalışır ve satırları kopyalarken işlemek gerektiğinde kullanışlıdır: satırları süzmek, numaralandırmak ya da değiştirmek gibi.
public class SatirKopya {
public static void main(String[] args) throws IOException {
File dosya = new File("dosya.txt");
File dosya2 = new File("dosya2.txt");
FileReader fr = new FileReader(dosya);
BufferedReader br = new BufferedReader(fr);
FileWriter fw = new FileWriter(dosya2);
BufferedWriter bw = new BufferedWriter(fw);
PrintWriter pw = new PrintWriter(bw);
String satir;
// dosya sonuna kadar bir satır oku
while ((satir = br.readLine()) != null)
pw.println(satir); // satırı yaz
pw.close();
br.close();
System.out.println("Kopyalandı: " + dosya2.getName());
}
}Kopyalandı: dosya2.txtOkuma tarafında FileReader ve BufferedReader, yazma tarafında FileWriter, BufferedWriter ve PrintWriter zinciri vardır. Döngü her satırı readLine() ile alır ve println ile yazar. pw.close() zincirin tamamını, yani bw ve fw'yi de kapatır.
Bu yöntemin iki sınırı vardır. Birincisi, yalnız metin dosyaları için uygundur. İkili bir dosya karakter akışından geçerse kodlama dönüşümü baytları bozar. İkincisi, kopya bayt bayt aynı olmayabilir. readLine() satır sonunu atar; println ise sistemin satır sonunu ekler: Windows'ta \r\n, Linux'ta \n. Kaynaktaki son satırın sonunda satır sonu yoksa kopyada bir satır sonu fazladan bulunur. Aşağıdaki deneme Windows'ta, son satırı satır sonuz biten 41 baytlık bir dosyayla yapılmıştır; kopya 43 bayt tutar:
Kopyalandı: dosya2.txt
41 -> 43 bayt; line.separator uzunluğu 2Kaynağın satır sonları Linux biçiminde (\n) olsaydı Windows'taki kopyada her satır bir bayt daha uzun olurdu. Bu nedenle bayt bayt aynı bir kopya gerekiyorsa her zaman bayt akışları kullanılır.
8.7.6Web sayfası indirme
Kopyalama döngüsünün kaynağı bir dosya olmak zorunda değildir. Aşağıdaki program bir web sayfasını indirip diske kaydeder. Kaynak bu kez ağdır: url.openStream() bir InputStream döndürür. Döngünün geri kalanı dosya kopyalamayla aynıdır.
public class WebIndir {
public static void main(String[] args) throws IOException {
URL url = URI.create("https://sercankulcu.github.io/").toURL();
BufferedInputStream bis = new BufferedInputStream(url.openStream());
FileOutputStream fos = new FileOutputStream(new File("kopya.html"));
BufferedOutputStream bos = new BufferedOutputStream(fos);
for (int c = bis.read(); c != -1; c = bis.read()) {
bos.write(c);
}
bis.close();
bos.close();
System.out.println("kopya.html: " + new File("kopya.html").length() + " byte");
}
}kopya.html: 14283 byteÇıktıdaki boyut sayfaya ve indirme anına göre değişir. Bu değer, not hazırlanırken sayfanın sunucudan gelen boyutudur. Kopyalama döngüsü bu kez for ile yazılmıştır: ilk okuma başlatma kısmında yapılır, −1 değilse bayt yazılır ve güncelleme kısmında yeniden okunur. while ile yazılan döngüyle aynı işi görür.
Programdaki URI.create(…).toURL() kullanımı bilinçlidir. new URL(String) yapıcısı Java 20'den beri kullanımdan kaldırılmıştır; yerine bu iki adımlı yol önerilir. URI adresin söz dizimini daha sıkı denetler. Ağ bağlantısı kurulamazsa ya da sunucunun sertifikası doğrulanamazsa openStream() bir IOException alt sınıfı fırlatır. Bu program da main başlığındaki throws IOException ile hatayı JVM'e bırakır.
8.7.7Kısa yollar ve karşılaştırma
Kopyalama döngüsünü her seferinde yeniden yazmak gerekmez. Java kütüphanesinde bu işi yapan iki metot vardır. in.transferTo(out) (Java 9+) bir girdi akışındaki bütün baytları bir çıktı akışına aktarır ve aktardığı bayt sayısını döndürür. Files.copy(kaynak, hedef) (Java 7+) bir dosyayı doğrudan başka bir dosyaya kopyalar. İkisi de içeride blok kopyalama yapar.
public class KopyaKisa {
public static void main(String[] args) throws IOException {
// Java 9+: transferTo
try (InputStream in = new FileInputStream("dosya.txt");
OutputStream out = new FileOutputStream("dosya2.txt")) {
long adet = in.transferTo(out);
System.out.println(adet + " byte aktarıldı");
}
// Java 7+: Files.copy
Files.copy(Path.of("dosya.txt"), Path.of("dosya3.txt"),
StandardCopyOption.REPLACE_EXISTING);
System.out.println(Files.size(Path.of("dosya3.txt")) + " byte kopyalandı");
}
}4 byte aktarıldı
4 byte kopyalandıFiles.copy hedef dosya varsa, üzerine yazma izni verilmedikçe FileAlreadyExistsException fırlatır. Bu davranış bir güvenlik önlemidir: var olan bir dosyanın yanlışlıkla ezilmesini önler. Üzerine yazmak için üçüncü argüman olarak StandardCopyOption.REPLACE_EXISTING verilir. Seçenek verilmeden yapılan deneme istisnayı ve hedef dosyanın değişmediğini gösterir:
java.nio.file.FileAlreadyExistsException: k2.txt
k2.txt: eskiTablo 8.17 kopyalama yöntemlerini karşılaştırır, Şekil 8.20 ise 1 MB'lık bir dosyada çağrı sayılarının ne kadar farklı olduğunu gösterir.
| Yöntem | Birim | Uygun olduğu durum |
|---|---|---|
Bayt bayt (read() / write(int)) | 1 bayt | öğrenmek için; tamponsuz çok yavaş |
Blok blok (read(byte[])) | 8–16 KB | her tür dosya, hızlı |
Satır satır (readLine / println) | 1 satır | yalnız metin; satırları işlerken |
in.transferTo(out) (Java 9+) | blok | iki akış arasında tek satırda kopya |
Files.copy(kaynak, hedef) (Java 7+) | blok | dosyadan dosyaya en kısa yol |
read çağrısı sayısı (çubuk boyları logaritmiktir). Bayt bayt okuma 1 048 576, 8 KB'lık diziyle blok okuma 128 çağrı yapar; Files.copy tek metot çağrısıdır.Hangi yöntem seçilirse seçilsin üç kural geçerlidir: tamponlu çalışılır, akışlar kapatılır (tercihen try-with-resources ile) ve IOException ele alınır. Gerçek bir programda dosyadan dosyaya kopya için Files.copy, iki akış arasında kopya için transferTo kullanılır. Elle yazılan blok döngüsü, kopyalanırken veri üzerinde bir işlem yapılacaksa gerekir; örneğin ilerleme çubuğu göstermek ya da baytları şifrelemek için.
8.8Rastgele erişim
Akışların hepsi sıralıdır: veriyi baştan sona okur ya da yazar. Bazı işler ise dosyanın ortasındaki bir kayda doğrudan gitmeyi gerektirir. Bir öğrenci dosyasındaki bininci öğrencinin notunu okumak için önceki 999 kaydı okumak gereksiz bir yüktür.
8.8.1Sıralı ve rastgele erişim
Sıralı erişimde (sequential access) her okuma ya da yazma bir öncekinin bittiği konumdan devam eder; geri dönülemez. Rastgele erişimde (random access) program dosya içinde istenen konuma atlar ve oradan okur ya da yazar. Konum, dosya başından itibaren bayt sayısıdır ve 0'dan başlar.
Sıralı erişim bir kaset gibidir: bir şarkıya ulaşmak için öncekiler sarılır. Rastgele erişim bir plak gibidir: iğne istenen yere konur (Şekil 8.21). Akışların (FileInputStream, BufferedReader …) hepsi sıralıdır. Sıralı erişim, dosyanın yapısı bilinmediğinde ya da dosyanın tamamı işlenirken kullanılır; kopyalama bunun tipik örneğidir.
seek ile istenen konuma atlar.Rastgele erişim, kayıtların boyu ve konumu biliniyorsa kullanılabilir. Bütün kayıtlar aynı boyda olursa k numaralı kaydın konumu çarpmayla bulunur: konum = k · kayıt boyu (numaralar 0'dan başlar). Belirli bir kaydı okumak ya da yerinde değiştirmek için bu yol çok kullanışlıdır. Sabit boy şartı önemlidir. Satırları farklı uzunluktaki bir metin dosyasında k'ıncı satırın nerede başladığı hesaplanamaz.
8.8.2RandomAccessFile
Java'da rastgele erişim java.io.RandomAccessFile sınıfıyla yapılır. Bu sınıf akışlardan farklıdır: tek bir nesne hem okur hem yazar ve InputStream/OutputStream hiyerarşisinde yer almaz. Nesnenin içinde bir dosya işaretçisi (file pointer) vardır. Her okuma ve yazma işaretçinin gösterdiği konumda yapılır ve işaretçiyi okunan ya da yazılan bayt sayısı kadar ilerletir. Tablo 8.18 sınıfın temel üyelerini verir.
| Üye | Görevi |
|---|---|
RandomAccessFile(String ad, String kip) | kip "r": yalnız okuma; "rw": okuma ve yazma (dosya yoksa oluşturulur) |
seek(long konum) | dosya işaretçisini verilen konuma taşır |
getFilePointer() | işaretçinin dosya başına uzaklığı (bayt) |
length() | dosyanın uzunluğu (bayt) |
read() / write(int b) | bir bayt okur (int olarak döndürür) / yazar |
readInt(), writeInt(int), readDouble() … | ilkel tipleri ikili okur ve yazar (int: 4 bayt) |
close() | sistem kaynaklarını serbest bırakır |
int kayıtları. seek(2 * 4) işaretçiyi 8. bayta taşır; readInt() 2 numaralı kaydı okur. Bayt konumu = kayıt numarası · kayıt boyu.Aşağıdaki program aynı dosyayı önce sıralı, sonra rastgele erişimle okur. dosya burada 124 baytlık bir veri dosyasıdır. Bu notta program, ilk baytı 1 ve 100–103. baytları 00 00 01 00 olan bir dosyayla çalıştırılmıştır.
public class RasgeleErisim {
public static void main(String[] args) throws IOException {
// Sıralı Erişim
FileInputStream sirali = new FileInputStream("dosya");
int bayt = sirali.read();
sirali.close();
System.out.println("İlk byte: " + bayt);
// Rasgele Erişim
RandomAccessFile rast = new RandomAccessFile("dosya", "rw");
// Dosyanın başından 100 byte ilerideki konuma git
rast.seek(100);
// 100. konumdan itibaren 4 byte'lık bir int oku
int veri = rast.readInt();
System.out.println("100. konumdaki int: " + veri);
rast.close();
}
}İlk byte: 1
100. konumdaki int: 256FileInputStream dosyanın başından okur ve ilk baytı, 1'i verir. seek(100) işaretçiyi 100 konumuna, yani 101. bayta taşır, çünkü konumlar 0'dan başlar. Oradaki dört bayt 00 00 01 00'dır. readInt() en anlamlı baytı önce okur: 0 · 224 + 0 · 216 + 1 · 28 + 0 = 256.
RandomAccessFile'ın sınırlarda davranışı akışlardan biraz farklıdır. İşaretçi dosyanın sonunun ötesine taşınabilir; orada yazma yapılırsa dosya o noktaya kadar büyür ve aradaki baytlar tanımsız kalır (bu denemede sıfırdır). Dosya sonunda read() −1 döndürür, readInt() ise EOFException fırlatır. Kip yalnız "r", "rw", "rws" ya da "rwd" olabilir; "w" geçersizdir:
seek(10)+write: uzunluk 11, işaretçi 11
ilk bayt: 0
sonda read(): -1
sonda readInt(): EOFException
r kipi, dosya yok: FileNotFoundException
w kipi: Illegal mode "w" must be one of "r", "rw", "rws", or "rwd"8.8.3Kod izleme: kayıtlara rastgele erişim
Aşağıdaki program beş notu int olarak bir dosyaya yazar. Sonra üçüncü notu doğrudan okur, dördüncü notu yerinde değiştirir ve bütün notları toplar.
import java.io.*;
public class Kayitlar {
public static void main(String[] args) throws IOException {
RandomAccessFile f = new RandomAccessFile("n.dat", "rw");
int[] notlar = {70, 85, 90, 45, 60};
for (int n : notlar)
f.writeInt(n); // her kayıt 4 byte
System.out.println("Uzunluk: " + f.length());
f.seek(2 * 4); // 3. kayda git
System.out.println("3. not: " + f.readInt());
System.out.println("Konum: " + f.getFilePointer());
f.seek(3 * 4); // 4. kayıt
f.writeInt(50); // 45 yerine 50
f.seek(0);
int toplam = 0;
while (f.getFilePointer() < f.length())
toplam += f.readInt();
System.out.println("Toplam: " + toplam);
f.close();
}
}| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 5: RandomAccessFile f = new RandomAccessFile("n.dat", "rw"); | — | — |
| 2 | 6: int[] notlar = {70, 85, 90, 45, 60}; | notlar=[70, 85, 90, 45, 60] | — |
| 3 | 7: for (int n : notlar)8: f.writeInt(n); // her kayıt 4 byte | n=70 | — |
| 4 | 7: for (int n : notlar)8: f.writeInt(n); // her kayıt 4 byte | n=60 | — |
| 5 | 9: System.out.println("Uzunluk: " + f.length()); | — | Uzunluk: 20 |
| 6 | 10: f.seek(2 * 4); // 3. kayda git | — | — |
| 7 | 11: System.out.println("3. not: " + f.readInt()); | — | 3. not: 90 |
| 8 | 12: System.out.println("Konum: " + f.getFilePointer()); | — | Konum: 12 |
| 9 | 13: f.seek(3 * 4); // 4. kayıt | — | — |
| 10 | 14: f.writeInt(50); // 45 yerine 50 | — | — |
| 11 | 15: f.seek(0); | toplam=0 | — |
| 12 | 17: while (f.getFilePointer() < f.length())18: toplam += f.readInt(); | toplam=355 | — |
| 13 | 19: System.out.println("Toplam: " + toplam); | toplam=355 | Toplam: 355 |
Tablo 8.19'ın ilk adımında "rw" kipi dosya yoksa onu oluşturur; işaretçi 0'dadır. Döngüde her writeInt 4 bayt yazar ve işaretçiyi 4 ilerletir. Döngü bitince dosya 5 · 4 = 20 bayttır ve işaretçi dosyanın sonunda, 20'dedir. Dosyanın gerçek baytları şöyledir:
20: 00 00 00 46 00 00 00 55 00 00 00 5A 00 00 00 32 00 00 00 3C46 = 70, 55 = 85, 5A = 90'dır. Bu satırdaki dördüncü kayıt 32 = 50'dir, çünkü baytlar program bittikten sonra yazdırılmıştır; ilk yazılan değer 45, yani 2D idi.
Altıncı adımda seek(2 * 4) işaretçiyi 8. bayta taşır. Burası üçüncü kaydın, yani 2 numaralı kaydın başıdır. Önceki kayıtlar okunmaz; doğrudan oraya atlanır. readInt() 8–11. baytları okur ve 90 döndürür. İşaretçi 12'ye ilerler. Dokuzuncu adımdaki seek(3 * 4) işaretçiyi 12'ye taşır; işaretçi zaten oradadır. Onuncu adımda writeInt(50) 45'in yerine yazılır. Dosyanın geri kalanı değişmez. Sıralı bir akışla bu yapılamaz: dosyanın ortasındaki bir kaydı değiştirmek için bütün dosyanın yeniden yazılması gerekirdi.
seek(8) işaretçiyi üçüncü kaydın başına taşır. (b) writeInt(50) dördüncü kaydı yerinde değiştirir.Son adımlarda seek(0) başa döner. Döngü, işaretçi dosya uzunluğuna ulaşana kadar int okur: 70 + 85 + 90 + 50 + 60 = 355. Programın çıktısı sunumdaki değerlerle aynıdır.
Uzunluk: 20
3. not: 90
Konum: 12
Toplam: 355Maliyet açısından fark açıktır. Sıralı akışla k numaralı kayda ulaşmak için önceki k kaydın okunması gerekir; süre k ile orantılıdır, O(k). Rastgele erişimde konum bir çarpmayla hesaplanır ve tek seek yapılır; süre kayıt numarasından bağımsızdır, O(1).
Bir öğrenci dosyasında her kayıt sırasıyla bir int öğrenci numarası, bir double ortalama ve bir int kredi tutsun. Kayıtlar 0'dan numaralanıyor. 37 numaralı kaydın ortalaması nasıl okunur?
Kayıt boyu 4 + 8 + 4 = 16 bayttır. 37 numaralı kayıt 37 · 16 = 592. baytta başlar. Ortalama, kaydın içinde öğrenci numarasından sonra gelir; kayıt başına göre 4 bayt ileridedir. f.seek(37 * 16 + 4); double ort = f.readDouble(); yazılır. Okumadan sonra işaretçi 592 + 4 + 8 = 604'tedir; bu da aynı kaydın kredi alanının başıdır. Kayıtlardan biri double yerine float ile yazılmış olsaydı bütün konum hesapları bozulurdu. Rastgele erişim, dosya biçiminin kesin olarak bilinmesini gerektirir.
8.9Serileştirme
Şimdiye kadar dosyalara sayı ve metin yazıldı. Nesne yönelimli bir programda ise saklanmak istenen şey çoğu zaman bir nesnedir: bir oyunun durumu, bir öğrenci kaydı, bir ayar nesnesi. Nesnenin her alanını tek tek yazıp geri okumak zahmetlidir. Java bu işi tek çağrıyla yapan bir mekanizma sunar.
8.9.1Serileştirme nedir?
Serileştirme (serialization), bir nesnenin durumunu, yani alanlarının değerlerini, bir bayt dizisine çevirmektir. Bu bayt dizisi bir dosyaya yazılabilir ya da ağ üzerinden başka bir ortama taşınabilir. Bayt dizisinden yeniden nesne kurmaya serileştirmeyi geri alma (deserialization) denir.
Bir sınıfın nesnelerinin serileştirilebilmesi için sınıf java.io.Serializable arayüzünü uygulamalıdır. Bu arayüzün hiç metodu yoktur. Böyle arayüzlere işaret arayüzü (marker interface) denir; tek görevi sınıfı “serileştirilebilir” diye işaretlemektir. Serileştirme mekanizması nesneyi yazmadan önce bu işarete bakar.
Serileştirmenin üç akışı vardır (Şekil 8.24). Bellekte bir nesne durur. ObjectOutputStream.writeObject onu baytlara çevirir; dosya AC ED sihirli sayısıyla başlar. ObjectInputStream.readObject bu baytlardan yeni bir nesne kurar. Yeni nesnenin değerleri aynıdır ama kendisi farklı bir nesnedir.
writeObject bellekteki Ogrenci nesnesini 72 baytlık bir diziye çevirip dosyaya yazar; readObject aynı durumda, ama farklı bir nesne kurar.Serileştirme oyun kayıtlarında, önbelleklerde ve nesnelerin ağ üzerinden başka bir JVM'e gönderilmesinde kullanılır. Biçimi Java'ya özgüdür ve sınıfın yapısına sıkı bağlıdır. Bu yüzden uzun süre saklanacak ya da başka dillerle paylaşılacak veriler için JSON gibi metin biçimleri daha çok tercih edilir. Ayrıca güvenilmeyen bir kaynaktan gelen serileştirilmiş veriyi geri almak güvenlik açığı oluşturabilir, çünkü geri alma sırasında programın sınıfları bilinmeyen verilerle kurulur.
8.9.2Serileştirilebilir sınıf
import java.io.Serializable;
class Ogrenci implements Serializable {
private static final long serialVersionUID = 1L;
private String ad;
private int yas;
public Ogrenci(String ad, int yas) {
this.ad = ad;
this.yas = yas;
}
public String toString() {
return "Ogrenci [ad=" + ad + ", yas=" + yas + "]";
}
}Sınıftaki üç ayrıntı serileştirmeyle ilgilidir:
implements Serializablesınıfı serileştirilebilir yapar. Metot yazmak gerekmez.serialVersionUIDsınıfın sürüm kimliğidir. Serileştirilmiş baytlara bu sayı da yazılır. Okunurken dosyadaki sayı ile programdaki sınıfın sayısı karşılaştırılır; farklıysaInvalidClassExceptionfırlatılır. Sayının elle yazılması önerilir. Yazılmazsa serileştirme mekanizması onu sınıfın yapısından hesaplar; sınıfa bir alan ya da metot eklenince hesaplanan sayı değişir ve eski dosyalar okunamaz.- Alanlar da serileştirilebilir olmalıdır.
Stringveintserileştirilebilir. Bir alanın tipiSerializabledeğilse yazma sırasındaNotSerializableExceptionfırlatılır.
Sürüm denetimi gerçekten çalışır. serialVersionUID = 1L ile yazılan bir dosya, aynı sınıfın 2L sürümüyle okunmaya çalışıldığında şu istisna gelir:
java.io.InvalidClassException: Ogrenci; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 28.9.3Nesneyi yazma ve geri okuma
Serileştirme için ObjectOutputStream, geri almak için ObjectInputStream kullanılır. İkisi de sarmalayıcı akıştır ve bir dosya akışını sarar.
// Serileştirme işlemi
try (ObjectOutputStream cikti = new ObjectOutputStream(
new FileOutputStream("ogrenci.ser"))) {
Ogrenci ogrenci = new Ogrenci("Ahmet", 20);
cikti.writeObject(ogrenci);
System.out.println("Serileştirme başarılı.");
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("ogrenci.ser: " + new File("ogrenci.ser").length() + " byte");
// Seriyi okuma işlemi
try (ObjectInputStream girdi = new ObjectInputStream(
new FileInputStream("ogrenci.ser"))) {
Ogrenci ogrenci = (Ogrenci) girdi.readObject();
System.out.println("Ogrenci: " + ogrenci);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}Serileştirme başarılı.
ogrenci.ser: 72 byte
Ogrenci: Ogrenci [ad=Ahmet, yas=20]İlk blok bir Ogrenci nesnesi oluşturur ve writeObject ile yazar. Dosya 72 bayttır. Bu küçük nesne için çok gibi görünür. Ama dosyada yalnız iki alanın değeri değil, sınıfın tanımı da vardır: sınıfın adı, sürüm kimliği, alanların adları ve tipleri. Dosyanın baytları bunu gösterir:
AC ED 00 05 73 72 00 07 | ····sr··Ogrenci···········I··yasL··adt··Ljava/lang/String;xp····t··Ahmetİlk dört bayt AC ED 00 05 akış başlığıdır: sihirli sayı ve serileştirme protokolünün sürümü. Okunabilir kısımda Ogrenci, yas, ad, java/lang/String ve Ahmet görünür.
İkinci blok nesneyi geri okur. readObject Object tipinde bir değer döndürür; bu yüzden (Ogrenci) dönüşümü gerekir. readObject iki kontrol edilen istisna fırlatabilir: IOException ve ClassNotFoundException. İkincisi, dosyadaki sınıf programda bulunamadığında fırlatılır. Program ikisini çoklu catch ile birlikte yakalar. ClassNotFoundException yakalanmaz ya da bildirilmezse kod derlenmez:
Ana.java:6: error: unreported exception ClassNotFoundException; must be caught or declared to be thrown
Object o = g.readObject();
^
1 errorDönüşüm yanlış tipe yapılırsa çalışma zamanında ClassCastException, dosya hiç serileştirilmiş veri değilse StreamCorruptedException fırlatılır:
ClassCastException
StreamCorruptedException: invalid stream header: 6D6572686D657268, düz metin dosyasının ilk dört baytıdır: m, e, r, h. Akış başlığı AC ED olmadığı için okuma en baştan reddedilir.
8.9.4transient ve static alanlar
Bir nesnenin her alanı saklanmak istenmez. Şifre saklanmamalıdır. Geçici bir önbellek ya da açık bir dosya bağlantısı saklanamaz bile. Bu tür alanlar transient (geçici) anahtar kelimesiyle işaretlenir.
transient olarak işaretlenen alan serileştirmede atlanır. Nesne geri okunduğunda bu alan tipinin varsayılan değerini alır: referanslar null, sayılar 0, boolean false. static alanlar ise nesneye değil sınıfa ait oldukları için hiç serileştirilmez.
class Kullanici implements Serializable {
private static final long serialVersionUID = 1L;
static int sayac;
private String ad;
private transient String sifre; // yazılmaz
Kullanici(String ad, String sifre) {
this.ad = ad;
this.sifre = sifre;
}
public String toString() {
return "Kullanici [ad=" + ad + ", sifre=" + sifre + "]";
}
}Aşağıdaki deneme bir Kullanici nesnesini yazar, static sayacı değiştirir ve nesneyi geri okur:
Kullanici k = new Kullanici("ayse", "gizli123");
Kullanici.sayac = 5;
ObjectOutputStream c = new ObjectOutputStream(
new FileOutputStream("k.ser"));
c.writeObject(k);
c.close();
Kullanici.sayac = 99; // static: sınıfa ait
ObjectInputStream g = new ObjectInputStream(
new FileInputStream("k.ser"));
Kullanici okunan = (Kullanici) g.readObject();
g.close();
System.out.println(okunan);
System.out.println("sayac = " + Kullanici.sayac);
System.out.println("aynı nesne mi? " + (okunan == k));Kullanici [ad=ayse, sifre=null]
sayac = 99
aynı nesne mi? falseÜç satırın her biri bir kuralı gösterir. sifre null gelmiştir, çünkü transient alan dosyaya yazılmamıştır. sayac 99'dur, çünkü dosyada sayac yoktur ve okuma onu değiştiremez; değer bellekteki son değerdir. okunan == k false'tur, çünkü readObject yeni bir nesne kurar.
8.9.5Kod izleme: serileştirme
Aynı kod parçası adım adım izlendiğinde dosyanın boyutu ve nesnelerin durumu her an görülür. İzleme programı, sunumdaki koda iki satır ekleyerek dosya boyutunu da yazdırmıştır:
[başlıktan sonra: 4]
[k.ser: 63]
Kullanici [ad=ayse, sifre=null]
sayac = 99
aynı nesne mi? false| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 1: Kullanici k = new Kullanici("ayse", "gizli123"); | k=Kullanici@1 | — |
| 2 | 2: Kullanici.sayac = 5; | k=Kullanici@1, Kullanici.sayac=5 | — |
| 3 | 3: ObjectOutputStream c = new ObjectOutputStream(4: new FileOutputStream("k.ser")); | c=ObjectOutputStream | — |
| 4 | 5: c.writeObject(k); | c=ObjectOutputStream | — |
| 5 | 6: c.close(); | c=kapalı | — |
| 6 | 7: Kullanici.sayac = 99; // static: sınıfa ait | Kullanici.sayac=99 | — |
| 7 | 8: ObjectInputStream g = new ObjectInputStream(9: new FileInputStream("k.ser")); | g=ObjectInputStream | — |
| 8 | 10: Kullanici okunan = (Kullanici) g.readObject(); | okunan=Kullanici@2 | — |
| 9 | 12: System.out.println(okunan); | okunan=Kullanici@2 | Kullanici [ad=ayse, sifre=null] |
| 10 | 13: System.out.println("sayac = " + Kullanici.sayac); | Kullanici.sayac=99 | sayac = 99 |
| 11 | 14: System.out.println("aynı nesne mi? " + (okunan == k)); | okunan == k sonucu=false | aynı nesne mi? false |
Tablo 8.20'nin birinci adımında heap'te yeni bir Kullanici nesnesi oluşur; ad ve sifre doludur. static sayac sınıfın alanıdır ve tek kopyası vardır. Üçüncü adımda ObjectOutputStream açılır ve dosyaya hemen 4 baytlık akış başlığını yazar. Dördüncü adımda writeObject sınıf tanımını ve alanları yazar; dosya 63 bayt olur. transient sifre ve static sayac yazılmaz.
writeObject yalnız ad'ı yazar; dosya 63 bayt. (b) readObject yeni bir nesne kurar; sifre null'dır.Sekizinci adım serileştirmenin ince bir noktasını taşır. readObject heap'te yeni bir nesne kurar ama Kullanici'nin yapıcısını çağırmaz. Alanların değerleri doğrudan dosyadan gelir. Dosyada olmayan alanlar varsayılan değerde kalır. Bu yüzden transient bir alanın tanımındaki ilk değer de çalışmaz: transient int b = 2; yazılmış olsa bile geri okunan nesnede b 0'dır. Son adımda iki referans karşılaştırılır; içerikleri benzese de iki ayrı nesnedir.
Serializable'ı unutmak. writeObject ile yazılan sınıf ya da alanlarından birinin tipi bu arayüzü uygulamıyorsa çalışma zamanında java.io.NotSerializableException: Araba gibi bir istisna fırlatılır. Derleyici bunu varsayılan ayarlarla denetlemez. Çözüm sınıfı Serializable yapmak ya da alanı transient işaretlemektir.
8.10NIO
Java NIO (New I/O, yeni G/Ç), Java 1.4 ile java.nio paketinde geldi. Klasik java.io akışlarına göre daha yüksek başarım ve esneklik sağlar. Java 7 ile gelen NIO.2 (java.nio.file) dosya işlemleri için Path ve Files sınıflarını ekledi.
8.10.1NIO'nun yapı taşları
NIO dört temel kavram üzerine kuruludur:
- Tampon (
Buffer): veriyi geçici olarak tutan bellek alanları. - Kanal (
Channel): giriş ve çıkış işlemlerini yöneten, çift yönlü olabilen bağlantı nesneleri. - Seçici (
Selector): birden çok kanalı tek bir iş parçacığıyla izleyen ve hazır olanları bildiren nesne. - Yol (
Path): dosya ve klasör yollarını temsil eden NIO.2 sınıfı.
8.10.2Akış mı, kanal mı?
Klasik G/Ç'de program bir akışla çalışır ve veri tek yönde, bayt bayt ya da karakter karakter akar. NIO'da program bir tampona yazar ya da tampondan okur; kanal tampon dolusu veriyi kaynağa taşır (Şekil 8.26).
Klasik G/Ç (java.io) | NIO (java.nio) | |
|---|---|---|
| Temel kavram | akış | kanal ve tampon |
| Yön | tek yön: girdi ya da çıktı | kanal çift yönlü olabilir |
| Birim | bayt ya da karakter | tampon dolusu blok |
| Bekleme | her zaman bloklayan | bloklamayan kip de var |
| Çok bağlantı | bağlantı başına bir iş parçacığı | tek iş parçacığı ve seçici |
| Ne zaman? | basit dosya ve konsol işleri | çok bağlantılı sunucular, büyük dosyalar |
“Bloklamayan” (non-blocking) kip, NIO'nun en önemli farkıdır. Klasik akışta read() veri gelene kadar bekler; iş parçacığı o sürede başka bir iş yapamaz. Bin istemcili bir sunucu bin iş parçacığı açmak zorunda kalır. Bloklamayan bir kanal ise veri yoksa hemen döner. Tek bir iş parçacığı böylece birçok kanalı sırayla yoklayabilir.
8.10.3Tampon ve özellikleri
NIO tamponu, verileri geçici olarak saklayan sabit boyutlu bir bellek alanıdır: bir dizi ve onun üzerinde konum bilgileri. Dört özelliği vardır. Kapasite (capacity) tutulabilecek en fazla öğe sayısıdır ve hiç değişmez. Konum (position) bir sonraki okumanın ya da yazmanın yapılacağı indistir. Sınır (limit) okunamayan ya da yazılamayan ilk indistir. İşaret (mark), mark() ile saklanan konumdur; reset() oraya döner. Her zaman 0 ≤ işaret ≤ konum ≤ sınır ≤ kapasite.
remaining() = sınır − konum = 4'tür.Her ilkel tip için bir tampon sınıfı vardır: ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer. Kanallar bayt taşıdığı için en çok ByteBuffer kullanılır. Tampon ya ByteBuffer.allocate(1024) ile oluşturulur ya da var olan bir dizi ByteBuffer.wrap(dizi) ile sarılır.
Bir tamponun iki kipi vardır. Yazma kipinde konum bir sonraki boş hücreyi, sınır kapasiteyi gösterir. Okuma kipinde konum bir sonraki okunacak hücreyi, sınır yazılan verinin sonunu gösterir. Kipler arasında flip() ile geçilir. Tampon kendisinin hangi kipte olduğunu bilmez; programcı bunu izlemek zorundadır.
8.10.4ByteBuffer örnekleri
public class TamponOrnek {
public static void main(String[] args) {
// 1 KB boyutunda bir ByteBuffer oluştur
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Veri yazma
buffer.put("Merhaba, Java!".getBytes());
System.out.println("put sonrası : " + buffer);
// Buffer'ı okuma moduna geçirme
buffer.flip();
System.out.println("flip sonrası : " + buffer);
// Veriyi okuma
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
}
}put sonrası : java.nio.HeapByteBuffer[pos=14 lim=1024 cap=1024]
flip sonrası : java.nio.HeapByteBuffer[pos=0 lim=14 cap=1024]
Merhaba, Java!put 14 baytı yazar ve konumu 14'e ilerletir. flip() sınırı 14'e çeker ve konumu 0'a döndürür; tampon artık okuma kipindedir. hasRemaining() konumun sınırdan küçük olup olmadığını sorar. Döngü tam 14 bayt okur. flip() unutulsaydı okuma 14. konumdan başlar ve 1010 boş bayt okunurdu.
Bu döngü yalnız ASCII metinde doğru sonuç verir. Her baytı tek başına char'a çevirmek, çok baytlı karakterleri bozar:
public class TamponUmlaut {
public static void main(String[] args) {
ByteBuffer b = ByteBuffer.allocate(64);
b.put("Dünya".getBytes(StandardCharsets.UTF_8));
b.flip();
while (b.hasRemaining())
System.out.print((char) b.get()); // yanlış
System.out.println();
b.rewind();
System.out.println(StandardCharsets.UTF_8.decode(b)); // doğru
}
}Dᅢᄐnya
Dünya“Dünya” UTF-8'de 6 bayttır; ü = C3 BC. (char) b.get() işaretli byte değerini char'a çevirir. C3 bir byte olarak −61'dir ve char'a çevrilince işaret biti genişler: ᅢ, yani ᅢ. BC de ᄐ olur. Metni çözmek için Charset.decode kullanılır. rewind() konumu yeniden 0'a getirdiği için aynı baytlar bu kez doğru çözülür.
8.10.5Kod izleme: tampon işlemleri
Aşağıdaki program altı tampon işlemini sırayla kullanır: put, flip, get, compact, rewind ve clear.
import java.nio.ByteBuffer;
public class TamponIzle {
public static void main(String[] args) {
ByteBuffer b = ByteBuffer.allocate(8);
b.put((byte) 'J');
b.put((byte) 'A');
b.put((byte) 'V');
b.put((byte) 'A');
b.flip();
char c1 = (char) b.get();
char c2 = (char) b.get();
b.compact();
b.put((byte) '!');
b.flip();
b.get();
b.rewind();
b.clear();
System.out.println(c1 + "" + c2 + " " + b);
}
}| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 5: ByteBuffer b = ByteBuffer.allocate(8); | pos=0, lim=8, cap=8 | — |
| 2 | 6: b.put((byte) 'J'); | pos=1, lim=8, cap=8 | — |
| 3 | 7: b.put((byte) 'A'); | pos=2, lim=8, cap=8 | — |
| 4 | 8: b.put((byte) 'V'); | pos=3, lim=8, cap=8 | — |
| 5 | 9: b.put((byte) 'A'); | pos=4, lim=8, cap=8 | — |
| 6 | 10: b.flip(); | pos=0, lim=4, cap=8 | — |
| 7 | 11: char c1 = (char) b.get(); | pos=1, lim=4, cap=8, c1='J' | — |
| 8 | 12: char c2 = (char) b.get(); | pos=2, lim=4, cap=8, c1='J', c2='A' | — |
| 9 | 13: b.compact(); | pos=2, lim=8, cap=8, c1='J', c2='A' | — |
| 10 | 14: b.put((byte) '!'); | pos=3, lim=8, cap=8, c1='J', c2='A' | — |
| 11 | 15: b.flip(); | pos=0, lim=3, cap=8, c1='J', c2='A' | — |
| 12 | 16: b.get(); | pos=1, lim=3, cap=8, c1='J', c2='A' | — |
| 13 | 17: b.rewind(); | pos=0, lim=3, cap=8, c1='J', c2='A' | — |
| 14 | 18: b.clear(); | pos=0, lim=8, cap=8, c1='J', c2='A' | — |
| 15 | 19: System.out.println(c1 + "" + c2 + " " + b); | pos=0, lim=8, cap=8, c1='J', c2='A' | JA java.nio.HeapByteBuffer[pos=0 lim=8 cap=8] |
Tablodaki değişkenler tamponun içini göstermez. Her işlemden sonra tamponun sekiz hücresini de yazdıran bir izleme programı bu eksiği giderir (· boş hücre):
allocate pos=0 lim=8 [········]
put J pos=1 lim=8 [J·······]
put A pos=2 lim=8 [JA······]
put V pos=3 lim=8 [JAV·····]
put A pos=4 lim=8 [JAVA····]
flip pos=0 lim=4 [JAVA····]
get J pos=1 lim=4 [JAVA····]
get A pos=2 lim=4 [JAVA····]
compact pos=2 lim=8 [VAVA····]
put ! pos=3 lim=8 [VA!A····]
flip pos=0 lim=3 [VA!A····]
get V pos=1 lim=3 [VA!A····]
rewind pos=0 lim=3 [VA!A····]
clear pos=0 lim=8 [VA!A····]İlk beş adımda dört put JAVA'yı yazar ve konum 4'e ilerler. flip() sınırı eski konuma, 4'e çeker; konum 0 olur. Artık yalnız yazılan dört bayt okunabilir. İki get J ve A'yı okur; okunmamış iki bayt kalır.
flip(): sınır 4, konum 0. (b) compact(): okunmamış V ve A başa taşınır, konum 2, sınır 8. (c) clear(): konum 0, sınır 8; veri silinmez.compact() okunmamış V ve A'yı tamponun başına kopyalar. Konum kalan öğe sayısına, 2'ye, sınır kapasiteye, 8'e çekilir; tampon yeniden yazma kipine geçer. Dikkat edilecek nokta şudur: 2 ve 3 numaralı hücrelerdeki eski V ve A silinmez. Hücre içeriği VAVA'dır; yeni yazılan veri bu eski baytların üzerine yazılır. Sonraki put('!') konum 2'deki eski V'nin üzerine yazar: VA!A.
İkinci flip() sınırı 3'e çeker; okunabilir veri V, A, !'dir. Bir get V'yi okur. rewind() konumu 0'a döndürür ama sınırı değiştirmez; aynı veri baştan yeniden okunabilir. clear() konumu 0'a, sınırı kapasiteye çeker. Adına rağmen veriyi silmez; tampon yalnız “boş” sayılır ve üzerine yazılır. Son satır toString()'in konum, sınır ve kapasiteyi verdiğini gösterir: JA java.nio.HeapByteBuffer[pos=0 lim=8 cap=8]. Tablo 8.23 işlemleri özetler.
| Metot | Ne yapar? | Konum | Sınır |
|---|---|---|---|
put(x) | konuma veri yazar | +1 | — |
get() | konumdaki veriyi okur | +1 | — |
get(i) | mutlak indisteki veriyi okur | değişmez | — |
flip() | yazmadan okumaya geçer | 0 | eski konum |
rewind() | aynı veriyi baştan yeniden okumak için | 0 | değişmez |
clear() | yeniden yazmaya hazırlar; veriyi silmez | 0 | kapasite |
compact() | okunmamış veriyi başa taşır, yazmaya devam eder | kalan sayısı | kapasite |
mark() / reset() | konumu işaretler / işarete döner | — / işaret | — |
Tamponun artısı, aynı bellek alanının tekrar tekrar kullanılmasıdır. Veri üzerinde esnek gezinme ve değiştirme yapılabilir. Eksisi, kullanımın karmaşık olmasıdır: flip'i unutmak en sık hatadır. Tamponun boyutu sonradan değiştirilemez; sınırdan fazla yazma BufferOverflowException, sınırdan fazla okuma BufferUnderflowException fırlatır:
java.nio.HeapByteBuffer[pos=0 lim=5 cap=5]
BC sonra reset: pos=1 get=B
BufferOverflowException
BufferUnderflowException
java.nio.HeapIntBuffer[pos=0 lim=2 cap=3] 7 8
putInt+putShort pos=6Bu çıktının ikinci satırı mark ile reset'i gösterir: konum 1'deyken işaret konur, iki okumadan sonra reset() konumu yeniden 1'e getirir. Son satırda putInt ve putShort konumu 4 + 2 = 6 ilerletir. ByteBuffer da DataOutputStream gibi ilkel tipleri ikili yazabilir.
flip()'i unutmak. b.put((byte) 'J'); b.put((byte) 'A'); yazıldıktan sonra doğrudan b.get() çağrılırsa konum 2'deki boş hücre okunur ve 0 döner. Yazılan veri değil, sonrası okunur. Yazmadan sonra okumadan önce flip() şarttır; yeniden yazmadan önce de clear() ya da compact(). Bir de tip hatası vardır: ByteBuffer'ın put metodu byte ister ve b.put('J') derlenmez, çünkü char'dan byte'a dönüşüm kendiliğinden yapılmaz.
8.10.6Kanallar
Kanal (channel), bir dosya, bir ağ soketi ya da başka bir G/Ç aygıtıyla veri alışverişi yapan bağlantıdır. Kanaldan veri her zaman bir tampona okunur (read(tampon)) ve bir tampondan yazılır (write(tampon)).
put eder ve tampondan get eder; kanal tamponu dosyaya ya da sokete yazar ve onlardan tampona okur.Dört temel kanal türü vardır: FileChannel dosyada okuma ve yazma, SocketChannel TCP ağ soketi üzerinden veri iletimi, ServerSocketChannel gelen TCP bağlantılarını kabul etme, DatagramChannel UDP soketi üzerinden veri iletimi içindir. Kanallar FileChannel.open(yol, seçenekler…) ve SocketChannel.open() gibi fabrika metotlarıyla açılır. İş bitince close() ile ya da try-with-resources ile kapatılır.
public class KanalAcKapa {
public static void main(String[] args) throws IOException {
// FileChannel oluşturma
FileChannel fileChannel = FileChannel.open(
Paths.get("dosya.txt"), StandardOpenOption.READ);
// SocketChannel oluşturma
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("example.com", 80));
System.out.println("Bağlandı: " + socketChannel.isConnected());
// Kanal kapatma
fileChannel.close();
socketChannel.close();
}
}Bağlandı: trueBu program iki kanal açar: bir dosya kanalı ve example.com'un 80 numaralı portuna bağlanan bir ağ kanalı. connect bloklayan kipte bağlantı kurulana kadar bekler. Program bu not için gerçekten çalıştırıldı ve bağlantı kuruldu. Kanalları açık bırakmamak için iki close() çağrısı yapılır; bir istisna olursa bu satırlara ulaşılmaz. Bu yüzden try-with-resources daha güvenlidir.
Dosya kanalıyla okuma, tampon kullanımının tipik kalıbını izler:
public class KanalOku {
public static void main(String[] args) throws IOException {
try (FileChannel channel = FileChannel.open(
Paths.get("dosya.txt"), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Dosyadan veriyi okuma
int bytesRead = channel.read(buffer);
System.out.println(bytesRead + " byte okundu");
// Buffer'ı okuma moduna geçirme
buffer.flip();
// Veriyi ekrana yazma
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
}
}
}17 byte okundu
NIO kanal ornegidosya.txt'de NIO kanal ornegi ve bir satır sonu vardır: 16 + 1 = 17 bayt. channel.read(buffer) kanaldan tampona okur ve okunan bayt sayısını döndürür; dosya sonunda −1 döndürür. Okuma tampona bir yazmadır; tampondan okumadan önce flip() gerekir. Büyük bir dosyada tek read dosyanın tamamını okuyamaz; o zaman okuma bir döngüde yapılır ve her turun sonunda tampon clear() ya da compact() ile boşaltılır.
public class KanalYaz {
public static void main(String[] args) throws IOException {
try (FileChannel kanal = FileChannel.open(Paths.get("cikti.txt"),
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
byte[] veri = "Kanal ile yazıldı\n".getBytes(StandardCharsets.UTF_8);
ByteBuffer tampon = ByteBuffer.wrap(veri);
int yazilan = kanal.write(tampon);
System.out.println(yazilan + " byte yazıldı");
}
}
}20 byte yazıldıByteBuffer.wrap(veri) hazır bir diziyi tampon olarak kullanır. Konum 0, sınır dizinin boyudur; tampon zaten okuma kipinde olduğu için flip() gerekmez. Metin UTF-8'de 20 bayttır: 17 karakter, ı'ların her biri için birer bayt daha ve satır sonu. CREATE seçeneği dosya yoksa onu oluşturur. Dosya varsa ve daha uzunsa CREATE ile WRITE onu kısaltmaz; yeni baytlar başa yazılır, eski içeriğin sonu kalır. Dosyayı sıfırlamak için TRUNCATE_EXISTING eklenir.
Dosya kanalı çift yönlüdür. Aynı kanal hem okuyup hem yazabilir ve position ile istenen konuma gidebilir; bu yönüyle RandomAccessFile'a benzer:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;
public class KanalCift {
public static void main(String[] args) throws IOException {
try (FileChannel k = FileChannel.open(Path.of("cift.dat"), StandardOpenOption.CREATE,
StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
k.write(ByteBuffer.wrap("ABCDEF".getBytes()));
System.out.println("yazdıktan sonra konum " + k.position() + ", boyut " + k.size());
k.position(2);
ByteBuffer t = ByteBuffer.allocate(3);
int n = k.read(t);
t.flip();
System.out.println(n + " bayt: " + new String(t.array(), 0, t.limit()));
ByteBuffer u = ByteBuffer.allocate(10);
System.out.println("sonda read: " + k.read(u));
}
}
}yazdıktan sonra konum 6, boyut 6
3 bayt: CDE
sonda read: 1Ağ kanalı da aynı kalıpla çalışır. Aşağıdaki parça bir main gövdesidir: bir web sunucusuna HTTP isteği gönderir ve cevabın ilk satırını yazar.
try (SocketChannel socketChannel = SocketChannel.open()) {
socketChannel.connect(new InetSocketAddress("example.com", 80));
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(("GET / HTTP/1.1\r\nHost: example.com\r\n"
+ "Connection: close\r\n\r\n").getBytes(StandardCharsets.US_ASCII));
buffer.flip();
// Veriyi sokete yazma
socketChannel.write(buffer);
// Cevabı okuma
buffer.clear();
socketChannel.read(buffer);
buffer.flip();
String cevap = StandardCharsets.US_ASCII.decode(buffer).toString();
System.out.println(cevap.substring(0, cevap.indexOf("\r\n")));
} catch (IOException e) {
e.printStackTrace();
}HTTP/1.1 200 OKİstek önce tampona put edilir, sonra flip edilip kanala yazılır. Cevap için tampon clear() ile boşaltılır, kanaldan okunur ve yeniden flip edilir. HTTP/1.1 isteklerinde Host başlığı zorunludur. Cevabın ilk satırı HTTP/1.1 200 OK'dir; bu satır not hazırlanırken programın gerçek çıktısıdır. Tek read çağrısı cevabın tamamını almayabilir. Gerçek bir istemci, sunucu bağlantıyı kapatıp read −1 döndürene kadar okumaya devam eder.
8.10.7Path ve Files
NIO.2'nin Path sınıfı File'ın modern karşılığıdır. Files sınıfı ise yaygın dosya işlerini tek çağrıya indiren static metotlar içerir.
public class DosyalarNio {
public static void main(String[] args) throws IOException {
Path yol = Path.of("veri", "notlar.txt"); // Java 11+
Files.createDirectories(yol.getParent());
Files.writeString(yol, "Ali 70\nAyşe 85\nCan 90\n");
System.out.println(yol.getFileName() + ": " + Files.exists(yol));
System.out.println("Boyut: " + Files.size(yol) + " byte");
List<String> satirlar = Files.readAllLines(yol);
for (String satir : satirlar) {
System.out.println(satir.toUpperCase(Locale.ROOT));
}
Files.write(yol, List.of("Deniz 60"), StandardOpenOption.APPEND);
System.out.println(Files.readAllLines(yol).size() + " satır");
}
}notlar.txt: true
Boyut: 23 byte
ALI 70
AYŞE 85
CAN 90
4 satırPath.of("veri", "notlar.txt") yolu parçalardan kurar ve sistemin ayırıcısını kendisi koyar. createDirectories ara klasörlerle birlikte klasörü oluşturur. writeString metni UTF-8 ile tek çağrıda yazar: Ayşe'deki ş iki bayt olduğu için 22 karakter 23 bayt tutar. readAllLines bütün dosyayı satır listesi olarak okur. Files.write ile APPEND seçeneği yeni bir satırı sona ekler ve dosya 4 satır olur.
Programdaki toUpperCase(Locale.ROOT) çağrısı bilinçlidir. toUpperCase() parametresiz çağrıldığında bilgisayarın yerel ayarını kullanır. Türkçe yerel ayarda küçük i'nin büyüğü İ'dir; program Türkçe bir sistemde ALİ 70, İngilizce bir sistemde ALI 70 yazardı. Locale.ROOT (java.util.Locale) dönüşümü dilden bağımsız kılar ve çıktı her makinede aynı olur. Dile bağlı olmaması gereken dönüşümlerde, örneğin dosya adlarında ya da anahtar kelimelerde, bu biçim kullanılır. Fark Türkçe yerel ayarda şöyle görünür:
tr: ALİ 70 / ROOT: ALI 70 / tıtle| Metot | Görevi |
|---|---|
Path.of(…) / Paths.get(…) | yol oluşturur (File'ın modern karşılığı) |
Files.readAllLines, Files.readString | bütün dosyayı tek çağrıda okur (UTF-8) |
Files.write, Files.writeString | tek çağrıda yazar |
Files.exists, size, copy, move, delete | dosya sistemi işlemleri |
Files'ın metotları File'ınkilerden bir yönüyle daha güvenlidir: başarısızlığı false döndürerek değil, açıklayıcı bir istisnayla bildirir. Files.delete dolu bir klasörü silemezse DirectoryNotEmptyException fırlatır. readAllLines ve readString bütün dosyayı belleğe aldığı için küçük ve orta boy dosyalar içindir; çok büyük dosyalar BufferedReader ile satır satır okunur.
8.10.8Seçici
Seçici (selector), birden çok kanalı tek bir iş parçacığında izleyen NIO nesnesidir. Kanallar bloklamayan kipe alınıp seçiciye kaydedilir. select() en az bir kanal hazır olana kadar bekler ve hazır kanal sayısını döndürür.
Seçici Selector.open() ile oluşturulur. Kanal configureBlocking(false) ile bloklamayan kipe alınır ve channel.register(selector, olaylar) ile kaydedilir. Kanalın hangi olayları beklediği SelectionKey sabitleriyle belirlenir (Tablo 8.25).
select() ile dört kanalı izler ve yalnız hazır olanları işler.| Sabit | Anlamı |
|---|---|
OP_READ | kanaldan okuma yapılabilir |
OP_WRITE | kanala yazma yapılabilir |
OP_CONNECT | bağlantı kurma tamamlandı |
OP_ACCEPT | yeni bir bağlantı kabul edilebilir |
Aşağıdaki iskelet bir sunucunun seçici döngüsüdür. Döngü sonsuzdur; program bu yüzden yalnız derlenerek denetlendi.
public class SeciciGoster {
public static void main(String[] args) throws IOException {
ServerSocketChannel kanal = ServerSocketChannel.open();
Selector secici = Selector.open();
kanal.bind(new InetSocketAddress(8080));
kanal.configureBlocking(false);
kanal.register(secici, SelectionKey.OP_ACCEPT);
while (true) {
int hazirKanallar = secici.select();
if (hazirKanallar > 0) {
Set<SelectionKey> secilenler = secici.selectedKeys();
for (SelectionKey secilen : secilenler) {
if (secilen.isAcceptable()) {
// Bağlantı kabul
} else if (secilen.isReadable()) {
// Veri okuma
}
}
secilenler.clear(); // İşlenen anahtarları temizle
}
}
}
}Sunucu kanalı 8080 portuna bağlanır, bloklamayan kipe alınır ve OP_ACCEPT için kaydedilir. Döngüde select() hazır kanal sayısını, selectedKeys() hazır anahtarları verir. İşlenen anahtarlar clear() ile temizlenmezse sonraki turda yeniden gelir. Gerçek bir sunucuda kabul edilen istemci kanalı da OP_READ ile kaydedilir ve veri okunur. İki kural kayıt sırasında denetlenir. Bloklayan bir kanal kaydedilemez; sunucu kanalı da yalnız OP_ACCEPT olayını destekler:
bloklayan kanal kaydı: IllegalBlockingModeException
sunucu kanalında OP_READ: IllegalArgumentException
validOps == OP_ACCEPT: true8.10.9NIO'nun artıları ve eksileri
NIO'nun artıları üç başlıkta toplanır. Başarım: veri bloklar hâlinde aktarılır; büyük dosyalarda klasik G/Ç'den verimlidir. Esneklik: tampon ve kanallarla esnek veri işleme yapılır; kanallar çift yönlüdür ve dosya ile ağ aynı yöntemle işlenir. Çoklu bağlantı: seçiciyle birçok kanal tek iş parçacığında izlenir; yalnız etkin kanallar işlenir.
Eksileri ise karmaşıklıktır. Klasik G/Ç'ye göre kod daha uzun ve daha zordur. Tampon durumu ve yarım okuma, kopan bağlantı gibi hata durumları elle yönetilir. Seçicinin öğrenilmesi de zaman alır, çünkü çok sayıda kanalın durumunu doğru izlemek gerekir. Kural olarak tek bir dosyayı satır satır okumak için BufferedReader ya da Files yeterlidir. Kanallar çok büyük dosyalar ve çok sayıda ağ bağlantısı için öne çıkar.
8.11Örnek problemler ve alıştırmalar
12 ve 345 önce metin, sonra ikili olarak yazılıyor. Program hangi iki sayıyı yazdırır?
try (PrintWriter pw = new PrintWriter("s.txt")) {
pw.print(12);
pw.print(' ');
pw.print(345);
}
System.out.println(new File("s.txt").length());
try (DataOutputStream d = new DataOutputStream(new FileOutputStream("s.dat"))) {
d.writeInt(12);
d.writeInt(345);
}
System.out.println(new File("s.dat").length());PrintWriter sayıları karakterlere çevirir: "12 345", altı karakter. Karakterlerin hepsi ASCII olduğu için dosya 6 bayttır. DataOutputStream ise iki int yazar: 2 · 4 = 8 bayt. Çıktı 6 ve 8'dir. Burada metin kısa çıkmıştır, çünkü sayılar az basamaklıdır.
a.txt başta yok. Program ne yazdırır?
try (FileWriter w = new FileWriter("a.txt")) {
w.write("ab");
}
try (FileWriter w = new FileWriter("a.txt")) {
w.write("cd");
}
try (FileWriter w = new FileWriter("a.txt", true)) {
w.write("ef");
}
try (BufferedReader r = new BufferedReader(new FileReader("a.txt"))) {
System.out.println(r.readLine());
}Birinci FileWriter dosyayı oluşturur ve ab yazar. İkinci FileWriter ekleme kipinde değildir; dosyayı sıfırlar, ab silinir ve cd yazılır. Üçüncüsü true ile sona ekler. Dosyada tek satır vardır ve çıktı cdef'tir.
Program üç satırda ne yazdırır?
ByteBuffer b = ByteBuffer.allocate(10);
b.put((byte) 1).put((byte) 2).put((byte) 3);
b.flip();
b.get();
System.out.println(b.position() + " " + b.limit() + " " + b.remaining());
b.compact();
System.out.println(b.position() + " " + b.limit());
b.clear();
System.out.println(b.position() + " " + b.limit() + " " + b.get(0));Üç put'tan sonra konum 3'tür. flip konumu 0'a, sınırı 3'e çeker. Bir get konumu 1'e ilerletir; kalan 3 − 1 = 2'dir. İlk satır 1 3 2. compact okunmamış 2 ve 3'ü başa taşır; konum kalan sayısı olan 2, sınır kapasite olan 10 olur. İkinci satır 2 10. clear konumu 0'a, sınırı 10'a çeker ama veriyi silmez; get(0) başa taşınmış olan 2'yi verir. Üçüncü satır 0 10 2.
Program ne yazdırır?
try (RandomAccessFile f = new RandomAccessFile("r.dat", "rw")) {
for (int i = 1; i <= 4; i++) f.writeInt(i * 10);
f.seek(4);
int a = f.readInt();
f.seek(f.length() - 4);
int b = f.readInt();
System.out.println(a + " " + b + " " + f.getFilePointer());
}Dosyada dört kayıt vardır: 10, 20, 30, 40; her biri 4 bayt, toplam 16 bayt. seek(4) ikinci kaydın başıdır ve a = 20 olur. length() - 4 = 12, son kaydın başıdır; b = 40. Okumadan sonra işaretçi 16'dadır, yani dosyanın sonunda. Çıktı 20 40 16'dır.
Sunumdaki dört alıştırmanın çözümleri de JDK 21 ile çalıştırılarak doğrulanmıştır. Birincisi, “Java ile dosya / okuma ve yazma / (boş satır) / çok kolay” içeriğindeki metin.txt'nin satır, kelime ve karakter sayısını bulur:
int satirSayisi = 0, kelimeSayisi = 0, karakterSayisi = 0;
try (BufferedReader br = new BufferedReader(new FileReader("metin.txt"))) {
String satir;
while ((satir = br.readLine()) != null) {
satirSayisi++;
karakterSayisi += satir.length();
String temiz = satir.trim();
if (!temiz.isEmpty()) kelimeSayisi += temiz.split("\\s+").length;
}
} catch (IOException e) {
System.err.println("Hata: " + e.getMessage());
}
System.out.println(satirSayisi + " satır, " + kelimeSayisi + " kelime, "
+ karakterSayisi + " karakter");4 satır, 8 kelime, 37 karakterBoş satır için trim() ve isEmpty() denetimi gereklidir. "".split("\\s+") uzunluğu 1 olan bir dizi döndürür; denetim olmasaydı boş satır bir kelime sayılırdı. Karakterler 14 + 14 + 0 + 9 = 37'dir; satır sonlarını readLine attığı için onlar sayılmaz.
İkinci alıştırma klavyeden n okur, 1'den n'ye kadar sayıları ve karelerini kareler.txt'ye yazar, sonra dosyayı okuyup karelerin toplamını bulur:
Scanner klavye = new Scanner(System.in);
try (PrintWriter pw = new PrintWriter(new FileWriter("kareler.txt"))) {
int n = klavye.nextInt();
for (int i = 1; i <= n; i++) {
pw.println(i + " " + i * i);
}
}
int toplam = 0;
try (Scanner dosya = new Scanner(new File("kareler.txt"))) {
while (dosya.hasNextInt()) {
dosya.nextInt(); // i
toplam += dosya.nextInt(); // i * i
}
}
System.out.println("Kareler toplamı: " + toplam);Kareler toplamı: 55Yazma bloğu kapandıktan sonra okuma başlar; aksi hâlde veri tamponda kalır ve dosya boş okunurdu. n = 5 için 1 + 4 + 9 + 16 + 25 = 55. Üçüncü alıştırma bir int dizisini serileştirir ve geri okur. Diziler serileştirilebilir:
int[] dizi = {3, 1, 4, 1, 5};
try (ObjectOutputStream c = new ObjectOutputStream(new FileOutputStream("dizi.ser"))) {
c.writeObject(dizi);
}
try (ObjectInputStream g = new ObjectInputStream(new FileInputStream("dizi.ser"))) {
int[] okunan = (int[]) g.readObject();
int toplam = 0;
for (int x : okunan) toplam += x;
System.out.println(okunan.length + " eleman, toplam " + toplam);
}5 eleman, toplam 14Dördüncü alıştırmada beş not RandomAccessFile ile int olarak yazılır ve verilen sıra numarasındaki not dosyanın tamamı okunmadan bulunur: f.seek((sira - 1) * 4); int not = f.readInt();. Üçüncü notun konumu 8'dir. Aynı yaklaşım 10 000 satırlık bir kayıt dosyasının son kaydı için de geçerlidir, ama yalnız kayıtlar sabit boyluysa: seek((n - 1) * boy). Satır boyları farklıysa konum hesaplanamaz. Ya dosya baştan sıralı okunur ya da sondan geriye doğru \n aranır.
8.12Sık yapılan hatalar
Bölüm boyunca yer yer anılan hatalar, derleyicinin ya da JVM'in gerçek iletileriyle birlikte burada toplanır.
Kontrol edilen istisnayı ele almamak. new FileReader("a.txt") FileNotFoundException, readLine() IOException fırlatabilir. Kod try-catch içinde olmalı ya da metot throws IOException demelidir:
Ana.java:5: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
BufferedReader r = new BufferedReader(new FileReader("a.txt"));
^
Ana.java:6: error: unreported exception IOException; must be caught or declared to be thrown
String s = r.readLine();
^
2 errorsimport'u unutmak. Scanner java.util'de, G/Ç sınıfları java.io'dadır. import yazılmazsa derleyici sınıfı bulamaz:
Ana.java:3: error: cannot find symbol
Scanner sc = new Scanner(System.in);
^
symbol: class Scanner
location: class Ana
Ana.java:3: error: cannot find symbol
Scanner sc = new Scanner(System.in);
^
symbol: class Scanner
location: class Ana
2 errorsÖteki hataların her biri ilgili kesimde gerçek çıktısıyla gösterildi: Windows yolunda tek ters bölü yazmak, finally'de null denetimi yapmamak, nextInt'ten sonra nextLine kullanmak, tamponlu yazıcıyı kapatmamak, flip()'i ya da Serializable'ı unutmak, read()'in sonucunu byte'ta tutmak ve blok kopyada write(parca) yazmak. Bunların çoğunda program derlenir ve çalışır, ama yanlış sonuç verir. Bu yüzden G/Ç kodu yazıldıktan sonra kopyanın boyu, okunan değerler ve dosyanın gerçek içeriği mutlaka denetlenir.
8.13Özet
- Akış, veriyi kaynaktan hedefe sırayla taşıyan yapıdır. Her Java programının üç standart akışı vardır:
System.in,System.outveSystem.err. Çıktı yönlendirildiğindeSystem.errekranda kalır. Scannerklavyeyi, dosyaları ve dizgileri okur.nextInt()satır sonunu tamponda bırakır; ardından gelennextLine()boş dizgi döndürür.- Her dosya bir bayt dizisidir. Metin dosyasında baytlar bir karakter kodlamasıyla harflere çevrilir; ikili dosyada sayı, piksel ya da komut olarak yorumlanır.
- ASCII 7 bit, ISO-8859-9 Türkçe için 8 bittir; UTF-8 bir karakteri 1–4 baytla yazar ve Java 18'den beri varsayılandır. Aynı dosya yazıldığı kodlamayla okunmalıdır.
- 127 metin olarak 3 bayt,
intolarak 4 bayttır. İkili boyut tipe, metin boyutu basamak sayısına bağlıdır. İkili veri yazıldığı sırayla ve tiple geri okunur. Fileyalnız bir yoldur, içeriği okumaz. Metotlarının çoğu hatayıfalsedöndürerek bildirir.- Bayt akışları (
InputStream/OutputStream) her tür veri, karakter akışları (Reader/Writer) metin içindir. Sarmalayıcı akışlar yetenek ekler; en dıştakini kapatmak hepsini kapatır. readLine()dosya sonundanull,read()−1 döndürür.read()'in sonucuint'te tutulur.FileWriter(ad, true)sona ekler; parametresiz hâli dosyayı sıfırlar.PrintWriteristisna fırlatmaz, hatacheckError()ile görülür.- Tamponlama disk erişimlerini toplu yapar. Yazılan veri
flush()ya daclose()'a kadar bellekte bekleyebilir. - try-with-resources kaynakları açılış sırasının tersiyle ve
catch'ten önce kapatır. - Blok kopyada yalnız okunan
nbayt yazılır:write(parca, 0, n). Kısa yollartransferToveFiles.copy'dir. RandomAccessFileseekile istenen konuma atlar; sabit boylu kayıtlarda k. kaydın konumu k · kayıt boyudur.- Serileştirme nesneyi baytlara çevirir. Sınıf
Serializable'ı uygular;staticvetransientalanlar yazılmaz; geri okumada yapıcı çağrılmaz. - NIO tampon, kanal ve seçiciyle çalışır. Tampona yazdıktan sonra okumadan önce
flip(), yeniden yazmadan önceclear()ya dacompact()çağrılır.
8.14Kavram listesi
- akış stream
- standart girdi / çıktı standard input / output
- metin dosyası / ikili dosya text file / binary file
- karakter kodlaması character encoding
- bayt akışı / karakter akışı byte stream / character stream
- sarmalayıcı akış decorator
- tampon / tamponlama buffer / buffering
- try-with-resources
- kontrol edilen istisna checked exception
- rastgele erişimli dosya random access file
- serileştirme serialization
- kanal channel
- seçici selector
- yerel ayar locale
- iş parçacığı thread
8.15Kendini Dene
"Ilık"kelimesi UTF-8 ile ve ISO-8859-9 ile kaç bayt tutar?Cevap
UTF-8'de
I,lvekbirer,ıiki bayttır: 5. ISO-8859-9'da her harf bir bayttır: 4.Bir
PrintWriterileprint(5L),print('A'),print(1.5f); birDataOutputStreamilewriteLong(5),writeChar('A'),writeFloat(1.5f)yapılıyor. İki dosya kaç bayttır?Cevap
Metin dosyasında
5A1.5yazar: 5 bayt. İkili dosyadalong8,char2,float4 bayttır: 14 bayt.print(1.5f)yerel ayardan bağımsızdır ve nokta yazar.10 000 baytlık bir dosya 4096 baytlık bir diziyle
read(dizi)çağrılarak okunuyor. Çağrılar hangi değerleri döndürür? Kopyadawrite(dizi)kullanılsaydı kopya kaç bayt olurdu?Cevap
4096, 4096, 1808 ve dosya sonunda −1.
write(dizi)her turda dizinin tamamını yazar: 3 · 4096 = 12 288 bayt. Son turda 2288 eski bayt fazladan yazılır.ByteBuffer t = ByteBuffer.allocate(5);sonraputile 1, 2, 3 yazılıyor;flip(), ikiget(),compact(),put((byte) 9)ve yenidenflip()yapılıyor. Konum, sınır, kalan ve 0 ile 1 numaralı hücreler nedir?Cevap
İki
get'ten sonra okunmamış tek öğe 3'tür.compactonu başa taşır: konum 1, sınır 5.put(9)1 numaralı hücreye yazar, konum 2 olur.flipsınırı 2'ye, konumu 0'a çeker. Sonuç: konum 0, sınır 2, kalan 2, hücreler 3 ve 9.Boş bir dosyaya
RandomAccessFileile 7, 8, 9intolarak yazılıyor. Sonraseek(4); writeInt(100); seek(length()); writeShort(1);yapılıyor. Dosyanın uzunluğu nedir veseek(0)'dan sonrareadShort()ne döndürür?Cevap
Üç
int12 bayttır;writeInt(100)8'in yerine yazılır ve uzunluk değişmez.writeShortsona 2 bayt ekler: 14.readShort()ilk iki baytı okur. 7'nin baytları00 00 00 07olduğu için sonuç 0'dır.Yapıcısı “kuruldu” yazan,
transient String not = "geçici";veint n = 5;alanları olan serileştirilebilir bir sınıfın nesnesi oluşturuluyor,n8 yapılıyor, nesne yazılıp geri okunuyor. “kuruldu” kaç kez yazılır ve okunan nesnedenotilennedir?Cevap
Bir kez;
readObjectyapıcıyı çağırmaz.notnull'dır, çünkütransientalan yazılmaz ve alan ilklendiricisi de çalışmaz.n8'dir.try (R a = new R("A"); R b = new R("B"); R c = new R("C")) { … }bloğu istisnasız biter;Raçılırken ve kapanırken ileti yazıyor. İletilerin sırası nedir?Cevap
A açıldı, B açıldı, C açıldı, gövde, C kapandı, B kapandı, A kapandı. Kaynaklar soldan sağa açılır ve ters sırayla kapanır.
3 MB'lık bir dosya varsayılan
BufferedInputStreamile bayt bayt okunuyor. AlttakiFileInputStream'den kaç okuma yapılır?Cevap
3 · 1 048 576 / 8192 = 384 veri taşıyan okuma ve dosya sonu için bir okuma: 385.
8.16İleri okuma
- Oracle, The Java Tutorials, “Basic I/O”: akışları,
Scannerve biçimlendirmeyi, NIO.2 ile dosya işlemlerini kısa örneklerle anlatan resmî ve ücretsiz kaynaktır. - C. S. Horstmann, Core Java, Volume II: Advanced Features (12. baskı), Bölüm 2 “Input and Output”: akış sınıflarını, karakter kodlamalarını, serileştirmeyi ve bellek eşlemeli dosyaları ayrıntılı işler.
- Oracle, Java Object Serialization Specification: serileştirilmiş baytların biçimini,
serialVersionUID'nin nasıl hesaplandığını ve sürüm uyumluluğu kurallarını tanımlar. - OpenJDK, JEP 400 “UTF-8 by Default”: Java 18'de varsayılan kodlamanın neden UTF-8 yapıldığını ve eski programlara etkisini açıklar.
8.17Kaynaklar
- Oracle, The Java Tutorials: “Basic I/O” (I/O Streams, File I/O featuring NIO.2), “Scanning and Formatting”.
- Oracle, Java SE 21 API Documentation:
java.io,java.nio,java.nio.channels,java.nio.filepaketleri. - Oracle, Java Object Serialization Specification.
- J. Gosling ve diğerleri, The Java Language Specification, Java SE 21 Edition (3.10.6 kaçış dizileri, 14.20.3 try-with-resources).
- P. Deitel ve H. Deitel, Java: How to Program, Early Objects, 11. baskı, Pearson, 2017 (Bölüm 15: Files, Input/Output Streams, NIO and XML Serialization).
- C. S. Horstmann, Core Java, Volume II: Advanced Features, 12. baskı, 2022 (Bölüm 2: Input and Output).
- K. Sierra ve B. Bates, Head First Java, 3. baskı, O'Reilly, 2022 (Bölüm 16: Saving Objects (and Text)).
- A. B. Downey ve C. Mayfield, Think Java, 2. baskı, O'Reilly, 2019.
- OpenJDK, JEP 400: UTF-8 by Default, JDK 18, 2022. https://openjdk.org/jeps/400
İlgili sayfalar: Bölüm 8 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.