I/O (INPUT — OUTPUT)
Java IO API, Java’da çok önemli API’lerden birisidir. Basitçe I/O için, kullanıcıların girdiyi almasına ve bu girdiye göre çıktı üretmesine yardımcı olduğunu söyleyebiliriz. Girdiyi işlemek ve çıktıyı üretmek için Java IO kullanılır. java.io paketi input ve output işlemleri için gerekli olan tüm sınıfları içerir. Ayrıca java io ile Java’da dosya işlemleri de yapmak mümkündür. Java IO paketi genellikle bir kaynaktan temel bilgilerin okunmasını ve bir hedefe yazılmasını içerir.
“File Class” ı
Bir File class’ı file/directory ‘nin özelliklerini elde etmek için kullanılan methodları içerir. Ayrıca renaming ve deleting methodlarını da barındırır.
NOT: File Class’ı file’ın contentiyle alakalı bir method içermez. Yani File classını kullanarak file’ın içeriğine data yazamayız. İçindeki datayı okuyamayız.
Bir program içerisinde data storage etmek geçicidir. Bilgisayar ya da program herhangi bir sebeple sonlandığında bilgiler de kaybolur. Bilgileri kalıcı bir şekilde depolamak istersek o zaman bilgisayarın diskine ya da öbür device’lara kaydetme işlemini yapmak gerekir. Data storage etmenin yollarından birisi de file’dır ve bir file daha sonra taşınabilir ve diğer programlar tarafından okunabilir.
Her file, file systemde bir directory içerisinde yer alır. Burada da bilmemiz gereken 2 farklı kavram ile karşılaşırız.
Absolute Path: İlgili file’ın full path’ini bize verir. Bşr absolute path “file name”, “complete path”, “Drive” içerir. Örn;
Windows makineler için C:\book\Welcome.java
C:\book burada file için(Örneğimizdeki Welcome.java) directory path’e karşılık gelir.
Relative Path: İlgili working fike ile lişikilidir.
Absolute Path’de verdiğimiz örnek için Welcome.java bizim için Relative Path’dir.
NOT: Pathlerle ilişkili bir kod yazıyorsak bu kodları generic yazmalıyız. Ya da mümkün mertebe Absoulte Path kullanmamalıyız. Çünkü, path kavramı işletim sistemine özgü olduğu için çalışmama ihtimali yüksek.
String pathName = "Deneme.txt";
File fileInstance = new File(pathName);
Yukarıdaki kod bloku basit bir File işlemi içermektedir. Verilen bir pathname’e göre file ile alakalı bazı bilgileri edinebiliriz. Bu bilgilerden bazıları için aşağıdaki methodlar yeterli ip uçları verecektir.
- exist() : İlgili path’de file olup olmadığını kontrol eder.
- createNewFile() : İlgili / working / current directoryde yeni bir file oluşturur.
- length() : File’ın uzunluğunu verir.
- canRead() : File’ın okunabilirliğini kontrol eder.
- canWrite() : File’ın yazılabilirliğini kontrol eder.
- isDirectory() : Verilen path’in bir directory’ye ait olup olmadığını kontrol eder.
- isFile() : Verilen path’in bir file olup olmadığını kontrol eder.
- getAbsolutePath() : File’in absolute path’ini verir.
- lastModified() : Tarih ve Zaman şeklinde file’ın son modifiye edildiği zamanı verir.
Bu cümleyi tekrar hatırlamamız gerekecek o yüzden lütfen dikkatli okuyalım. (:
File’lar “text” ve “binary” olarak sınıflandırılabilir. Bir text editörü tarafından (NotePad, Word..) işlenmiş bir file text olarak adlandırılabilir. Bunun dışındaki “file”lar binary “file”lardır. Binary “file”lar text editörü tarafından okunamaz. Bu “file”lar programdan tarafından okunabilmek ya da yazılabilmek için özel olarak tasarlanmışlardır.
Örn; Java Source Code text filedır ve text editörü tarafından okunabilir. Fakat .class uzantılı bir file binary filedır ve JVM tarafından okunur.
FILE IO
- PrintWriter
Bir file’a text datası yazmak için kullanılır. Bir file oluşturmak için de kullanılabilir.
PrintWriter classının instance’ı üzerinden herhangi bir method çağırımı yapmadan sadece consturctor’ına bir file instance’ı vererek bir file oluşturabiliriz.
Eğer file mevcutsa, içerik kullanıcı/programcı tarafından doğrulanmadan silinir.
İkinci bir hatırlatma ise, açılan PrintWriter instance’ı dosya yazma ya da oluşturma işlemi bittikten sonra close() methodu ile kapatılmalıdır. Çünkü veriler düzgün bir şekilde kaydolmayabilir.
Aşağıdaki örnekte PrintWriter ile bir File’a nasıl dosya yazılabileceğini görebilirsiniz. Fakat close() methodu yok. PrintWriter Autoclosable interface’ini gerçekler. Bu yüzden try — with — resources kavramıyla close() methodunu çağırmamıza gerek kalmaz.
- Scanner
Konsoldan primitive value’ları ve String değerleri okumak için kullanılır.
Scanner scannerInstance = new Scanner(...);
şeklinde kullanılılır. Boş constructor’u yoktur. Bu yüzden buraya muhakkak constructor için gerekli değerler girilmelidir.
Burada bahsedilmesi gereken önemli bir husus vardır. Bir Scanner açıldığında close() methodu ile kapatmak must değildir ama kapatılması iyi bir practice olabilir. Çünkü, dosyanın ya da scanner nesnesinin kapladığı ya da kullandığı kaynakları serbest bırakmak faydalı olacaktır.
scannerInstance.close();
Yukarıdaki kod bloğunda var olan bir pathteki file’ı Scanner ile okuma kodu verilmiştir. Bu kod bloğundaki close() methoduna bir parantez açmak gerekirse, aynı PrintWriter classında olduğu gibi Scanner class’ı da Autoclosable interface’ini uygular. Bu yüzden yukarıdaki kod bloku da try — with — resources kavramı kullanılarak yazılabilir.
Scanner Nasıl Çalışır?
Scanner classının çalışma mantığını anlamak için 2 tane kavramı iyi anlamak gerekir.
Token-Based Input: Delimiter karaktere (sınırlayıcı karakter) kadar okunan girdilier token-based input olarak adlandırılır. Java’da bu karakter default olarak whitespace yani boşluk karakteridir. next(), nextByte(), nextShort(), nextInt(), nextLong(), nextFloat(), nextDouble() gibi methodlar aslında token-based inputlar için geliştirilmiş, token-based inputların okunması için tasarlanmış methodlardır. Bu methodlar konsoldan girilen bütün satırı okumak yerine belirlenmiş karakterlere kadar (delimiter) okurlar.
Line-Based Input: Tüm satırın okunduğu girdilerdir. nextLine() methodu bir line-based input methodudur.
Tam da bu noktada bir kavramdan daha bahsetmek faydalı olacaktır. Bı kavram da line seperator kavramıdır.
Yukarıdaki kodun çıktısı şöyledir.
Line Seperator :
(whitespace)
13
10
(whitespace)Not: (whitespace)ler boşluk karakterleridir.
Line Seperator satır sonuna kadar gelinen ve satır sonuna geldiğinde input tipine göre davranan bir yapıdır. (Kafalar burada biraz karışabilir. (: ) Kodda forEach döngüsü içerisinde 13 ve 10 değerlerini gördük çünkü \r\n karakterleri default olarak gelir ve bu karakterler line seperator yapmamızı sağlar. 13 ve 10 bu değerlerin integer karşılıklarıdır. Aslında aşağıdaki kod bloğunun çıktısı bu konunun kafamıza tam olarak oturmasına yardımcı olabilir.
Yukarıdaki kodda debug ile geldiğimiz zaman alt satıra geçiş karakterleriye birlikte lineSeperator kullanımını görüyoruz. Debug yaptığımız zaman “str” instance’ı içerisinde değerler geldi. Peki ya output.?
Bu gariplikte bir iş var gibi. (: Debug’da görmemize rağmen “two” değerini outputta göremedik. Çünkü bu line seperator’ün çalışmasıyla alakalı. Ki yazının ilerleyen kısımlarında bu konuya değineceğiz. Ama öncesinde ufak bir reklam verip delimiter’ları değiştirmeye ne dersiniz.? Bu yazı için araştırma yaparken öğrendim ve buraya da yazmak istedim. (: Java’da delimiter karakteri System tarafından otomatik atanır. Ama biz bu delimiter’i değiştirebiliriz. Bu konuyla alakalı önce bir örnek verip başlayalım.
1. Scanner getData = new Scanner(System.in);
2. String s = getData.next();
3. System.out.println(s);
Yukarıda çok basit bir kod var. Konsoldan veri istiyoruz ve aldığımız değeri ekrana yazıyoruz. Eğer kullanıcı burada 2. satırda konsoldan değer girerken birden fazla whitespace karakteri içeren bir cümle girerse (Örn; Turkey Java Community) 3. satırda sadece Turkey değerini görür. Çünkü next() methodu token-based bir methoddur ve sadece delimiter’e kadar okur. Satırın tamamını okuması için nextLine() methodunu kullanması lazım.
Yukarıdaki kod bloğunda delimiter olarak -> değerini verdik. Scanner classından ürettiğimiz instance üzerinden useDelimiter methoduyla delimier’i değiştirebiliriz. Delimiter’den oluşan değerleri hasNext() methoduyla dolaşıp değerlerimizi teker teker ekrana yazabiliriz. Yukarıdaki kod bloğunun çıktısı aşağıdaki gibi olacaktır.
(Furkan, 28)
(Alperen, 23)
(Ahmet, 17)
Şimdi reklamı burada bitirip tekrardan scanner nasıl çalışır sorusuna geri dönüş yapalım.
Bir token-based input öncelikle bütün delimiterleri atlar. Sonrasında sırasıyla bütün token’ı okur. Sonra bu token otomatik olarak sırasıyla byte, short, int, long, float, ve double değerlerinin bunları okuyacak olan methodlarına çevrilir. Bu işlem next() methodu için yapılmaz. Eğer token ile expected value match olmazsa;
Runtime’da InputMissMatchException hatası fırlatır.
Token-based input methodları delimiter’dan sonra gelen tokenları okumazlar. Eğer nextLine() methodu token-based input methodundan sonra çağrılırsa bu method, bu delimiterden başlayan ve line seperator ile biten karakterleri okur.
Bu açıklamayı kod üzerinde görelim.
Elimizde bir tane txt dosyası olsun. İçerisinde 34 ve 567 değerleri var olsun.
Bu kodun çıktısı aşağıdaki gibidir ama lütfen altı çizili alandaki boşluk karakterine dikkat edin.
Şimdi aynı kodu dosyadan okumak yerine standart System.in üzerinden yapıp sonuçları karşılaştıralım.
Sonuç olarak bu kod ile bir önceki kod arasında kağıt üzerinde fark gözükmemesine rağmen çıktıyı inceleyecek olursak;
Peki burada ne oldu.? Neden böyle bir sonuç aldık.?
Token-based input olan nextInt() methodu 34'ü okur ve bu durumda bir line seperator olan enter’da durur. nextLine() methodu line seperatör’ü okuduktan sonra sona erer ve line seperator den önce okunan String’i döndürür. Line seperatorden önce karakter olmadığı için satır boştur.
Bu nedenle token-based inputtan sonra line-based input kullanmamalıyız.
IO çok önemli bir konu. Ve çok geniş kapsamlı. Bu yüzden bu yazı en az iki seri olacak. Okuduğunuz için teşekkürler. Yapıcı eleştrilere her zaman açığım tabiiki. (:
İletişim, İşbirlikleri ve Teklifler için,
github: https://github.com/fsk
twitter: https://twitter.com/frknshnklksz
mail: fsk@fskdev.co
linkedIn: https://www.linkedin.com/in/frknshnklksz/
bu yazıdaki kodlar: https://bitbucket.org/furkandev/io/src/master/