23 Kasım 2011 Çarşamba

Düzenli Deyimler

Bir hesaplama sürecine girdi sağlarken, kimi zaman farklı girdilerin aynı anlama geldiğini veya farklı da olsalar benzer anlamlar taşıdığını söylemek isteriz. Örneğin, bir işleme devam etmek isteyip istemediğimizin sorulması durumunda, "Hayır" yerine "H" veya "h"nin de işi görmesi bize zamandan kazandıracak ve—"hayır" yerine "hıyar" yazdığınızı düşünün—hata oranını azaltacaktır. İşte tam bu noktada, bir grup karakter katarını sahip oldukları içeriğin ortak özelliklerini temel alarak betimleyen özel bir çeşit karakter katarı şeklinde tanımlayabileceğimiz düzenli deyimler işin içine girer. Bu yazımızda da yapacağımız, Java'da düzenli deyimler için sağlanan desteğe bakmak olacak.

Temel Kullanım


Java'da düzenli deyim desteği java.util.regex paketindeki türler vasıtasıyla sağlanır. Bu türlerin sunduğu işlevsellikten yararlanarak, olası girdileri betimleyen düzenli deyimin, programın çalıştırılması sırasında sağlanan asıl girdi ile eşleşip eşleşmediği kontrol edilir. Bunun için yapılması gereken, düzenli deyim ile girdinin argüman olarak geçirildiği Pattern.matches metodunun çağrılmasıdır. Aşağıdaki kod parçasından da görülebileceği gibi, düzenli deyimin String olması beklenirken, girdinin CharSequence arayüzünü destekleyen herhangi bir sınıftan olması mümkündür.
import java.util.regex.Pattern;
...
String düzenliDeyim = ...;
String girdi1 = ...;
boolean tanındıMı = Pattern.matches(düzenliDeyim, girdi1);
...
StringBuilder girdi2 = new StringBuilder("...");
tanındıMı = Pattern.matches(düzenliDeyim, girdi2);
matches metodunun iki özelliği bizi ikinci bir yol aramaya sevkedecektir. Öncelikle, döndürülen boolean değer, kullanıcının sağladığı girdinin düzenli deyimle uyumlu olup olmadığı konusunda fikir verir; girdinin yapısı hakkında herhangi bir bilgi edinmek olanaksızdır. Ayrıca, matches metodu, kendisine geçirilen düzenli deyimi içsel bir gösterime çevirdikten sonra girdinin uyumunu denetler. Bu ise, aynı düzenli deyimin birden çok kez kullanılması durumunda, içsel gösterime dönüşümün tekrar tekrar yapılmasıyla zaman kaybetmek anlamına gelir ki, çözüm Pattern.compile metodunun kullanımından geçer. Bu metot, kendisine geçirilen düzenli deyimi dönüştürür ve dönüşümün sonucunu tutan desen nesnesinin tutacağını döndürür.1 İstenecek olursa, bu tutacak aracılığıyla gönderilecek iletiler, girdinin düzenli deyimle uyuşması sonrasında girdinin bileşenlerine ilişkin sorgularımızı yanıtlayabilir.
import java.util.regex.*;
...
String düzenliDeyim = ...;
Pattern desen = Pattern.compile(düzenliDeyim);
String girdi1 = ...;
Matcher eşleştirci = desen.matcher(girdi1);
boolean tanındıMı = eşleştirici.matches();
if (tanındıMı) ... // girdi1'in bileşenlerini keşfet
...
StringBuilder girdi2 = new StringBuilder(...);
eşleştirici = desen.matcher(girdi1);
tanındıMı = eşleştirici.matches();
if (tanındıMı) ... // girdi2'nin bileşenlerini keşfet

Düzenli Deyim İçeriği


Düzenli deyim tanımında, betimlenmekte olan girdinin karakterleri ile birlikte bazı özel işleçler kullanılabilir. İstenecek olursa, düzenli deyim ile girdi arasındaki eşlemenin nasıl yapıldığını görebilmek adına düzenli deyim öbek adı verilen parçalara ayrılabilir. Ayrıca, eşleştirme sürecinin büyük/küçük harf ayrımı ve dönüşümü, satır ayırma gibi konularda nasıl davranacağını belirleyen bayraklardan da yararlanılabilir.

Düzenli deyim içeriğini oluşturan karakterlerin bazıları, tıpkı karakter sabitlerinde olduğu gibi, özel bir biçimde yorumlanır. Örneğin, '(' yeni bir öbek başlatırken ')' en son başlatılan öbeği kapatır. Dolayısıyla, bu karakterleri [ve diğerlerini] özel görevleri dışında sade bir karakter olarak görmek istediğimizde niyetimizi söz konusu karakterin önüne '\' koyarak belirtmemiz gerekir. Buna ek olarak, bazı sıradan karakterler, önlerine '\' konulmak suretiyle, tıpkı 'n' karakterinin '\n' haline getirilmesinde olduğu gibi, özel bir anlam kazanır. Mesela, 'd' alfabedeki bir harfe karşılık gelen karakteri temsil ederken, '\d' ondalık sayıları yazmakta kullanılan herhangi bir rakamı temsil eden karakter grubuna karşılık gelir. Dolayısıyla, "\\{\\d\\}" yegâne elemanı tek basamaklı bir ondalık sayı olan kümeleri betimler.
System.out.print(Pattern.matches("\\{\\d\\}", "{3}")); // ⇒ true
'\' karakterlerinin çokluğu başınızı döndürdü değil mi? Bunun nedeni, karakter katarımızın bir kez String sabiti, bir kez de düzenli deyim olarak yorumlanmasıdır. Yani, yukarıdaki komutun ilk argümanı önce String sabiti gibi yorumlanacak ve "\{\d\}" haline dönüştürüldükten sonra düzenli deyim olarak ele alınacaktır. Bu sıkıcı durum, alıntılama düzeneği ile biraz olsun düzeltilebilir. Düzenli deyimin içinde geçen \Q (İng., quote), \E (İng., end quote) görülene kadar hiçbir şeyin yorumlanmayacağını bildirir.2 İstenecek olursa, kendisine geçirilen String nesneyi \Q ve \E ile çevreleyen Pattern.quote metodu da aynı amaçla kullanılabilir.

Tek bir karmaşık sayı içeren kümeyi betimleyen düzenli deyim aşağıda verilmiştir. Dikkat ederseniz, düzenli deyimin baş ve son tarafındaki yorumlanmayan parçaların sınırlarını belirleyen \Q ve \E yorumlanmakta olup, sırasıyla, \\Q ve \\E şeklinde yazılmak zorundadır.
import static java.lang.System.out;
import static java.util.regex.Pattern.*;
...
String desen ="\\d\\s*(\\+|-)\\s*\\d";
out.print(
  matches("\\Q{(\\E" + desen + "\\Q)}\\E", "{(3 -5i)}")); // ⇒ true
out.print(
  matches(quote("{(") + desen + quote(")}"), "{(3+4i)}")); // ⇒ true

Karakter Sınıfları


Düzenli deyim oluştururken, karakterlerin yanısıra ortak özellikleri bulunan karakterleri gruplayan karakter sınıflarını da kullanabiliriz. Örneğin, Java'da kullanılabilecek tanımlayıcı adlarını betimlemek istediğimizde, _ ve tüm alfabetik karakterlerin ilk karakter olarak geçebileceğini söyleyerek bu karakterleri aynı sınıfa koymuş oluyoruz. Bu noktada, karakter sınıflarının bir katarı değil tek bir karakteri tanımladığı unutulmamalıdır. Dolayısıyla, tek rakamları betimleyen bir karakter sınıfının düzenli deyim tanımında kullanılması tek rakamlardan oluşan çok basamaklı bir sayının değil, {1', '3', '5', '7', '9'} kümesinin bir tek elemanının kullanılabileceği anlamını taşır.

Köşeli ayraç çifti ile sınırlanan karakterler ve diğer karakter sınıflarından oluşan karakter sınıfları, içerilen karakterlerin yanyana yazılmasının yanısıra, tümleyen sınıfın dışlanması ile de tanımlanabilir. Örneğin, "[13579]" '1', '3', '5', '7' ve '9' karakterlerinden oluşan bir karakter sınıfını tanımlarken, "[^02468]" '0', '2', '4', '6' ve '8' karakterleri dışındaki tüm karakterleri kapsar. Bu noktada, ikinci örneğimizin sadece tek rakamları kapsamadığını, listelenenler dışındaki herhangi bir karakteri kapsadığının altını çizelim. Buna karşılık, rakamların ve çift sayı olmayan karakterlerin kesişimini alan "[0-9&&[^02468]]" deyimi, tümleme işleci (^) ve kesişim işlecinden (&&) yararlanarak ilk örneğimizle eşdeğer bir sonuç verir.

Betimlenen karakter sınıfındaki karakterlerin ardışık olması halinde, tüm karakterleri teker teker yazmaktansa aralık işlecini (-) kullanabiliriz. Buna göre, "[abcçde12345]" yerine "[a-eç1-5]" yazmak yeterli olacaktır. [Dikkat ederseniz, aralık tanımının ASCII temelli olması nedeniyle Türkçe'ye özel 'ç' ayrıca eklenmek zorunda.] Bunun bir sonucu olarak—karakterler arasında kullanıldığında aralık işleci görevini gördüğü için—'-' sınıf içinde ancak ve ancak birinci sırada geçebilir. Dolayısıyla, temel aritmetik işleçlerin tanımı "[-+*/]", bu sınıfın tümleyeni "[^-/*+]" şeklinde yapılmalıdır.

Sıklıkla kullanılan bazı karakter sınıfları önceden tanımlanmışlardır. Söz konusu karakter sınıflarının yeniden tanımlanması hem zamandan kayıp hem de hataya açık bir çabadır. Ancak, bu karakter sınıflarından bazılarının ASCII temelli tanımlandığı ve özel ayarlamalar yapılmadığı müddetçe alfabemizdeki kimi harfleri barındırmayacağı akılda tutulmalıdır.
  • .: Unicode tablosundaki herhangi bir karakter.
  • \p{ASCII} ([\x00-\x7F]): Unicode tablosunun ASCII altkümesinde bulunan karakterler.
  • \p{Alpha} ([a-zA-Z]): ASCII tablosundaki alfabetik karakterler. \p{Lower} ve \p{Upper}, sırasıyla, küçük ve büyük alfabetik karakterleri tanımlar.
  • \p{Digit} veya \d ([0-9]): Ondalık sayıların basamaklarında kullanılabilecek rakamlar. 16'lı tabandaki sayıların basamakları \p{XDigit} ile tanımlanır. Ondalık sayıların tümleyeni olan sınıf—yani, ondalık rakam olmayan karakterler—\D ile temsil edilir.
  • \p{Alnum} ([\p{Alpha}\p{Digit}]): Alfanümerik karakterler.
  • \w ([a-zA-Z0-9_]): Çoğu programlama dili tarafından tanımlayıcı adı oluşturmakta kullanılan karakterler. Tümleyen sınıf \W ile temsil edilir.
  • \p{Punct}: Noktalama imleri, ayırıcılar ve işleçler.
  • \p{Graph} ([\p{Alnum}\p{Punct}]): Fiziksel gösterimi bulunan karakterler.
  • \p{Print} ([\p{Graph}\x20]): Basılabilir karakterler. [\x20 boşluk karakterine karşılık geliyor.]
  • \p{Cntrl} ([\x00-\x1F\x7F]): Kontrol karakterleri.
  • \p{Space} veya \s ([ \t\n\x0B\f\r]): Bir metnin görünümünü düzenlemek için yararlanılan beyaz boşluk karakterleri.
  • \p{Blank} ([ \t]): Boşluk ve sekme karakterleri.

Yukarıdaki karakter sınıflarından ASCII tablosuna sınırlı olanların davranışı UNICODE_CHARACTER_CLASS bayrağı kullanılarak değiştirilebilir. Buna göre; "\p{Lower}" ile "ç" eşleşmezken, "(?U)\p{Lower}" ile "ç" eşleşecektir. Aynı etkiyi, Pattern.compile metoduna ikinci argüman olarak geçirilen bayraklar arasına UNICODE_CHARACTER_CLASS sabitini koyarak da yaratabiliriz.
import java.util.regex.Pattern;
import static java.util.regex.Pattern.*;
...
Pattern küçükHarf = compile("(?U)\\p{Lower}");
küçükHarf = compile("\\p{Lower}", UNICODE_CHARACTER_CLASS)); // Yukarıdakiyle aynı
Unicode tablosunu kullanmanın bir diğer yolu, Character sınıfındaki yüklemler vasıtasıyla işini gören karakter sınıflarından yararlanmaktır. Bu karakter sınıflarının adları, Character sınıfının ilişkin yüklemindeki is öneki yerine java konulmasıyla oluşturulur. Örnek olarak, argümanının birbirini tamamlayan karakter çiftlerinden ((), [], {}, <>, vd.) birine ait olup olmadığını denetleyen isMirrored yüklemini ele alalım. Düzenli deyimimizdeki bir karakterin bu tür bir karakter ile eşleşmesini istediğimizde yapmamız gereken, isMirrored'ı javaMirrored'a çevirmek ve karakter sınıfı adı olarak kullanmaktır. Dolayısıyla, "\p{javaMirrored}" ">" veya "(" ile eşleşirken çift olarak gelmeyen diğer karakterlerle eşleşmeyecektir.
küçükHarf = compile("\\p{javaLowerCase}"); // Yukarıdakilerle aynı
Pattern çifttenBiri = compile("\\p{javaMirrored}");

Düzenli Deyim İşleçleri


Temel düzenli deyim oluşturma işleci olan bitiştirme, iki karakterin [veya karakter öbeğinin] yanyana yazılmasıyla ifade edilip özel bir simgenin kullanımını gerektirmezken, geçiş sayısını belirtmek amacıyla farklı niceleme işleçlerinden yararlanılabilir. Karakteri takiben yazılan bu işleçlerden joker grubu olarak adlandırabileceklerimiz *, + ve ?, sırasıyla, 0 veya daha fazla sayıda, 1 veya daha fazla sayıda ve 0 veya 1 kez anlamına gelir. Yineleme sayısının kesin olması durumunda, kıvrımlı ayraç çifti ({}) arasında yazılacak bir sayı işi görecektir; yineleme sayısının alt ve üstten sınırlandırılması ise virgülle ayrılmış sınırların kıvrımlı ayraç çifti arasında verilmesiyle mümkün olurken, üst sınırın yazılmaması yinelemenin alt sınırdan az olmamak üzere belirsiz bir sayıda olabileceği anlamına gelir.

Yineleme işleçlerinden +, * ve bitiştirmenin birlikte kullanımına denk olduğu için işlevsellik adına bir şey katmaz. Örneğin "a+" "aa*" şeklinde ifade edilebilir. Ancak; okunabilirliği artırması ve düzenli deyim derleyicisinin kimi eniyilemeleri yapmasını olanaklı kılması nedeniyle "a+" deyiminin kullanımı daha doğru olacaktır. Benzer gözlemler diğer işleçlerin bazı kullanımları için de yapılabilir. Mesela, aynı nedenlerden ötürü (okunabilirlik ve eniyileme) "(ab|b)" yerine—|, ayırdığı deyimler arasında uygulanan "veya" işlecine karşılık gelir—"a?b" deyiminin yeğlenmesi yerinde olacaktır.

Niceleyicilerin sadece en son karakteri [veya karakter öbeğini] nicelediği unutulmamalıdır. Örneğin, "Ali+" "Al" ile başlayıp bir veya daha fazla sayıda 'i' ile devam eden girdileri betimler, bir veya daha fazla sayıda "Ali" değerini değil. Buna karşılık, "(Ali){2,}" şeklinde tanımlanan düzenli deyim, ayraçlar vasıtasıyla yapılan öbek tanımı sayesinde, iki veya daha fazla sayıda "Ali" değerinin geçtiği karakter katarlarını betimler.

Önceki paragraflarda anlatılan niceleyicilerin işleyiş mantığı, girdide sağlanan karakterlerden mümkün olduğunca çok tüketecek şekilde bir eşleme yapmak şeklindedir. Örneğin, "birberber" sabitini başarılı bir şekilde betimleyen ".*ber" düzenli deyiminde bulunan ".*" olabildiğince çok karakter yutacak ve "birber" ile eşleşecektir. Bir diğer deyişle, düzenli deyimin sonundaki "ber" girdinin sonundaki "ber" ile eşleşecektir. Yinelemenin en az sayıda karakter yutularak yapılması için ise niceleyici sonrasına ? konulması gerekir. Mesela, "*.?ber" deyimindeki "*.?", "birberber" içindeki ilk üç karakteri tüketecektir. Yani, eşleşmenin mümkün olduğu durumlarda ilk kullanımdaki niceleyici olabildiğince çok yiyerek açgözlü davranırken ikincisi olabildiğince az yiyerek gönülsüz davranacaktır.

İşleyiş ayrıntılarına girildiğinde, açgözlü niceleyicilerin bir zaafı ortaya çıkar: düşük performans. Örneğimiz üzerinden anlamaya çalışalım. Eşleştirici, "birberber" sabitinin ".*ber" düzenli deyiminin betimlediği kümede olup olmadığına karar vermek istediğinde, öncelikle ".*" ile tüm girdiyi tüketir ve "ber" kısmının eşleştirilmesi için geriye hiçbir şey kalmaz. Sonucun başarısızlık olması üzerine eşleştirici, bir karakter geriye sarar ve ".*" ile "birberbe" sabitini eşleştirerek "ber" kısmını "r" ile eşleştirmeye çalışır. Bu da olmayınca, yapılacak olan bir kere daha geriye sarmaktır. Bu sefer, ".*" "birberb" ile eşleştirilir ve arda "er" kalır. Üçüncü hüsranın sonrasında girdinin geriye sarılması ile "*." "birber", "ber" ise sondaki "ber" ile eşleşir ve bu güzel haber [dördüncü denemeyi takiben] kullanıcıya muştulanır. Geri sarmanın getireceği performans düşüklüğünün önüne geçmek, kimi zaman bir diğer niceleyici grubunun kullanılması ile mümkün olabilir: sahiplenici niceleyiciler. Benzer şekilde çalışan bu niceleyiciler, açgözlü eşdeğerlerinin aksine geri sarma işlemine başvurmaz ve eşleştirmenin başarısızlıkla sonlandığını ilan eder. Bundan dolayı, "birberber" ".*+ber" tarafından kabul edilmeyecektir. Çünkü, ".*+" açgözlü davranarak tüm girdiyi tüketecek ve geriye "ber" ile eşleştirilecek bir şey kalmayacaktır. Bu, geriye sarmanın olmaması ile birleştiğinde, sonucun olumsuz olacağı anlamına gelir. Yani, açgözlü niceleyiciyle eşleştirilen bazı girdiler sahiplenici niceleyiciyle eşleştirilemeyecektir. O zaman, işlev açısından eşdeğer olup bize performans açısından kazandırdıkları bir örnek görerek sahiplenici niceleyicilerin gerekliliğine ikna olalım.
Pattern.matches("\\d*+\\(Ev\\)", "123456789Ev)") // → false
Bir numarayı ev telefonu olduğu bilgisiyle birlikte betimleyen bu düzenli deyimde, sahiplenici niceleyici (*+) yerine açgözlü uyarlamanın (*) kullanılması yarar getirmeyeceği gibi daha düşük bir performansa neden olacaktır. Çünkü, dokuz basamaklı sayıyı yedikten sonra girdide '(' arayan eşleştirici, dokuz kez geriye sarıp önceki karakterlerin hiçbirinin aradığı karakter olmadığını pahalı yoldan öğrenecektir. Yani, geriye sarma sonucunda fazladan keşfedilecek bir eşlemenin olmaması geri sarmaya tenezzül etmeyen sahiplenici niceleyiciyi öne çıkarmaktadır.

Bayraklar


Pattern.compile metodu, eşlemenin nasıl yapılacağını etkileyen bayraklar da alabilir. Eşleştiricinin üzerinde çalışacağı tüm girdiler için geçerli olacak bu bayraklar, istenecek olursa, benzer bilgilerin düzenli deyimin içine yerleştirilmesi suretiyle de etkinleştirilebilir veya geçersiz hale getirilebilir. Pattern sınıfı içinde sabit olarak tanımlanan bu bayraklar ve anlamları şöyledir.
  • UNICODE_CASE (?u): Büyük küçük harfler arasındaki dönüşüm ve eşitlik denetimleri Unicode tablosu temel alınarak yapılır. Türkçe'yi temel alarak işlem yapmak istiyorsanız—mesela, i'den I yerine İ'ye dönüşüm yapılmasını istiyorsanız—bu bayrağı aklınızdan çıkarmamanızda yarar olacaktır.
  • CASE_INSENSITIVE (?i): Harflerin eşlenmesi sırasında büyük küçük ayrımı yapılmayacaktır. UNICODE_CASE ile birlikte kullanılmadıkça bu bayrağın etkisinin ASCII tablosundaki karakterlere sınırlı kalacağı unutulmamalıdır.
  • UNICODE_CHARACTER_CLASS (?U): Kullanılacak ASCII temelli karakter sınıflarının Unicode tablosunu temel alarak işlev görmesini sağlar. Bu bayrağın etkin kılınması ile birlikte, UNICODE_CASE bayrağının da otomatikman etkinleştirildiği akılda tutulmalıdır.
  • COMMENTS (?x): Düzenli deyim içindeki beyaz boşluklar ve '#' karakteri ile sonrasındaki satır sonuna kadar her şey göz ardı edilir.
  • MULTILINE (?m): Girdinin satır ayırıcı karakter(ler)inin (Unix temelli işletim dizgelerinde yeni satır karakteri ('\n'), Microsoft işletim dizgelerinde satır başı ('\r') ve yeni satır karakterleri) olduğu yerlerden ayırarak satırlar halinde incelenmesini sağlar. Buna göre, ^ ve $ artık girdinin başı ve sonunu değil, incelenmekte olan satırın başı ve sonunu belirtecektir.
  • UNIX_LINES (?d): Satırların Unix temelli işletim dizgelerinde olduğu gibi yeni satır karakteri ile sonlandığı varsayılacaktır.
  • DOTALL (?s): "." deyiminin satır ayırıcıları da betimlemesini sağlar. Bu, satır ayırıcılarının da ".*" ile yutulacağı anlamına gelir ve bu sebepten dolayı kimi zaman tek satır kipi olarak da adlandırılır.
  • LITERAL: Eşleştiricinin karakter katarını yorumlamayacağını bildiren bu bayrağın etkisi, düzenli deyimin "\Q"-"\E" çiftiyle çevrelenmesi yoluyla da elde edilebilir.
  • CANON_EQ: Unicode tablosunda birden çok nokta ile temsil edilen veya birden çok karakterin bileştirilmesi ile de oluşturulabilecek karakterlerin değişik karşılıklarının birbirine eşit olmasını sağlar. Örneğin, 231 (0xE7) nolu konumdaki ç harfi, c harfine kanca iminin (\u0327) monte edilmesiyle de oluşturulabilir.
    Pattern desen = compile("c\u0327", CANON_EQ);
    out.println(desen.matcher("ç").matches()); // ⇒ true
    
    Dikkat edecek olursanız, \u önekiyle sağlanan karakterlerin, String sabitinin oluşturulması sırasında içselleştirilmesi nedeniyle, önüne ikinci bir \ konulmasına gerek yoktur.

Bayrakların düzenli deyim içinde belirtilmesi durumunda, birden çok sayıda bayrak aynı noktada etkinleştirilebileceği gibi, istenen bayraklar geçersiz de kılınabilir. Örneğin, (?Ui) betimlemenin Unicode tablosunu temel alarak büyük-küçük farkı gözetmeksizin yapılacağını ifade ederken, (?-d) [belki de daha önce etkinleştirilmiş olan] Unix usulü satır ayırıcı bayrağını o anki noktadan itibaren geçersiz hale getirmektedir.

Öbekler


Kimi zaman, düzenli deyimle girdi arasında bir eşleşmenin olup olmamasının yanısıra, eşleşme ile ilgili kimi ayrıntıları da öğrenmek isteriz. Örneğin, telefon numaralarını betimleyen bir düzenli deyimde, alan kodu ve numara ile ayrı ayrı ilgileniyor olabiliriz. Bu gibi bir durumda yapmamız gereken, düzenli deyimi karakter öbeklerine ayırmak ve eşleme sonrasında bu öbekleri sorgulamak olmalıdır.

Öbekler, ilgilenilen karakterlerin ayraç çifti arasına alınması ile oluşturulur. Eşleştirme sonrasında atıfta bulunulabilmesi için, öbekler açış ayraçlarının düzenli deyimdeki geçiş sırasına göre numaralandırılırlar. Örneğin, genç kızlık soyadını koruyarak adını yazan bayanların adları "(\p{Alpha}+)\s((\p{Alpha}+)-(\p{Alpha}+))" ile betimlenebilir. [Düzenli deyimi sınamak istediğinizde, \ yerine \\ koymayı unutmayınız.] Bu düzenli deyim, ad ile başlayıp bir boşlukla devam eden ve birbirinden - ile ayrılmış iki soyadını tanımlamaktadır. Ad ilk öbekle eşleşirken, tüm soyadı ikinci, eşin soyadı üçüncü, ve nihayet, genç kızlık soyadı dördüncü öbek olarak eşleştirilecektir.

Oluşturulan bir öbeği atıfta bulunarak kullanmak istediğimizde, bunu geçiş sırasının önüne \ koyarak sağlayabiliriz; \0 her zaman girdinin tümü ile eşleştirilir. İlk üç rakamın alan kodu ile aynı olduğu telefon numaralarını betimleyerek buna bir örnek verelim: "(\d{3})-(\1\d{4})". İlk öbeği alan kodu, ikinci öbeği telefon numarası ile eşleştiren bu düzenli deyim, "532-5321234" numarasını betimlerken "232-5321234" numarasını betimlemeyecektir.

İstediğimiz takdirde, öbeklere kendilerine verilen ad ile de atıfta bulunulabilir. Bunun için, öbeğe uygun görülen adın üçgen ayraç çifti arasına alınıp adlı öbeği başlatan (? sonrasına yazılması gerekir; öbeğe atıfta bulunmak istenmesi durumunda ise, öbek adının üçgen ayraçlarla birlikte \k'ye eklenmesi yeterli olacaktır. Buna göre, telefon numarası örneğimiz şöyle de yazılabilir: "(?<kod>\d{3})-(\k<kod>\d{4})". Bu arada; bir öbeğe ad verilmesi, söz konusu öbeğin geçiş sırasının geçersiz olduğu anlamına gelmez. İsteyecek olursak, düzenli deyimimizi "(?<kod>\d{3})-(\1\d{4})" olarak da tanımlayabiliriz.

Kimi zaman, eşleştirilen bir öbeği göz ardı etmek isteyebiliriz. Yani; eşleşmenin başarılı olması sonrasında söz konusu öbeğin hesaba katılmasını istemeyebiliriz. Bunun için yapmamız gereken şey, öbeğin (?: ile başlatılmasından ibarettir. Bu durumda, göz ardı edilen öbeğin açış ayracı öbek sırasını saptamakta yararlanılan sayacı etkilemeyecektir. Son olarak, şu nokta da unutulmamalıdır: adlandırılan öbekler göz ardı edilemezler.

Eşleştirici Kipleri


Java'daki düzenli deyim desteği, Pattern ve Matcher sınıflarındaki matches metotlarında gerçekleştirilenden farklı eşleştiriciler de sağlar. Bunlardan Pattern sınıfındaki split iletisi, argümanındaki CharSequence kategorisine ait katarı, ileti alıcının temsil ettiği deseni kullanarak String nesnelerine böler ve bu String'leri içeren diziyi döndürür.
...
Pattern desen = Pattern.compile(":");
String[] bilgi = desen.split("Gökçe Begüm:Ege:532-1234567");
// bilgi[0] ← "Gökçe Begüm", bilgi[1] ← "Ege", bilgi[2] ← "532-1234567"
İstenecek olursa, split'e sağlanacak ikinci argüman ile döndürülecek dizinin eleman sayısı sınırlandırılabilir. Örneğin, yukarıdaki kullanımda ikinci argüman olarak 2 geçirilmesi, ilki "Gökçe Begüm" ikincisi "Ege:532-1234567" değerine sahip iki elemanlı bir dizi döndürecektir.

Göz atacağımız diğer eşleştiriciler marifetlerini Matcher nesnelerine gönderilen iletiler yoluyla gösterirler. Dolayısıyla, düzenli deyimin compile metodu ile derlenmesi sonucu elde edilen desene (Pattern nesnesi) matcher iletisinin gönderilmesi yapacağımız şeylerin başında gelmelidir. İkinci adım olan eşleştiricinin çağrılması öncesinde, işlemin etkili olacağı girdi bölgesi region iletisi ile belirtilebilir. [Daha sonra eşleştiricimizin girdinin hangi bölümünü ele aldığını görmek istersek, ilişkin bölgenin tanımlanmasında kullanılan argüman değerlerini döndüren regionStart ve regionEnd iletilerinden yararlanabiliriz.] Son adım olarak ise, desen ile girdinin uyuşması halinde icra edilecek bir inceleme aşaması vardır. Bu, String argümanlı group iletisinin yanısıra MatchResult arayüzünde yer alan iletilerin eşleştiriciye gönderilmesi ile yapılablir. Dolayısıyla, tipik eşleştirici kullanımı aşağıdaki şablonu takip edecektir.
import java.util.regex.*;
...
Pattern desen = Pattern.compile(...);
Matcher eşleştirici = desen.matcher(...);
eşleştirici.region(başİndis, sonİndis + 1); // Seçimli
... // Eşleştiriciyi uygun bir kipte kullan.
if (tanındıMı) {
  MatchResult sonuç = eşleştirici.toMatchResult();
  ... // sonuç'u incele
}
Eşleştiricinin yaratılması sonrasında girdinin ele alınması noktasında farklı çalışma kiplerini temsil eden üç yüklemden bahsedebiliriz: matches, lookingAt, find. Önceki örneklerimizden de gördüğümüz gibi matches, girdi ile [düzenli deyimin içselleştirilmiş karşılığı olan] deseni eşleştirirken girdinin tümünü tüketmeye çalışır; aksi takdirde, eşleştirme sonucu olumsuz olacaktır. Bundan dolayıdır ki, ".*ber" "birberbere" sabitini tanımaz. Çünkü, ".*" ile "birber" ve "ber" ile girdideki "ber" eşleşmesini takiben sondaki "e" açıkta kalır ki, bu matches metodundan false döndürülmesine neden olur. Artık girdinin eşleşmeyi engellemesi istenmiyorsa, matches yerine lookingAt iletisi kullanılmalıdır. matches'da olduğu gibi, girdinin ilgilenilen bölgesinin başından itibaren eşleştirerek işini gören lookingAt, girdinin sonunda eşleşme ile kapsanmayan karakterlerin kalmasına itiraz etmez ve true döndürür. Dolayısıyla, bu eşleştirici kipinde ".*ber" "birberbere" sabitini betimliyor kabul edilecektir. Ne var ki, lookingAt iletisi de, tıpkı matches gibi, girdi başının desenle uyuşmaması durumunda devamında ne olursa olsun false döndürür. Örneğin "a.*b", ne matches ne de lookingAt ile kullanıldığında, "qawdesb" girdisini betimlemeyecektir. Çünkü, her iki eşleştirici kipi de düzenleyici deyim başındaki 'a' değerini girdinin en başında arayacak ve başarısızlık sonrasında false döndürecektir. Sıkıntımızın çözümü, eşleştiriciyi find kipiyle kullanmakta yatar. Seçimli bir int argüman bekleyen bu eşleştirici kipi, girdinin başındaki eşleşmeyen karakterleri atlar ve deseni girdi içinde arar; girdinin başındaki ve sonundaki eşleşmeyen parçalar sonucun olumsuz olmasına neden olmaz. Dolayısıyla, "a.*b" düzenli deyiminin "qawdesbc" içinde aranması, baştaki "q" değerini göz ardı ettikten sonra, "awsdesb" ile eşlemeyi sağlayacak ve sonda artan "c" değerine rağmen true döndürecektir.

Eşleştirici kipleri arasındaki bir diğer fark, daha önceden eşleştirilmiş desen-girdi çiftinin sıfırlanmaksızın tekrar kullanılması durumunda sergilediği davranış biçimidir. Girdinin tümünü eşleştirmeye çalışan matches, girdiyi tüketmiş olduğu için ikinci ve sonraki kullanımlarında her zaman false döndürürken, lookingAt her zaman ilk kullanımda döndürdüğü sonucu döndürür. Dolayısıyla, aşağıdaki kod parçası sonsuz döngü içinde standart çıktı ortamına ba yazmaktan başka bir şey yapmayacaktır. [MatchResult arayüzü iletilerinden olan group, argümanında geçirilen sıradaki öbeği döndürür; 0 eşleştirilen tüm girdi parçasına karşılık gelir ve aynı etki group iletisini argümansız kullanmakla da yaratılabilir.]
String düzenliDeyim = "ba", girdi = "baba";
Pattern desen = Pattern.compile(düzenliDeyim);
Matcher e = desen.matcher(girdi);
while (e.lookingAt())
  System.out.println(e.group(0));
Bu durumun önüne geçilmesi lookingAt iletisinin üzerinde çalıştığı bölgenin aşağıdaki gibi değiştirilmesi ile mümkündür. Argümansız end iletisi, en son eşleştirmenin tükettiği son karakterin ötesindeki ilk karakterin indisini döndürür; öbek sayısını belirten bir tamsayının geçirilmesi halinde ise, aynı ileti belirtilen öbeğin sonunu takip eden karakterin indisini döndürür. [e.end() gönderisinin etkisi, e.start() + e.group().length() ile de sağlanabilir.]
while (e.lookingAt()) {
  System.out.println(e.group(0));
  e.region(e.end(), girdi.length());
}
find iletisi ise, başarılı eşleştirmenin ardından girdinin eşleşme sonrasındaki karakterinden devam ederek işini görür. Buna göre, yukarıdaki kod parçası şöyle de yazılabilir.
while (e.find())
  System.out.println(e.group(0));
Girdinin aynı veya farklı bölgelerinin farklı desenler kullanılarak eşleştirilmesi için, girdiyi parçalamak ve farklı eşleştiriciler kullanmaktansa, usePattern iletisi tercih edilmelidir. Bu iletinin kullanıldığı noktadan itibaren, ileti alıcı konumundaki eşleştirici girdinin sonuna veya bir sonraki usePattern kullanımına kadar tüm eşleştirmeleri söz konusu iletiye geçirilen deseni kullanarak yapacaktır.
String düzenliDeyim = "ab", girdi = "abba";
Pattern desen = Pattern.compile(düzenliDeyim);
Matcher e = desen.matcher(girdi);
if (e.find()) {
  System.out.println(e.group(0));
  e.usePattern(Pattern.compile("ba"));
  if (e.find())
    System.out.println(e.group());
}
Aynı eşleştiricinin farklı bir desenle kullanılmasını olanaklı kılan bir diğer ileti, CharSequence kategorisindeki yeni düzenli deyimi argüman olarak bekleyen reset'tir. usePattern'dan farklı olarak bu ileti, eşleştiriciyi girdinin başına konumlandırarak bölge tanımlarını geçersiz kılar. Bu işlemin desen değiştirilmeden yapılması için reset iletisinin argümansız uyarlamasının kullanılması yeterli olacaktır.

Eşleştiricilerin kimi kullanım desenleri, yüksek kullanım potansiyelleri nedeniyle Matcher sınıfında gerçekleştirilen iletiler halinde desteklenirler. Bunlardan biri olan appendReplacement, daha ziyade find ile birlikte kullanılır ve girdinin eşleşmeyen kısmını ilk argümanındaki StringBuffer türlü karakter tamponuna olduğu gibi eklerken, eşleştirilen parça yerine ikinci argümandaki String'i koyar. Bu iletiyi tamamlayan appendTail ise, başarısız bir eşleştirme çabası sonrasında girdinin ilişkin parçasını argümanındaki karakter tamponunun sonuna ekler.
String düzenliDeyim = "k", girdi = "bakbakbakşuna";
Pattern desen = Pattern.compile(düzenliDeyim);
Matcher e = desen.matcher(girdi);
StringBuffer tampon = new StringBuffer();
while (e.find())
  e.appendReplacement(tampon, "h");
String sonuç = e.appendTail(tampon).toString();
Buna göre, yukarıdaki döngünün ilk dönüşündeki eşleştirme ilk iki karakteri atlayacak ve desenimizi üçüncü konumdaki "k" ile eşleştirecektir. Sonuç olarak, tampon ile gösterilen bölgeye atlanan parça ("ba") değiştirilmeden, eşleştirilen parça ("k") ise "h" olarak eklenecektir. Dolayısıyla, ilk döngünün sonuna gelindiğinde tampon değişkeninde "bah" biriktirilmiş olacaktır. Döngünün ikinci ve üçüncü dönüşlerinde de benzer şekilde çalışan kod parçası, tamponda biriktirilen değeri "bahbahbah" haline dönüştürecektir. Dördüncü dönüşte, deseni "şuna" ile eşleştirmeye çalışan find olumsuz sonuç döndürecek ve döngüden çıkılacaktır. Bunu takiben gönderilen appendTail iletisi ise eşleştirilemeyen girdiyi tampon sonuna ekleyerek işi tamamlayacaktır.

Bir önceki paragrafta anlatılan değiştirme işlemi, replaceAll iletisi ile de yapılabilir. Anılan ileti, desenin eşleştirildiği girdi bölümlerini argümanında geçirilen String ile değiştirir. Benzer bir işlev gören repeatFirst ise, değişikliği eşleşmenin olduğu ilk noktada yapmakla yetinir.
String düzenliDeyim = "k", girdi = "bakbakbakşuna";
Pattern desen = Pattern.compile(düzenliDeyim);
Matcher e = desen.matcher(girdi);
String sonuç = e.replaceAll("h"); // sonuç ← "bahbahbahşuna"

  1. İki yöntem ile bir programın doğrudan yorumlanması ve Bytecode gibi bir aradile çevrildikten sonra yorumlanması arasında koşutluk kurabiliriz. ↑
  2. Bu uygulama, ifadenin konuşmacı tarafından yorum katılmadan aktarıldığını ifade eden dilimizdeki "Başbakan aynen şöyle dedi: ...", İngilizce'deki "The Prime Minister said, quote ... end quote" kalıplarına benzetilebilir. ↑

10 Kasım 2011 Perşembe

Soysallık

Kalıtlamanın getirilerinden biri, sınıflar arası ortak yönlerin bir üstsınıfta toplanmasına olanak tanıyarak kodun yeniden kullanımını sağlamasıdır.1 Bu sayede, değişikliğin gerektiği durumlarda birçok sınıfta değişiklik yapmaktansa üstsınıftaki kodun değiştirilmesi ile işin daha kısa sürede ve daha düşük hata oranlı bir biçimde yapılması mümkün olacaktır. Ne var ki, aynı yöntemin değişik türden verileri tutmada yararlanılan veri kaplarının gerçekleştiriminde kullanılması derleme zamanında denetlenebilecek kimi hataların çalışma zamanına kaymasına neden olmaktadır ki, bu piyasaya çıkmış bir yazılımın müşterinin "hatalı" kullanımı sonrasında göçebileceği anlamına gelir. Ne kastettiğimizi son giren ilk çıkar mantığıyla çalışan yığıt veri yapısının aşağıdaki gerçekleştirimi üzerinden görelim.

YığıtBoşDurumu.java
package vy.ayrıksıdurumlar;

public class YığıtBoşDurumu extends RuntimeException { ... }
IYığıt.java
package vy.arayüzler;

import vy.ayrıksıdurumlar.YığıtBoşDurumu;

public interface IYığıt {
  Object çıkar() throws YığıtBoşDurumu;
  void ekle(Object yeniElm);
  Object gözAt() throws YığıtBoşDurumu;
  boolean boşMu();
} // IYığıt arayüzünün sonu
Yığıt.java
package vy;

import java.util.Vector;
import vy.ayrıksıdurumlar.YığıtBoşDurumu;
import vy.arayüzler.IYığıt;

public class Yığıt implements IYığıt {
  public Yığıt() { _kap = new Vector(); }
  ...
  public Object çıkar() throws YığıtBoşDurumu {
    if (boşMu()) throw new YığıtBoşDurumu();

    Object üstteki = _kap.get(_kap.size() - 1);
    _kap.remove(_kap.size() - 1);

    return üstteki;
  } // Object çıkar() sonu 
  ...
  private Vector _kap;
} // Yığıt sınıfının sonu
Ekleme ve çıkarmanın elemanları tutan kabın aynı ucuna—mesela, Vector sonu veya LinkedList başı—yapılarak gerçekleştirilebilecek yığıt, söz konusu işlemlerin ilişkin arayüz tanımında da belirtildiği üzre Object tutacakları ile işlerini görmeleri nedeniyle herhangi türden bir nesneyi tutabilecektir. Buna göre, Yığıt sınıfının nesneleri yeri geldiğinde Öğrenci nesneleri tutarken, yeri geldiğinde Öğretmen nesneleri de tutabilecektir. Ancak, dikkat etmediğimiz takdirde aşağıda olduğu gibi bir durumun ortaya çıkması da olanaklıdır.
public static void yığıtıKullan() {
  IYığıt sınıf = new Yığıt();
  sınıf.ekle(new Öğrenci(...));
  Öğrenci ilkÖğrenci = (Öğrenci) sınıf.gözAt();
  if (Math.random() > Math.random())
    sınıf.ekle(new Öğrenci(...));
    else sınıf.ekle(new Öğretmen(...));
  ...
  Öğrenci sonÖğrenci = (Öğrenci) sınıf.çıkar();
  ...
} // void yığıtıKullan() sonu
Yığıt tanımımız Object tutacağı ile gösterilebilen türden—yani, Java'daki bileşke türlerin tümü—nesne tutabileceği için elemanların türdeş olma garantisi derleyici tarafından denetlenemez. Her ikisi de eninde sonunda Object'ten kalıtladığı için, aynı yığıta Öğrenci nesnesi de Öğretmen nesnesi de eklenebilmektedir. Bu ise, yukarıdaki kod parçasının son satırında olduğu gibi türdeşlik garantisinden hareketle yazılan satırların programın çalıştırılması sırasında ClassCastException ayrıksı durumuna neden olması demektir. Yani, yığıtımız ekleme sırasında itiraz etmediği nesnenin geri döndürülmesi sırasında kullanıcının kodunu göçertmektedir. Bunun önüne geçmek ancak öngörülen türlerin tümü için ayrı yığıt gerçekleştirimlerinin sağlanması ile olanaklıdır. Bu ise aşağıdaki gibi gereğinden kalabalık ve bakımı zor bir sınıf sıradüzeni anlamına gelir.

  • Object
    • Kişi
      • Çalışan
        • Öğretmen
        • Müstahdem
      • Öğrenci
    • Yığıt_Object, Yığıt_Kişi, Yığıt_Çalışan, Yığıt_Öğrenci, Yığıt_Öğretmen, Yığıt_Müstahdem, ...

Dilimizde iki ucu gaytalı değnek şeklinde nitelenen bu durum, J2SE 5.0 sürümüne kadar geçerli olmuş ve anılan sürüm ile birlikte Java diline eklenen soysallık yoluyla ortadan kaldırılabilmiştir. Bu özellik sayesinde, olası biçimlendirme hatalarının derleme zamanında önüne geçilerek tür güvenliği sağlanmış ve tek bir tür tanımının kullanılması ile kod bakımı kolaylaştırılmıştir. Soysallıktan yararlanarak oluşturulan sıradüzeni aşağıdaki gibi olacaktir.

  • Object
    • Kişi
      • Çalışan
        • Öğretmen
        • Müstahdem
      • Öğrenci
    • Yığıt<E>

Soysal Türlerin Tanımı ve Kullanımı


Soysal türlerin tanımı ve kullanımı metotları andırır. Nasıl ki, metotların hangi türden değerler geçirilerek işletilebileceğini belirtmek için ayraç çifti arasında belirtilen parametre listesi kullanılır, soysal türlerin hangi türler için parametrize edildiğini belirtmek için üçgen ayraç çifti arasında belirtilen tür parametre listesinden yararlanılır. Buna göre, yukarıdaki arayüz ve sınıfın soysal uyarlaması aşağıdaki gibi olacaktır.

IYığıt.java
package vy.arayüzler;

import vy.ayrıksıdurumlar.YığıtBoşDurumu;

public interface IYığıt<E> {
  E çıkar() throws YığıtBoşDurumu;
  void ekle(E yeniElm);
  E gözAt() throws YığıtBoşDurumu;
  boolean boşMu();
} // IYığıt<E> arayüzünün sonu
Yığıt.java
package vy;

import java.util.Vector;
import vy.ayrıksıdurumlar.YığıtBoşDurumu;
import vy.arayüzler.IYığıt;

public class Yığıt<E> implements IYığıt<E> {
  public Yığıt() { _kap = new Vector<E>(); }
  ...
  public E çıkar() throws YığıtBoşDurumu {
    if (boşMu()) throw new YığıtBoşDurumu();

    E üstteki = _kap.get(_kap.size() - 1);
    _kap.remove(_kap.size() - 1);

    return üstteki;
  } // E çıkar() sonu
  ...
  private Vector<E> _kap;
} // Yığıt<E> sınıfının sonu
Bir soysal türün nesnesinin yaratılması, tür parametrelerine karşılık gelen tür argümanlarının soysal türün adının ardından sağlanmasıyla mümkün olur. Bunun sonucunda soysal türün örneği olarak kullanılacak türe parametreli tür denir. Örneğin, aşağıdaki kod parçasında IYığıt<Öğrenci> parametreli türüne sahip sınıf değişkeni Yığıt<Öğrenci> parametreli sınıfına ait bir kabı göstermektedir ve bu kap Öğrenci tutacakları—dolayısıyla, Öğrenci köklü sıradüzenindaki sınıfların türünden nesneler—içerecektir.2 Ayrıca, dikkat ederseniz, tür parametresi ile belirtilen dönüş türlerine sahip iletiler/metotlar (gözAt, çıkar) biçimlendirme olmaksızın kullanılmaktadır; zira, biçimlendirme derleyici tarafından eklenen kod sayesinde otomatikman yapılmaktadır.
public static void yığıtıKullan() {
  IYığıt<Öğrenci> sınıf = new Yığıt<>();
  sınıf.ekle(new Öğrenci(...));
  // Biçimlendirmeye gerek yok
  Öğrenci ilkÖğrenci = sınıf.gözAt();
  if (Math.random() > Math.random())
    sınıf.ekle(new Öğrenci(...));
    else sınıf.ekle(new Öğretmen(...)); // Derleme hatası!!!
  ...
  Öğrenci öğr = sınıf.çıkar();
  ...
} // void yığıtıKullan() sonu
Bu noktada, tür argümanlarının bileşke türlü olmak zorunluluğu hatırlatılmalıdır. Dolayısıyla, IYığıt<int> tamsayılar = new Yığıt<>(); şeklinde bir tanımın yapılması derleyici tarafından kabul görmeyecektir. Ancak bu, parametreli sınıflara ait kaplara ilkel türlü değerlerin konulamayacağı anlamına gelmez. Yapılması gereken, kabı söz konusu ilkel türün karşılığındaki sarmalayıcı tür ile ilan edip işin gerisini derleyiciye bırakmaktır. Derleyici, eklenmek istenen ilkel türlü değeri usulca sarmalarken (İng., boxing, wrapping) döndürülen tutacağın gösterdiği nesneyi açarak (İng., unboxing) içeriği ilkel değere dönüştürecektir.
IYığıt<Integer> tamsayılar = new Yığıt<>();
tamsayılar.ekle(3); // Aşağıdaki ile aynı
tamsayılar.ekle(new Integer(3));
...
int üstteki = tamsayılar.çıkar();
// ≡ int üstteki = tamsayılar.çıkar.intValue();
Gerektiği takdirde, tür parametresine sınır getirerek kullanım noktasında geçirilmesi beklenen türlerin kısıtlanması sağlanabilir. Örnek olarak, sadece Number köklü sıradüzenindeki sınıfların nesnelerini içerebilecek bir kap düşünün. Bu istem, aşağıdaki sınıf başlığında olduğu gibi, Number sınıfının soysal türün parametresine üst sınır olarak getirilmesiyle ifade edilir. Böylece, SayıKabı soysal sınıfına üye parametreli sınıfların tür argümanı Number ya da Number'dan kalıtlayan sınıflara sınırlı olacaktır; derleyici diğer kullanımları hatalı kabul edecektir. Buna göre, SayıKabı<Byte> ve SayıKabı<Float> kabul görürken, SayıKabı<Object> ve SayıKabı<Character> reddedilecektir.
public class SayıKabı<E extends Number> { ... }
İstendiği takdirde, tür parametreleri gerçekleştirilmesi beklenen bir arayüz ile de sınırlandırılabilir. Hatta, sınıf adı ve birden çok arayüz adı birlikte verilerek de sınır konulabilir. Mesela, aşağıdaki soysal türün kullanımı noktasındaki kabul edilebilir tür argümanlarının Snf'den kalıtlaması ve Aryz1 ile Aryz2 arayüzlerini gerçekleştirmesi zorunludur.
public class SoysalTür<E extends Snf & Aryz1 & Aryz2> { ... }

J2SE 5.0 Öncesi Kod İle Birliktelik: Ham Türler


Java'daki soysal türler, J2SE 5.0 sürümüne dek geçen dokuz yıla yakın sürede üretilen kodun kullanılmasını sağlamak adına ham türleri destekler. Bu desteğin amacı, hali hazırda var olan milyonlarca satırın çöpe gitmesini önlemek ve söz konusu soysallık öncesi kodun zaman içinde dönüştürülmesini sağlamaktır.

Ham türlü tanımlayıcılar, soysal bir türün üçgen ayraç çifti ve tür argümanları olmaksızın, yani J2SE 5.0 öncesindeki gibi, kullanılması ile tanımlanır. Soysal türü tür parametrelerine Object geçiriliyormuş gibi kullanmaya denk olan bu kullanım, her şey eninde sonunda Object tutacağı yoluyla görülebileceği için, derleyicinin kullanım hatalarını denetleme yeteneğini ortadan kaldırır. Dolayısıyla, soysallık sonrası yazılan kod içinde ham türlerin kullanımından kaçınılmalıdır.

Ham türlerin kullanımı iki durumda zorunludur: i) Yeni kod içinden soysallık öncesi kod kullanıldığında, ii) eski kod içinden soysal kod kullanıldığında. Örneğimiz ile devam ederek iki duruma da bir bakalım. Varsayalım ki, soysallık öncesinde IYığıt ve Yığıt türlerini tanımladık ve sınadıktan sonra kullanıma sunduk. Bu türler, soysallığa dair tür parametreleri olmadığı için, tür argümanları geçirilerek kullanılamaz; kullanım, ister soysallık öncesi kod içinden olsun isterse soysallık sonrası kod içinden, aşağıdaki gibi olacaktır.
public static yığıtıKullan() {
  IYığıt sınıf = new Yığıt();
  sınıf.ekle(new Öğrenci(...));
  Öğrenci ilkÖğrenci = (Öğrenci) sınıf.gözAt();
  if (Math.random() > Math.random())
    sınıf.ekle(new Öğrenci(...));
    else sınıf.ekle(new Öğretmen(...)); // Derleme hatası vermez!
  ...
  Öğrenci sonÖğrenci = (Öğrenci) sınıf.çıkar();
  ...
} // void yığıtıKullan() sonu
Soysallık sonrası yazılmış olan kullanıcı kodun derlenmesi hataya sebep olmamakla birlikte, derleyicinin, bu çeşit bir kullanımı potansiyel çalışma zamanı hatalarına davet çıkarmak olarak görmesi nedeniyle, denetlenemeyen olası hata uyarısını (İng., unchecked warning) vermesine yol açacaktır. Yani, soysallık öncesindeki gibi sessiz kalmaktansa, derleyici olası hataya işaret etmekte ve bizden ya kullanıcı kodunu değiştirerek duruma açıklık getirmemizi ya da elimizin altındaysa hem kullanılan kodu hem de kullanıcı kodunu soysallık sonrası standartlarına getirmemizi istemektedir. Kullanılan kodun elimizin altında olmaması halinde, iki şey yapılabilir: i) Kullanılan kod soysal değilse, kodumuz içinde kullanımımızın doğru olduğuna dair derleyiciye garanti veririz, ii) kullanılan kod soysalsa soysal türü kullanma niyetimizi belirten tür argümanlarını kullanırız. İlk şık, aşağıdaki şekilde SuppressWarnings açımlamasıyla yerine getirilebileceği gibi derleyiciye geçirilecek -Xlint:-unchecked opsiyonu ile de yerine getirilebilir. Her iki durumda da yaptığımız, derleyiciye "unchecked" etiketli uyarıları göz ardı etmesini söylemektir. Ancak; ilk yöntemde uyarılar açımlamanın öncesine yerleştirildiği metot boyunca göz ardı edilirken, derleme opsiyonu yeğlendiğinde derleyicinin hoşgörüsü derlenen tüm koda yaygınlaştırılmaktadır.
@SuppressWarnings({"unchecked"})
public static void yığıtıKullan() {
  IYığıt sınıf = new Yığıt();
  sınıf.ekle(new Öğrenci(...));
  Öğrenci ilkÖğrenci = (Öğrenci) sınıf.gözAt();
  if (Math.random() > Math.random())
    sınıf.ekle(new Öğrenci(...));
    else sınıf.ekle(new Öğretmen(...));
  ...
  Öğrenci sonÖğrenci = (Öğrenci) sınıf.çıkar();
  ...
} // void yığıtıKullan() sonu
Kullanılan kodun elimizin altında olması halinde, verilen uyarı dikkate alınmalı ve kaynak kod soysallık sonrası standartlara getirilerek yeniden derlenmelidir. Yeniden derlenen kodun eski kullanıcıları bu değişiklikten etkilenmeyeceklerdir. Çünkü, soysallık öncesinde yazılmış kullanıcılar değiştirilmiş kodun gözünde ham türlerden yararlanan yeni koddan farklı değildir.

Daha Esnek Metot İmzaları İçin Joker Türler


Pek çok ölümlü Java programcısı için soysallık desteğini ustaların erişilmez alemine sınırlı kılan en başlıca etken, joker tür argümanlarının varlığıdır. Değişik biçimlerde kendini gösteren bu korkunç yaratığı, iki kümenin sahip olduğu ortak elemanların sayısını döndüren metodu gerçekleştirerek tanımaya başlayalım. Veri Kapları Çerçevesi'nin sağladığı Set arayüzünde karşılanmayan bu işlem, birinci argümandaki kümenin her bir elemanının diğer kümede var olup olmamasına göre sayaç değişkenini güncelleyen aşağıdaki metotla gerçekleştirilebilir.
import java.util.Set;
...
public static int ortakElmSayısı(Set km1, Set Km2) {
  int elmSayısı = 0;
  for (Object elm : km1)
    if (km2.contains(elm)) elmSayısı++;

  return elmSayısı;
} // int ortakElmSayısı(Set, Set) sonu
Bir önceki altbölümden dersini almış olanlarınız, metot imzasındaki ham türlere bakıp bildiklerinden kuşkuya düşerek, neden Set<Object> kullanılmamış, diye sorabilirler. Öncelikle, bu arkadaşları doğru yolda olduklarını söyleyerek yatıştıralım; gerçekten de, yukarıdaki imzada bulunan ham türler derleyicinin tür denetim desteğini engellemek suretiyle tehlikeye davet çıkarıyor. Ancak; ham tür yerine Set<Object> kullanmak da sorunu halletmiyor. Şöyle ki; T'nin S'den kalıtladığı ve G'nin soysal tür olduğu bir ortamda, G<T> G<S>'den kalıtlamaz. Bir diğer deyişle, G<T> nesneleri G<S> nesneleri olarak ele alınamaz. Somutlaştıracak olursak, Integer'ın Object'ten kalıtlıyor olmasına karşın, Set<Integer> Set<Object>'ten kalıtlamaz. Bu ise, iki türün uyumsuz olduğu ve birbirlerini ilklemekte veya birbirlerine atanmakta kullanılamayacağı anlamına gelir. Gelin, bu sonucu aşağıdaki kod parçasının üzerinden giderek pekiştirelim. Öncelikle, 3. satırda kümeyi yaratırken elemanlarımızın Integer (ve otomatikman sarmalanarak Integer'a dönüştürülen int) ile tür uyumlu olabileceğini ilan ediyoruz. Daha sonraki satırlarda, verdiğimiz bu söze uyarak kümemize eleman ekliyoruz. Ancak; intKüme ile Set<Object> türlü objKüme'yi ilkleyen son satır, derleyicinin sıkı denetimini aşamıyor ve hataya neden oluyor. Bunun sebebi, iki tutacak tarafından paylaşılan ve başta Integer ile tür uyumlu nesneler ile doldurulacağı ilan edilen küme nesnesinin artık objKüme aracılığıyla Object ile tür uyumlu nesneler—yani, Java nesne alemindeki bütün nesneler—ile doldurulması ihtimalidir.
import java.util.*;
...
Set<Integer> intKüme = new TreeSet<>();
intKüme.add(new Integer(0));
...
Set<Object> objKüme = intKüme; // Derleme hatası!!!
Derdimizin çaresi tür argümanı olarak joker kullanımından geçer. ? ile belirtilen joker, adını bilmediğimiz veya umursamadığımız türler için kullanılır. Aşağıdaki metot imzasını buna uygun okuyacak olursak, ortakElmSayısı'nın herhangi bir türden elemanlara sahip iki Set beklediğini söyleyebiliriz.
public static int ortakElmSayısı(Set<?> km1, Set<?> Km2) {
  ...
} // int ortakElmSayısı(Set<?>, Set<?>) sonu
Şu iki noktanın akılda tutulmasında yarar olacaktır: i) farklı parametrelerde kullanılan jokerler birbirinden bağımsızdır ve farklı türlerle eşleştirilebilirler, ii) joker parametreli türün nesnesine eleman olarak sadece null eklenebilir.3

Yığıt arayüzüne iki yeni ileti ekleyerek devam edelim. Bunlardan yükle, argümanındaki kabın elemanlarını teker teker hedef nesneye eklerken, temizle hedef nesneyi boşaltırken silinen elemenları daha sonraki kullanımlar için argümanda geçirilen kaba kaydediyor.
import java.util.Collection;

public class IYığıt<E> {
  ...
  public void temizle(Collection<E> kopya); // Kapsayıcı değil!
  public void yükle(Collection<E> kaynak); // Kapsayıcı değil!
  ...
} // IYığıt<E> arayüzünün sonu
İlk denememiz, soysal türlerin daha önceden bahsettiğimiz kalıtlama ile birlikte değişmeme özelliğinden dolayı tüm kullanımları kapsamıyor ve kimi zaman kullanıcı tarafında derleme hatasına neden olabiliyor. Anılan özelliği yinelemektense, yükle iletisinin bir kullanım örneğine bakarak durumu anlamaya çalışalım.
IYığıt<Kişi> güruh = new Yığıt<>();
// güruh'a bir şeyler koy
Vector<Öğrenci> sınıf = new Vector<>();
// sınıf'a bir şeyler koy
güruh.yükle(sınıf); // Derleme hatası!!!
Belli ki, yukarıdaki kodu yazan arkadaş Öğrenci sınıfının Kişi'den kalıtladığını düşünerek Vector<Öğrenci> adlı parametreli türün de Collection<Kişi>'yi gerçekleştiren Vector<Kişi>'den kalıtladığı sonucuna varmış. Ne var ki, soysallığın kalıtlama ile birlikte değişme özelliği olmaması nedeniyle, öncülü doğru olan bu tümcenin sonuç kısmı hatalı. Yani, Vector<Öğrenci> Vector<Kişi>'den kalıtlamaz. İş böyle olunca, kodun son satırı parametre (Collection<Kişi>) ile argüman (Vector<Öğrenci>) arasındaki tür uyumsuzluğu nedeniyle derleme hatasına yol açıyor.

İçine düştüğümüz sıkıntı, sınırlı joker kullanımıyla çözülebilir. Yukarıdaki örneği sürdürerek ifade edecek olursak; derleyiciye söylememiz gereken, Yığıt<Kişi> türlü bir yığıta Kişi veya Kişi'den kalıtlayan herhangi bir sınıfa ait elemanlar içeren bir kap ile yükleme yapılabileceğidir. Bu ise, yükle'nin parametresinin Collection<? extends E> türüne sahip ilan edilmesi ile olanaklıdır. Yani, hedef nesneye girdi sağlama görevi gören kap, üst sınırlı joker türüyle tanımlanmalıdır.

temizle iletisinin imzasında ortaya çıkan sorun da sınırlı jokerlerin koşut bir kullanım biçimiyle sağlanabilir. Önce derleyicinin kabul etmeyeceği bir kullanıcı kodu görelim.
IYığıt<Öğrenci> sınıf = new Yığıt<>();
// sınıf'a bir şeyler koy
Vector<Kişi> güruh = new Vector<>();
sınıf.temizle(güruh); // Derleme hatası!!!
Bu örnekte de esnek olmayan bir imzanın cezasını çekiyoruz: Öğrenci tür argümanıyla yaratılan sınıf ancak ve ancak Öğrenci tutacakları içeren bir kaba kaydedilebiliyor. Kullanıcının yapmak istediği gibi kap Kişi eleman türüne sahip olduğunda, derleyici karşımıza dikiliveriyor. Halbuki, Öğrenci tutacağı ile görülebilen nesneler Öğrenci'nın atası olan tüm sınıfların (Kişi ve Object) tutacakları ile de görülebilir. Yani, imzanın kullanıcımızın yapmak istediğine izin verecek şekilde gevşetilmesi gerekir. Bir diğer deyişle, yığıt içeriğinin kaydedildiği kabın eleman türünün Öğrenci ve Öğrenci'nin atası olan herhangi bir sınıf olabileceğini derleyiciye bildirmemiz gerekir. Bu ise, alt sınırlı bir joker türünün kullanımı ile olanaklıdır ve örneğimizde imzadaki parametre türünün Collection<? super E> şeklinde değiştirilmesi işimizi görecektir.

Buna göre, arayüze eklenmek istenen iletilerin imzası şu şekilde oluşur. Ortaya çıkan imzalar, parametre listesindeki kaplar için genelde izlenmesinde yarar olacak bir kuralı da ele vermektedir: Hedef nesneye girdi sağlama görevi gören kaplar üst sınırlı joker tür, hedef nesnenin ürettiği çıktının kaydedildiği çıktı amaçlı kaplar ise alt sınırlı joker tür ile tanımlanmalıdır.
import java.util.Collection;

public class Collections {
  ...
  public void temizle(Collection<? super E> kopya);
  public void yükle(Collection<? extends E> kaynak);
  ...
} // IYığıt<E> arayüzünün sonu

Soysal Metotlar


Veri kapları dışında soysallıktan yararlanılan bir diğer programlama öğesi metotlardır. Genelde soysal kaplar üzerinde çalışan bu metotların soysallığı, niteleyicilerinin sonrasında kullanılan tür parametreleri yoluyla ifade edilir. Standart Veri Kapları Çerçevesi'ndeki değişik türden kaplar üzerinde uygulanabilecek metotları içeren java.util.Collections sınıfında bulunan sıralama metotlarına bakarak görelim.
package java.util;

public class Collections {
  ...
  public static <E extends Comparable<? super E>> void
    sort(List<E> liste) { ... }
  public static <E> void
    sort(List<E> liste, Comparator<? super E> karşılaştırıcı) { ... }
  ...
} // Collections sınıfının sonu
Sıralama, çok özel koşullarda kullanılabilecek bazı algoritmalar dışında, elemanların karşılaştırılması yardımıyla icra edilen bir yeniden düzenleme işlemidir. Dolayısıyla, sıralanması istenen kabın elemanlarının karşılaştırılabilir bir türe ait olması gerekir. Bu beklenti Java'da iki şekilde karşılanabilir:
  1. Elemanların ait olduğu sınıf Comparable arayüzünü gerçekleştirir. Bu noktada, gerçekleştirme ilişkisinin doğrudan olması gerekmediği unutulmamalıdır. Genel olarak, bir nesne üyesi bulunduğu sınıfın atası olan herhangi bir sınıftaki compareTo metodu ile karşılaştırılabilir. Bu sebepten ötürü, List<E>'nin sıralanabilmesi için E'nin Comparable arayüzünü gerçekleştirmesi veya gerçekleştiren bir üstsınıfa sahip olması gerekir. Bu ise yukarıdaki imzada olduğu gibi alt sınırlı bir joker tür ile belirtilebilir.
  2. Elemanları karşılaştırmayı bilen bir başka sınıf bu talebi karşılar. Bunun için, söz konusu sınıfın java.util paketindeki Comparator arayüzünü eleman türüne uyumlu bir şekilde gerçekleştirmesi gerekir. Tür uyumluluğu, bir önceki maddede olduğu gibi alt sınırlı joker tür ile belirtilmelidir.

Soysallık ve Diziler


İlk öğrendikleri programlama dilinin komutsal olması nedeniyle Java'da da gerekli gereksiz dizi kullanmaya alışanları soysal türler kötü bir sürprizle karşılar: dizilerin bileşen türü tür parametresi kullanılarak ifade edilemez. Örneğin, Yığıt sınıfının aşağıdaki şekilde dizi kullanacak biçimde gerçekleştirilmesi derleme hatasına neden olacaktır.
...
public class Yığıt<E> implements IYığıt<E> {
  public Yığıt() { 
    _kap = new E[]; // Derleme hatası!!!
    ...
  } // varsayılan yapıcı sonu
  ...
  private E[] _kap;
} // Yığıt<E> sınıfının sonu
Bunun sebebi, dizi ve soysal türler arasındaki temel bir farktan kaynaklanır: dizilerin tür bilgileri derleme sonrasına da taşınırken, soysal türlerin tür bilgileri derleyicinin kullanımı sonrasında silinir. Mesela, aşağıdaki kod parçasında intDz değişkeninin [ve tüm diğer Integer elemanlı dizilerin] üstnesnesi Integer[].class iken dblDz değişkeninin [ve tüm diğer Double elemanlı dizilerin] üstnesnesi Double[].class'dır. İstenecek olursa, ilişkin üstnesne (ve eleman türünün üstnesi [Integer.class ve Double.class]) kullanılarak içgörü≝ (İng., reflection, introspection) yardımıyla Integer veya Double elemanlı yeni diziler yaratılabilir. Buna karşılık, intVec ve dblVec değişkenlerinin her ikisi de aynı üstnesneye sahip olacaktır: Vector.class. Çünkü, derleme sırasında kullanılan tür bilgileri tür silme (İng., type erasure) sonunda yok olmuş ve tüm parametreli türler aynı üstnesneyle temsil edilmek zorunda kalmıştır.4
Integer[] intDz = new Integer[10]();
Double[] dblDz = new Double[5]();
...
Vector<Integer> intVec = new Vector<>();
Vector<Double> dblVec = new Vector<>();
Bu nedenden ötürü, dizi kullandığımız yukarıdaki gibi durumlarda, ya dizi kullanmaktan vazgeçerek soysal türlerden birine yönelmemiz ya da derleme hatası vermemekle birlikte derleyici uyarısına neden olan şu kodu tercih etmemiz gerekir. Tavsiye edilen birinci yolun seçimidir.
...
public class Yığıt<E> implements IYığıt<E> {
  public Yığıt() { 
    _kap = (E[]) new Object[]; // Derleyici uyarısı!
    ...
  } // varsayılan yapıcı sonu
  ...
  private E[] _kap;
} // Yığıt<E> sınıfının sonu


  1. Anlatım, haklı olarak, kalıtlamanın sadece sınıflar arası geçerli bir ilişki olduğu izlenimini uyandırabilir. Fakat bu kesinlikle doğru değil; kalıtlama sınıflar arasında olduğu gibi arayüzler arasında da geçerli olan bir ilişkidir. ↑
  2. Kod parçasında Java SE 7 ile eklenen elmas işlecinin kullanımına dikkat ediniz. Dolayısıyla, çalışma ortamını henüz güncellememiş olanlar bu kodu denediklerinde derleyici hatası ile karşılaşacaktır. Bu hatanın giderilmesi için tanımın şu şekilde tür çıkarsama olmaksızın yapılması gerekir.

    IYığıt<Öğrenci> sınıf = new Yığıt<Öğrenci>();

    Elmas işleci ve diğer Java SE 7 yenilikleri için buraya🔎 bakınız.↑
  3. Bazılarınızın bunun çalışma zamanı hatasına gebe bir durum olduğunu söylediklerini duyar gibiyim. Doğru ya, ilk argüman olarak Öğrenci nesneleri tutan, ikinci argüman olaraksa Öğrenci ile alakasız Koyun sınıfının nesnelerini tutan bir kullanım düşünebiliriz. Bu durumda, metodumuzdaki contains iletisinin gerçekleştirimindeki equals çağrısı, Öğrenci nesneleri ile Koyun nesneleri karşılaştırılamayacağı için, çuvallayacak ... mıdır acaba? equals iletisinin Object sınıfında tanımlanan genel sözleşmesine baktığınızda, true döndürme koşulunun hedef nesne ile uyumlu bir türe ait argümandaki null olmayan nesnenin eşit addedilmesi olduğu, geri kalan durumlarda ise false döndürülmesi gerektiğini görürsünüz. Dolayısıyla, equals iletisinin Öğrenci sınıfındaki gerçekleştirimi hedef nesne ile uyumsuz olan bir nesne gördüğünde, kodun devamında bir hatanın oluşmasına sebebiyet vermeden false döndürmelidir. ↑
  4. Bunun bir sonucu olarak, üstnesneden yararlanarak işini gören instanceof işleci de soysal türün ham tür halini bekler. Yani, kullanım if (nesne instanceof Yığıt) ... şeklinde olmalıdır. ↑

27 Ekim 2011 Perşembe

Hatasız Programcı Olmaz: Java'da Hata Kotarımı

Yazılım gerçek dünyada var olan somut/soyut süreçlerin bilgisayar donanımı üzerinde çalıştırılan benzetimleridir. Amaç, sürecin, etkileşimde bulunulan diğer süreçler ile birlikte daha verimli ve sağlıklı bir şekilde işletilmesini sağlamaktır. Mesela, bir otomasyon yazılımı zayiatı azaltmak ve bakımı hızlandırmak suretiyle parçası olduğu üretim sürecinin daha verimli olmasını sağlayacaktır. Doğal olarak, bir yazılımın başarılı kabul edilmesindeki en önemli ölçüt, bilgisayarda oluşturulan hesaplama sürecinin benzetimi yapılmakta olan sürecin algılanışına1 sadık kalıp kalmadığı şeklinde ifade edilebilecek doğruluk (İng., correctness) özelliğidir. Peşinden koşulması gereken bir diğer özellik, yazılımın öngörülmeyen koşullar altında da kabul edilebilir bir tepki göstermesi olarak da tanımlayabileceğimiz dayanıklılıktır (İng., robustness). Bu beklentiler yazılım sektörünün rekabete dayalı pragmatik bir dünya olması gerçeği ile birlikte ele alındığında, yazılım üreticisinin birincil görevinin düzgün bir peformansa sahip, doğru ve dayanıklı—yani, güvenilir (İng., reliable)—programların makul bir hızda geliştirilmesi olduğu söylenebilir. İşte bu yazıda, ifade edilen görev tanımındaki güvenilirlik özelliğine yönelik olarak Java programlama dili tarafından sağlanan desteğe değineceğiz.

Kötü bir haber vererek başlayalım: Java, programlarınızın doğruluğunu sağlamak bağlamında kaynak kodunuzun dilin yazım kurallarına uygunluğunu denetlemek dışında pek bir destek sağlamaz; sözdizimsel hataların ötesinde Java derleyicisinin yapacağı çok şey yoktur. Aşağıdaki örnek üzerinden ne demek istediğimizi açalım.

Faktöryel.java
import static java.lang.System.out;
...

public class Faktöryel {
  public static void main(String[] ksa) {
    byte b = Byte.parseByte(ksa[0]);
    out.println(b + "!: " + fakt(b));
    ...
  } // void main(String[]) sonu
 
  public static long fakt(byte n) {
    long çarpım = 1;
    for (byte i = 2; i <= n; i++) çarpım += i;
  
    return çarpım;
  } // long fakt(byte) sonu
  ...
} // Faktöryel sınıfının sonu
$ javac -encoding UTF-8 Faktöryel.java
$ java Faktöryel 5
5!: 15 ✘
Görüldüğü gibi, sözdizimsel bir hata bulunmamakla birlikte, faktöryel kavramının hatalı gerçekleştiriminden kaynaklanan bir mantıksal hata ortaya çıkmıştır. Derleyicinin müdahele ederek işaretli satırdaki + yerine * yazılması gerektiği yorumunu yapması olanaksızdır. Bu gibi hataların, kodun sınanması sırasında keşfedilerek programcı tarafından ortadan kaldırılması gerekir. Bir an için bunun yapıldığını ve hatanın düzeltilerek doğru bir gerçekleştirimin sağlandığını düşünelim. Bu durumda, programımızın çalıştırılması aşağıdaki sonuçları verecektir.
$ java Faktöryel 5
5!: 120 ✔
$ java Faktöryel 20
20!: 2432902008176640000 ✔
$ java Faktöryel 21
21!: -4249290049419214848 ✘
Bir ihtimal Java'daki tamsayı türlerinin taşma davranışını bilmeyenleriniz, herhangi bir sayının eksi değerli bir faktöryele sahip olduğuna şaşırabilir. Bu gruptaki arkadaşlara önerim, daha fazla ilerlemeden şu yazıya🔎 bir göz atmaları. Diğer arkadaşlar ise, bunun sebebinin çarpım sonucunun long değerler için söz konusu olan aralığa düşmemesi nedeniyle kırpılması olduğunu bileceklerdir. Peki bu durumda, faktöryel metodumuzun doğru olmadığını söyleyebilir miyiz? Faktöryelin tanımını bilen herkesin bu soruya hayır yanıtı vermesi gerekir; ortada olan şey bir hatadan çok yetersizliktir ve muhtemelen metodun kullanıcısı ile iletişim eksikliğinden kaynaklanmaktadır. Soruna değişik şekillerde çözüm getirilebilir.
  1. Gerçekleştirime dokunulmaz ve kullanıcıya sağlanan belgelerde metodun 0 ile 20 arasındaki tamsayılar için doğru çözümler ürettiği ve dolayısıyla sağlanması beklenen argüman değerinin bu aralıkta olması gerektiği söylenir.
  2. Duruma has bir başka çözüm sağlanarak eksiklik giderilir. Örneğimizde, dönüş türü olarak long yerine java.math.BigInteger türünün kullanılması ve kodda buna göre değişikliklerin yapılması işimizi görecektir.
  3. Geçirilen argüman değerinin beklenen aralıkta olup olmadığı denetlenir. Bu, basit bir if komutuyla yapılabileceği gibi doğruluk savı (İng.; assertion) denetimiyle de yapılabilir. İlk yolun seçilmesi durumunda, görülen olumsuzluk dönüşte [0 gibi] özel olarak yorumlanan bir değerle belirtilebileceği gibi söz konusu durumu özetleyen bir ayrıksı durum (İng., exception(al condition)) nesnesi ile de bildirilebilir.
İkinci ve üçüncü maddelerdeki yaklaşımların nasıl gerçekleştirilebileceğini aşağıdaki alternatif çözümü izleyerek görelim. Özyinelemeli olan yeni metodumuzda, argümanın eksi olması hali, ki bu beklenilmeyen bir kullanıma işaret eder, çağırıcıya IllegalArgumentException türündeki bir ayrıksı durum nesnesi ile bildirilirken, argümanın 0 veya 1 olması halinde 1, geri kalan durumlarda ise matematikten tanıdık n * (n-1)! değeri döndürülmektedir. java.lang paketinde tanımlanan IllegalArgumentException, sağlanan argüman değerinin, -5'in faktöryelinin bulunmaya çalışılmasında olduğu gibi, uygunsuz olduğuna işaret eder.
...
import java.math.BigInteger;

public class Faktöryel {
  public static void main(String[] ksa) {
    ...
    long l = Long.parseLong(ksa[1]);
    try { out.println(l + "!: " + fakt2(l)); }
      catch(IllegalArgumentException a) {
        out.println(l + "!: " + fakt2(Math.abs(l)) + "!!!");
      }
  } // void main(String[]) sonu
  ...
  public static BigInteger fakt2(long n) {
    if (n < 0)
      throw new IllegalArgumentException(String.valueOf(n));
    if (n < 2)
      return BigInteger.ONE;
      else return BigInteger.valueOf(n)
                            .multiply(fakt2(n - 1));
  } // BigInteger fakt2(long) sonu
} // Faktöryel sınıfının sonu
$ java Faktöryel 21 21
21!: -4249290049419214848 ✘
21!: 51090942171709440000 ✔
$ java Faktöryel 5 -5
5!: 120 ✔
-5!: 120!!!
Beklenmedik koşulların oluştuğu noktada, özet bilgi içeren bir ayrıksı durum nesnesinin yaratılıp throw komutu ile fırlatılması gerekir. Bunun sonrasında, ortaya çıkan durumun çözümünü bilen birisinin duruma el atıp bir şeyler yapması beklenir. Biraz düşünüldüğünde, çözümün içine düşülen duruma sebep olanlar tarafından sağlanabileceği görülecektir. Bu ise, ayrıksı durumun ortaya çıktığı noktaya gelinene kadar izlenen çağrı zinciri üzerindeki noktalar demektir. Bir diğer deyişle, çare ya sorunun ortaya çıktığı noktada ya da o noktaya gelinmesi ile son bulan çağrı yığıtındaki diğer metotlarda bulunabilir. Çözüme dair bir şeyler yapabileceğini düşünenlerin bu iddialarını try-catch yapısı içinde bildirmeleri beklenir. try, takip eden kıvrımlı ayraç çifti arasındaki komutların sorun çıkarabileceğini bildirirken, kotarıcı olarak adlandırılan catch bloğu/blokları iliştirildikleri korumalı bölgede ortaya çıkabilecek sorunların tümü veya bazıları için çözümler sunar. Çağrı zinciri üzerindeki noktaların hiçbirinde çözüm önerilmemesi halinde ise, top denetim akışı geriye sarılarak programı başlatan JSM'ye atılacak ve JSM de ortaya çıkan durumu oluştuğu yerden başlayarak nerelere uğrayarak çözmeye çalıştığını söyleyen bir raporu standart hata ortamına2 yazarak programı sonlandıracaktır. Bunun nasıl olduğuna aşağıdaki çıktıya göz atarak bir bakalım.
...
public class Faktöryel {
  public static void main(String[] ksa) {
    ...
    long l = Long.parseLong(ksa[1]);
    out.println(l + "!: " + fakt2(l));
  } // void main(String[]) sonu
  ...
  public static BigInteger fakt2(long n) {
    if (n < 0)
      throw new IllegalArgumentException(String.valueOf(n));
    ...
  } // BigInteger fakt2(long) sonu
} // Faktöryel sınıfının sonu
$ java Faktöryel 5 -5
5!: 120 ✔
Exception in thread "main" java.lang.IllegalArgumentException: -5
 at Faktöryel.fakt2(Faktöryel.java:20)
 at Faktöryel.main(Faktöryel.java:9)
JSM'ye bakılacak olursa, programın [main adına sahip] ana izleğinde Faktöryel sınıfının main metodu içinden 9. satırda aynı sınıftaki fakt2 metodu çağrılmış ve denetim akışı bu metottta iken 20. satırda IllegalArgumentException ayrıksı durumu ortaya çıkmış. Anlamadıysanız ikinci bir örnekle açıklık getirmeye çalışalım; anlayanlar da bu sayede pekiştirmiş olurlar.
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.Scanner;

public class Örnek {
  public static void main(String[] ksa) {
    // ...
    m1();
    // ...
  } // void main(String[]) sonu

  private static void m1() {
    // ...
    m2();
    // ...
  } // void m1() sonu

  private static void m2() {
    String dosyaAdı;
    Scanner grdKanalı = new Scanner(System.in);
    System.out.print("Dosya adını giriniz: ");
    dosyaAdı = grdKanalı.nextLine();
    // ...
    m3(dosyaAdı);
    // ...
  } // void m2() sonu

  private static void m3(String dosyaAdı) {
    BufferedReader dosya =
      new BufferedReader(new FileReader(dosyaAdı));
    // ...
  } // void m3(String) sonu
} // Örnek sınıfının sonu
İşaretli satırlarda, argümanda geçirilen ada sahip ve o anki çalışma dizini içinde bulunan bir dosyanın tamponlanarak ve karakter yönelimli bir biçimde okunması için uygun türden bir akak nesnesi yaratılıyor. Bu işlem, sağlanan ada sahip bir dosyanın bulunmaması halinde java.io.FileNotFoundException ayrıksı durumu ile sonlanacaktır. Dolayısıyla, programın derlenip var olmayan bir dosyanın adı verilerek çalıştırılması, yukardakine benzer bir çağrı yığıtı özetinin üretilmesine neden olacaktır diye düşünebiliriz. Ancak, gelin görün ki, program derlendiğinde durumun hiç de öyle olmadığı görülür.
$ javac -encoding UTF-8 Örnek.java 
Örnek.java:29: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
      new BufferedReader(new FileReader(dosyaAdı));
                         ^
1 error
Yukarıdaki çıktıdan da görülebileceği gibi, IllegalArgumentException için sesini bile çıkarmayan derleyici, iş FileNotFoundException'a geldiğinde yaygarayı basmaktadır. Çifte standardın sebebi, ayrıksı durumların iki kategoriye ayrılmasıdır:
  • RuntimeException köklü sınıf sıradüzeninde olanlar: Bu kategoriye giren ayrıksı durumlar, ya ortaya çıktıkları noktada yakalanıp kotarılmalı ya da çağrı yığıtındaki diğer metotlardan birinde kotarılması gerektiğini hatırlatmak için metot imzasında ilan edilmelidirler.
  • Diğerleri: Bu ayrıksı durumların ne yakalanması ne de, yakalanmadıkları takdirde, metot başlığında ilan edilmeleri zorunludur.
Dolayısıyla, ilk kategoriye giren FileNotFoundException ayrıksı durumunun ortaya çıkması olasılığının belirmesiyle derleyici bizim kotarmak ya da bu görevi ihale etmek yönünde bir karar verdiğimizi görmek ister. Bu istemin karşılanmaması halinde ise söz konusu eksikliğe dikkat çekerek derlemenin başarısızlıkla sonlandığını bildirir. Burada bir noktanın özellikle vurgulanması yerinde olacaktır: derleyici, kodu okuyup var olmayan bir dosyanın girileceğinden emin olduğu için değil, böyle bir olasılığın var olduğu için itiraz etmektedir. Derleyicinin gözünde, FileReader sınıfının kullanılan yapıcısı FileNotFoundException türlü bir ayrıksı durum nesnesi fırlatabilir ve programcının da buna karşı hazırlıklı olduğunu kanıtlaması gerekir.

Kotarımın sorunun çıktığı nokta yerine çağrı yığıtındaki diğer metotlara bırakıldığı metot başlığına eklenen throws bildirimi ile ilan edilir. Buna bir örnek aşağıda verilmiştir: m3'te ortaya çıkabilecek sorun öngörülmüş fakat metot içinde çözülmektense metot başlığında belirtilmek suretiyle çağrı yığıtındaki diğer metotlara bırakılmıştır. Daha kesin ifade edecek olursak, m3'te ortaya çıkabilecek sorun için top önce m2'ye ve oradan da m1'e atılmış ve hal çaresi bu metot içinde sağlanmıştır.
...
import java.io.FileNotFoundException;

public class Örnek {
  public static void main(String[] ksa) {
    // ...
    m1();
    // ...
  } // void main(String[]) sonu

  private static void m1() {
    // ...
    try { m2(); } catch(FileNotFoundException a) { ... }
    // ...
  } // void m1() sonu

  private static void m2() throws FileNotFoundException {
    ...
    // ...
    m3(dosyaAdı);
    // ...
  } // void m2() sonu

  private static void m3(String dosyaAdı)
    throws FileNotFoundException {
    BufferedReader dosya =
      new BufferedReader(new FileReader(dosyaAdı));
    // ...
  } // void m3(String) sonu
} // Örnek sınıfının sonu
Kotarıcıların iliştirildikleri korumalı bölgeye özel oldukları unutulmamalıdır. Bundan dolayı, aşağıdaki kod parçası derleyici tarafından kabul görmeyecektir; kotarıcı m2 metodunun sadece birinci kullanımına çözüm sağlamaktadır.
private static void m1() {
    // ...
    try { m2(); } catch(FileNotFoundException a) { ... }
    // ...
    m2(); // ⇒ Derleme hatası!
    // ...
  } // void m1() sonu
Bu noktada, neden iki kategori olduğunu soruyor olabilirsiniz. Aşağıdaki örnekle anlamaya çalışalım. Belli ki, bu basit programı yazan arkadaş Java'da dizilerin 0-temelli indislere sahip olduğunu unutmuş ve üç elemanlı bir int dizisi yarattıktan sonra üçüncü eleman yerine 3 indisiyle gösterilen elemanın değerini 4 olarak güncellemek istemiş. Yani, Java programlama bilgisi eksik olan arkadaş derleme zamanında yakalanamayan bir hata yapmış. Takdir edersiniz ki, böylesine bir hatanın kotarıcıda düzeltilmesi ve programa devam edilmesi olanaklı değildir. Yapılması gereken, programcı arkadaşımızın dizileri daha iyi öğrenmesi ve kullanıcıdan 0..2 aralığında değer istemesi gerektiğini farketmesidir. Çünkü, ayrıksı durum düzeneği programın beklenmedik durumlarda daha sağlıklı bir tepki vermesini sağlamak için kullanılır, kötü programcıların programlama hatalarını düzeltmesi gibi erişilmesi olanaksız bir amaç için değil.
import java.util.Scanner;

public class DiziKullanımı {
  public static void main(String[] ksa) {
    int[] iDz = {1, 2, 3};
    Scanner grdKanalı =  new Scanner(System.in);
    System.out.print("1..3 aralığında bir sayı giriniz: ");
    iDz[grdKanalı.nextInt()] = 4;
  } // void main(String[]) sonu
} // DiziKullanımı sınıfının sonu
$ javac -encoding UTF-8 DiziKullanımı.java
$ java DiziKullanımı
1..3 aralığında bir sayı giriniz: 3
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
 at DiziKullanımı.main(DiziKullanımı.java:8)
Benzer bir gözlem null değerli bir tutacak yoluyla ileti gönderilmeye çalışılması sonrasında ortaya çıkan NullPointerException için de yapılabilir. Sıkça kendini gösteren bu ayrıksı durumun da sebebi programlama hatasıdır ve kotarılması olanaksızdır. Dolayısıyla, yakalanması veya metot başlığında ilan edilmesi pek anlamlı olmayacaktır.3 Buna karşılık, bir dosyanın olmaması kullanıcının dosya adını giren kullanıcının dikkatsizliğinden kaynaklanabilir. Doğal olarak, bu beklenmedik durumun öngörülerek kotarıcıda ikinci bir hak verilerek programın boş yere sona ermesinin önüne geçilmelidir.

Peki, ya sizin ve kullanıcının iradesi dışında gelişen sebepler nedeniyle programınız göçecek olursa. Mesela, arkadaşınız ortaklaşa kullandığınız bilgisayardan programınızın çalışması için gerekli olan bir sınıf dosyasını silecek olursa, ne olur? Ya da, dosyanın diskte kaydedildiği sektörlerden birinin bozulması nedeniyle sınıf dosyasının okunarak içselleştirilmesi söz konusu olamazsa, ne olur? Ya da ya da, programınızın işlemesi için gereksinilen kaynaklar JSM tarafından sağlanamayacak kadar büyük olmaya başlarsa, ne olur? Gelin bu soruların yanıtını Faktöryel sınıfını ikinci komut satırı argümanı çok büyük bir sayı ile deneyerek birlikte görelim.
$ java Faktöryel 5 5827 2> StackOverflowError.txt
5!: 120 ✔       
Standart hata ortamını yönlendirerek hata mesajlarını StackOverflowError.txt dosyasına gönderen yukarıdaki komutun ürettiği bilgi incelendiğinde, fakt2'nin sürekli kendini çağırdığı ve programın StackOverflowError ile sonlandığı görülecektir. Yani, JSM özyinelemeli çağrılar sonucu genişleyen çağrı yığıtını doldurmuş ve taşmanın vuku bulduğu noktada imdat çağrısını yapmıştır. Bu hatanın ortaya çıkışı, JSM'nin yararlandığı yığıt büyüklüğünü -Xss512k veya -Xss1m gibi bir opsiyonla büyüterek ertelenebilir. Ancak; işin özünde çaresiz olan JSM'dir ve belleğin bittiği yerde o da bir şey yapamayacaktır. Bir diğer deyişle, beklenmedik durum dış kaynaklıdır ve programcının yapacağı fazla bir şey yoktur. Örneğimizde, özyinelemeli çözümü for döngülüsüyle değiştirerek belki bir şeyler yapabiliriz ama bu her zaman mümkün olmayacaktır. İş böyle olunca, kotarımdan söz etmek de mantıksızdır.

Bu ana kadarki bilgilerimizi maddeleyerek özetleyecek olursak aşağıdaki listeyi elde ederiz. Java programları, iç ve dış koşullara göre beklenenden farklı bir şekilde davranabilir. Davranış sapması programcının müdahele edemeyeceği dış kaynaklardan ötürüyse, programcı Error köklü sıradüzenindeki sınıflardan birisinin nesnesi ile haberdar edilir. Yok eğer oluşan durum içselse ve programcı tarafından çözüm bulunabilecek gibi gözüküyorsa, programcının ihtiyacının duyduğu bilgi RuntimeException köklü sıradüzenindeki sınıflardan birisinin nesnesi ile sağlanır. Aksi takdirde, programcının yaptığı mantıksal bir hata dönüşerek ayrıksı durum haline gelmiştir ve bu, programcının dersini tamamlaması için, Exception köklü sıradüzenindeki diğer sınıfların nesneleri ile haber verilmelidir.

  • Throwable: Hata ve ayrıksı durumlar
    • Error: JSM'nin göçmesine neden olan hatalar
    • Exception: Ayrıksı durumlar
      • RuntimeException ve altsınıfları: Kotarılması zorunlu olmayanlar
      • Kotarılması zorunlu olanlar

Hangi kategoriye giriyor olursa olsun, programcı tüm koşullar altında elinden gelenin en iyisini sergilemelidir. Mümkünse, kotarıcıdaki kod ile programın sağlıklı bir şekilde devamı sağlanmalı, bu mümkün olmuyorsa, nasıl olsa bir şey yapamıyorum, denilerek oturmaktansa programın zarif bir şekilde bitmesi için elden gelen yapılmalıdır. Mesela, kullanılmakta olan dış kaynaklar kotarıcıda döndürülmeli, programın bakıcısının muhtemel bir programlama hatasını bulmasını kolaylaştırmak adına bir seyir defterine neden ile ilgili bilgiler not düşülmelidir. Bu noktada karşımıza, ayrıksı durum oluşsa da oluşmasa da çalıştırılacak kodu içeren bir kotarıcı olarak finally çıkıyor. Bir örnek ile görelim.
public void m() {
  ...
  try {
    FileReader dosya = new FileReader(...);
    ... 
  } catch (java.io.IOException a) { ... }
    catch (ADurumu a) { return; }
    ...
    catch (Exception a) { ... }
    finally { dosya.close(); ... }
  ...
} // void m() sonu
Birden çok kotarıcıya sahip yukarıdaki kod parçasında, try bloğunun işlenmesi sırasında oluşacak duruma göre farklı kotarıcılar devreye girebilir ya da işler yolunda giderse denetim akışı finally sonrasındaki komutla devam eder. Ancak, ne olursa olsun, finally kotarıcısı herhalükâda işlenecektir. Bu, başka hiçbir şey yapmadan çağırıcıya dönen ADurumu ayrıksı durumu için de geçerlidir; çünkü metottan dönüş öncesinde işlenmemiş finally kotarıcıları sırayla işlenir. Dolayısıyla, ortaya çıkan ayrıksı durum nedeniyle işlenen kotarıcıda metottan dönülse bile kapatılamamış olan dosya muhakkak kapatılacaktır.

Kod parçasında dikkat çekilmesi gereken bir diğer nokta, kotarıcıların yerleştiriliş sırasının önemli olduğudur. Bir kotarıcı sadece argümanındaki sınıfa ait nesneleri değil, o sınıfın kökü olduğu sıradüzeni içindeki tüm sınıfların nesnelerini yakalar. Örneğin, yukarıdaki ilk kotarıcı IOException nesnelerini yakaladığı gibi, FileNotFoundException'ın da içinde bulunduğu pek çok sayıda sınıfın nesnelerini de yakalayacaktır. Benzer şekilde, Exception argümanlı kotarıcı, kendisinden önce geçen kotarıcılarda yakalanmayan tüm ayrıksı durum nesnelerini yakalayarak amansız—bir o kadar da anlamsız—bir gümrük memuru görevini görecektir. Böylesine bir kotarıcının başa konulması—genelde, bir üstsınıf türlü argümana sahip kotarıcının altsınıf türlü argümana sahip kotarıcıdan önce gelmesi—her şeyi yakaladığı için takip eden kotarıcıları anlamsız kılacaktır. Kotarıcıların, özelden genele olacak şekilde sıralanması gerekir.

Evet, geldik kendi ayrıksı durumunu kendin pişirmeye. Ama, yazımızın vardığı uzunluğu ve muhtemel konsantrasyon azalmasını düşünerek bunu bir başka yazıya bırakalım. Onun için, şimdilik bu kadar. Unutmayın, hatasız program(cı) olmaz; birazcık defansif programlama ortaya çıkabilecek zararın faturasını azaltır. Onun için sadece doğruluk değil, dayanıklılığa da önem verin. Yani, güvenilir yazılım üretin.


  1. Yazılımın benzetimi yapılan sürecin geliştiriciler (alan uzmanı, gereksinim toplayıcı, mimar, tasarımcı ve gerçekleştirimci) tarafından yorumlanması ile ortaya çıktığı unutulumamalıdır. Dolayısıyla, doğruluk özelliğini sürecin algılanışına göre yapmak daha doğru olur. ↑
  2. Standart hata ortamı, bir programın işleyişi sırasında üretilen hata mesajlarının gönderildiği aygıttır ve yeniden yönlendirme yapılmadığı veya System.setErr metodu kullanılarak değiştirilmediği müddetçe standart çıktının da gönderildiği varsayılan aygıt olan ekrandır. ↑
  3. Yapılan ayrımın bir diğer olumlu yanı, programların fazladan try-catch yapıları ve throws bildirimleri ile doldurulmamasıdır. Örnek olarak verdiğimiz iki ayrıksı durum düşünüldüğünde—tüm dizi erişimi ve ileti gönderimlerinin try-catch yapısı içine alındığını veya bu işlemleri içeren metotların imzasına ayrıksı durum ilanının eklendiğini düşünün—kotarımın zorunlu olmamasının kaynak kodun okunabilirliğine yaptığı katkı takdir edilecektir. ↑

14 Ekim 2011 Cuma

Java Platformunun Dikkate Değer Bir Dili: Scala-2


Scala üzerine ikinci yazımızda bu JSM dilinin sağladığı sınıf tanımlama imkânına daha yakından bakacağız ve bunu yaparken de Java ile olan farklılıklara değinmeye çalışacağız.1 Matematikteki kesirli sayı kavramını soyutlayan aşağıdaki sınıf iskeletiyle başlayalım.2

Kesir.scala
package com.ta.matematik
...
class Kesir(pay: Long = 1, payda: Long = 1) {
  require(payda != 0)
  private var (_pay, _payda) = (pay, payda)
  sadeleştir()
  ...
  private def sadeleştir() = { ... }
} // Kesir sınıfının sonu
Java'dan gelenlerin gözüne çarpacak farklılıklardan belki de ilki, public niteleyicisinin sırra kadem basmış olması. Bu durumun sebebi, paket çapında erişimin varsayıldığı Java'dan farklı olarak, Scala'da programlama öğelerinin nitelenmemeleri halinde public erişime sahip kılınmasıdır. Dolayısıyla, Kesir ve erişim niteleyicisi bulunmayan tüm sınıf öğeleri herkes tarafından kullanılabilecektir. Bunun uygun olmaması halinde, programcının niyetini protected veya private niteleyicilerinden birini kullanarak bildirmesi gerekir. Bu noktada, protected niteleyicisinin paket içindeki türleri kapsamayıp, söz konusu öğeyi sadece o anki türden doğrudan veya dolaylı bir biçimde kalıtlayan türlere erişilebilir kıldığını söylemekte yarar vardır.3 Ayrıca, paket çapında erişimden de bahsedilmediği dikkatinizi çekmiştir. Ancak, telaşa düşmeyin; nitelenmekte olan öğeyi içeren paket, sınıf veya tek-örnekli sınıf adlarından birinin protected veya private ile birlikte belirtilerek daha rafine erişim politikalarının ifade edilmesi mümkündür. Örneğin, private yerine private[com.ta.matematik] nitelemesinin yapılması, sadeleştir adlı metodu sınıfa özel olmaktan çıkaracak ve com.ta.matematik paketindeki tüm öğelere görünür kılacaktır.

Java'dan gelenleri başlarda şaşırtabilecek bir diğer nokta, nesnenin yaratılması esnasında yapıcıya geçirilmesi beklenen argümanların özelliklerini belirten parametre listesinin sınıf adı sonrasında bulunmasıdır. Buna göre, Kesir sınıfının örneği ks1 = new Kesir(6, 10) şeklinde yaratılabilecektir. İlkleme ise Kesir sınıfının tanımındaki metotların dışında kalan tüm öğelerin koddaki geçiş sırasında işlenmesiyle yapılacaktır. Yani, sınıf gövdesindeki söz konusu öğeler nesne ilkleme bloğu görevini görecektir; Scala terminolojisiyle konuşacak olursak, sınıf gövdesindeki öğeler birincil yapıcı olarak ele alınacaktır. Örneğimiz üzerinden gidecek olursak; Predef adlı tek örnekli sınıftan ithal edilen require metodu ile geçirilen paydanın uygunluk denetiminin yapılmasını takiben, yaratılmakta olan nesnenin altalanları olan _pay ve _payda koşut bir biçimde pay ve payda ile ilklendikten sonra yaratılmakta olan kesir sadeleştirilecektir. Bu noktada, koşut atamanın tüm kollarının aynı anda işleniyormuş gibi düşünülmesi gerektiği unutulmamalıdır.

Haklı olarak, Predef sınıfının nereden çıktığını sorabilirsiniz. Yanıtı, Scala derleyicisinin, Java derleyicisinin java.lang paketini otomatikman ithal edilmiş kabul etmesindeki gibi, java.lang ve scala paketleriyle scala.Predef tek örneklisini otomatikman tüm programlara ithal etmesinde yatar. Bunun sonucunda, Predef tanımı da evrensel olarak görünür hale gelecek ve bazı işlemler öncesinde önkoşul denetimine yarayan require metodu da yukarıdaki gibi kullanılabilecektir.

Bir diğer farklılık, Java'da aynı adlı metotların ezilmesi ile öykünülen varsayılan argümanların kullanımı. Scala 2.8'den başlayarak geçerli olan bu özellik sayesinde, varsayılan değerin uygun olması durumunda, argümanın es geçilmesi de olanaklıdır. Örneğin, yukarıdaki sınıf tanımının geçerli olduğu bir ortamda, tamsayı = new Kesir(7) ve bir = new Kesir(), sırasıyla, 7/1 ve 1/1 değerlerini temsil eden kesirli sayıları yaratacaktır. Bu noktada, varsayılan argümanların metot imzasının sonunda yer alması zorunluluğu unutulmamalıdır. Dolayısıyla, 1/7 kesrini temsil eden nesnenin yaratılması için her iki argümanın da geçirilmesi gerekecektir.

Scala 2.8 ile birlikte, ileti gönderileri ve metot çağrıları sırasında argümanların parametre adları kullanılarak konumlarından farklı bir sırada geçirilmesi de olanaklı kılınmıştır. Buna göre, önceki paragraflardaki ks ve tamsayı tanımlayıcılarının, sırasıyla, new Kesir(payda = 5, pay= 3) ve new Kesir(pay= 7) şeklinde tanımlanması mümkün olacaktır. Bu sayede, birBölüYedi = new Kesir(payda = 7) tanımlamasının da geçerli olması sağlanarak 1/7 kesrine karşılık gelen nesnenin tek argüman geçirilerek yaratılması da mümkün olmaktadır.

Yukarıdaki örnekte varsayılan argümanlar yardımıyla sağlanan değişik sayıda argümanlarla kullanılabilen yapıcı metotlar görüntüsü, yardımcı yapıcı metotlar yoluyla da sağlanabilir. Varsayılan argümanlarla birlikte de yararlanılabilecek bu yaklaşımda, programcı ilk iş olarak uygun argümanlarla birincil yapıcıyı veya diğer yardımcı yapıcılardan birini çağıran this adına sahip metotlar yazar. Örneğin, aşağıdaki kod parçasında iki argümanın da sağlanması durumunda birincil yapıcı çağrılırken, bir veya sıfır argümanın sağlanması halinde, sırasıyla, 7. ve 8. satırlardaki yardımcı yapıcılar çağrılacaktır. Dikkat ederseniz, her iki yapıcı da işini bir diğer yapıcıya havale ederek görmekte.
...
class Kesir(private var _pay: Long,
            private var _payda: Long) {
  require(_payda != 0)
  sadeleştir()
  ...
  def this(_pay: Long) = this(_pay, 1)
  def this() = this(1)

  def pay = _pay
  def payda = _payda
  ...
  override def toString() = _pay + "/" + _payda
  ...
} // Kesir sınıfının sonu
Birincil yapıcının parametre listesindeki değişiklik de gözünüze çarpmıştır, Tanımının önüne var niteleyicisinin konulmasıyla ilişkin parametrenin değişken içeriğe sahip kılınması nedeniyle, daha önceki kod parçasında olduğu gibi yapıcıya geçirilen argümanların değişken içerikli altalanları ilklemesi ve değişikliklerin bu altalanlarda yapılması artık gerekli değildir. Ancak, birincil yapıcıya özel bu durum, Scala'nın atıf geçirme (İng., pass by reference) yöntemini desteklediği şeklinde yorumlanmamalıdır; Java'da olduğu gibi, Scala'da da argümanlar ilişkin metoda değer geçirme (İng., pass by value) yöntemiyle sağlanır.

Java'dan tanıdık gelecek bir nokta, üstsınıflardan kalıtlanan bir metodun ezilmekte olduğunu bildiren override niteleyicisidir. Ancak, Java dengi @Override açımlamasının kullanımı seçimli olup sadece tavsiye edilirken, Scala'daki bu niteleyicinin kullanımı aynı imzaya sahip metotların varlığında zorunludur. Bu kurala uyulmaması, derleyicinin hata mesajıyla karşılanacaktır.

toString'i bir yerlerden gözünüzün ısırdığını düşünüyorsanız, belleğinizin sizi aldatmadığını söyleyebilirim; Java'dan bildiğimiz bu ileti, Scala'da da hoş yazım amacıyla kullanılıyor. Tıpkı Java'da olduğu gibi, programcıların belli bir türe ait değerlerin hoş yazımı için kök sınıf tarafından sağlanan ilişkin metot gerçekleştirimini ezmesi gerekiyor. Ancak, toString'in Scala tür sıradüzeni içinde nereden nasıl kalıtlandığını daha iyi anlamak için, Java'da olmayan bir üstkavramın tanıtılmasında yarar var: çeşni (İng., mixin). Kaynak kodda trait anahtar sözcüğüyle karşılık bulan bu programlama kavramının anlaşılması, Java ve Scala türleri arasındaki etkileşimi daha iyi kavramak için de yardımcı olacaktır. O zaman, karşılaştırılabilirlik kategorisini tanımlayan scala.math.Ordered çeşnisinin Scala'nın resmi sitesindeki gerçekleştirimine ve Kesir sınıfına söz konusu çeşninin nasıl katıldığına göz atarak bu üstkavramı anlamaya çalışalım.

Soysal olan Ordered çeşnisi, başlığındaki kalıtlama ilişkisinden de görülebileceği gibi, Java platformundaki karşılaştırılabilir nesnelerin kategorisini tanımlayan Comparable arayüzünden kalıtlar. Scala türlerinin Java'dakileri geliştirebileceğine örnek oluşturan bu başlığın anlamı, işini compare metoduna havale ederek gören compareTo metodu gerçekleştiriminden de gözlemlenebilir. Böylece, Scala'da yazılan bir sınıfın nesneleri Java programları içinden de kullanılabilecektir.

Ordered.scala
package scala.math

trait Ordered[A] extends java.lang.Comparable[A] {
  def compare(sağ: A): Int
  def <(sağ: A): Boolean = (this compare sağ) <  0
  def >(sağ: A): Boolean = (this compare sağ) >  0
  def <=(sağ: A): Boolean = (this compare sağ) <= 0
  def >=(sağ: A): Boolean = (this compare sağ) >= 0
  def compareTo(sağ: A): Int = compare(sağ)
} // Ordered[A] çeşnisinin sonu

object Ordered { 
  implicit def orderingToOrdered[T](x: T)
    (implicit ord: Ordering[T]): Ordered[T] = 
    new Ordered[T] {
      def compare(sağ: T): Int = ord.compare(x, sağ)
    }
} // Ordered tek-örneklisinin sonu
Ordered çeşnisinin gövdesine baktığımızda, tanımlanan kategorideki iki Scala nesnesinin—ileti alıcı (this) ve sağ—karşılaştırılma sonucunu döndüren compare metodunun gövdesi verilmeden sağlandığını görüyoruz. Bu durum, Scala derleyicisi tarafından söz konusu metodun soyut olarak ele alınacağı anlamını taşır; derleyicinin yaptığı bu varsayım yüzünden programcının ayrıca çeşniyi veya metodu soyut olarak nitelemesine gerek yoktur.

Yukarıdaki kod parçasının ortaya koyduğu bir diğer önemli nokta, Scala'nın, Java'nın aksine, işleçlerin aşırı yüklenmesini—ya da, işin doğrusunu söylemek gerekirse, bu tür bir yanılsamayı yaratabilecek özellikleri—desteklediğidir. Öncelikle, tanımlayıcı adlarının oluşturulmasında kullanılan karakterler kullanageldiğimiz işleçlerin simgelerini de kapsayan daha geniş bir yelpazeden seçilebilir.4 Ayrıca, tek argüman alan iletiler işleç ortada sözdizimiyle de kullanılabilir. Örneğin +, topla veya add kadar geçerli bir tanımlayıcı adıdır. İsteyecek olursak, değişken/sabit veya ileti/metot adlarını topla veya add yerine + olarak da seçebiliriz. Aynı zamanda, adı ne şekilde verilmiş olursa olsun, tek argüman alan iletileri, alıcı.ileti(arg) yerine alıcı ileti arg şeklinde de gönderebiliriz. Dolayısıyla, yukarıdaki this compare sağ ifadesi this.compare(sağ) ile eşdeğerdir.

Kod parçamızda dikkat çeken bir diğer bölüm, tek-örneklimizin tanımında geçen implicit anahtar sözcüğüdür. Bu sözcük, söz konusu metodun, programcının açıkça kullanması dışında kimi zaman arka planda derleyicinin sentezlediği kod tarafından da çağrılabileceğini belirtir. Örneğimizde olduğu gibi dönüşüm amacıyla kullanılan bu tür metotlar, nesne tutacağını argüman türünden (Ordering) eşlik eden türe (Ordered) çevirir. Mesela, Ordering çeşnisi tutacağıyla bir nesneye ileti gönderilmesi ve bu iletinin geçerli olmadığının anlaşılması durumunda, derleyici yukarıdaki ve benzeri metotlardan birini usulca kullanarak nesneyi başka bir açıdan görecek ve program hata vermeden devam edecektir.

Tanımlanmış bir çeşni, bir diğer çeşni tarafından kalıtlanmak suretiyle geliştirilebileceği gibi, içerdiği soyut öğelerin sağlanması ve/veya bazı öğelerinin ezilmesi yoluyla bir sınıfa katılabilir. Her iki durum da extends seçilmiş sözcüğü ile ifade edilir. Ancak; sınıfın bir başka sınıftan kalıtlaması halinde, extends üstsınıfı belirtmek için kullanılırken, sınıfa katılan çeşniler with anahtar sözcüğü ile belirtilir. Çeşniler arası kalıtlama ilişkisinin çoklu olmasının yanısıra, bir sınıf birden çok çeşniyi gerçekleştirebilir.

Ordered çeşnisinin Kesir sınıfı tarafından gerçekleştirilmesi aşağıda verilmiştir. Bu tanıma göre, yaratılacak Kesir ve Kesir'den kalıtlayan tüm sınıfların nesneleri karşılaştırılabilir nesneler kategorisine gireceklerdir. Bu özellik, Ordered çeşnisinin Comparable'dan kalıtlaması nedeniyle, söz konusu nesnelerin sadece Scala programlarında kullanılmaları halinde değil, Java ve diğer Java platformu dillerinde yazılmış programlar içinden kullanılmalarında da geçerli olacaktır. Mesela, Kesir nesneleri ile doldurulmuş bir liste java.util paketindeki Collections.sort metodu ile sıralanabildiği gibi, scala.collection.immutable.List sınıfının sort metoduyla da sıralanabilir.
...
import scala.math

class Kesir extends Ordered[Kesir] {
  ...
  def equals(sağ: Kesir) = compare(sağ) == 0
  
  def compare(sağ:Kesir) = {
    val fark = this - sağ
    if (fark._pay < 0) -1
    else if (fark._pay > 0) 1
      else 0
  } // compare(Kesir): Int sonu
  ...
  def -(sağ: Kesir): Kesir = {
    val pay = _pay * sağ._payda - _payda * sağ._pay

    new Kesir(pay, _payda * sağ._payda).sadeleştir()
  } // -(Kesir): Kesir sonu
  ...
} // Kesir sınıfının sonu
Kesir sınıfındaki equals metodunun Ordered çeşnisindeki compare ile uyumlu olacak şekilde ezilerek gerçekleştirildiği gözünüze çarpmıştır. Sakın ola ki, Java'yı iyi bilen biri olarak, bunun eşitlik denetimi işlecini (==) etkilemeyeceğini düşünmeyin. Çünkü, Scala'da equals ile == her zaman aynı şekilde çalışır: kök sınıftaki equals gerçekleştiriminin bir sınıf tarafından ezilmesi == işlecinin de anlamını değiştirir. Eşitlik denetimine ek olarak aynılık denetimi isteyenlerin, eq ve onun değillemesi olan ne iletilerini kullanması tavsiye edilir.

Bir sınıfa çeşni katılması nesnenin yaratıldığı noktada, dinamik olarak da gerçekleştirilebilir. Örnek olarak, KesirEksik sınıfının Ordered çeşnisi katılmadan tanımlanmış olduğunu varsayalım. Bu takdirde, aşağıdaki kod parçasından da görebileceğiniz gibi, bu sınıfa ait [1/11 değerine sahip] bir nesne Java'daki adsız sınıflara benzer bir tanımla söz konusu çeşniye sahip kılınabilir.
val ks =
  new KesirEksik(3, 33) with Ordered[KesirEksik] {
    def compare(sağ: KesirEksik) = {
      val fark = this - sağ
      if (fark.pay < 0) -1
      else if (fark.pay > 0) 1
        else 0
    } // compare(KesirEksik): Int sonu
  
    def equals(sağ: KesirEksik) = compare(sağ) == 0
  } // Kesir'i geliştiren adsız sınıfın sonu
toString'in soy ağacını öğrenmek için yola çıkmıştık, şimdi equals ve arkadaşlarının da katılımı ile iş daha da karıştı, değil mi? Üzülmeyin, aşağıdaki kısmi tür sıradüzeninin açıklanması her şeyi yoluna koyacaktır. [Yatık yazılı türler çeşnileri, diğerleri ise sınıfları temsil ediyor.]

  • Any
    • AnyRef ≡ java.lang.Object
      • ... // Java'dan ithal ediilen bileşke türler
      • ScalaObject
        • ... // Scala'da tanımlanan bileşke türler
    • AnyVal
      • Boolean + scala.runtime.RichBoolean
      • Byte + scala.runtime.RichByte
      • ...
      • Unit

Java'daki bileşke tür ve ilkel tür ayrımı Scala'da bire bir karşılık bulmaz; ilkel türlerin ele alınışını C# diline benzeterek anlamak daha kolay olacaktır. Çünkü, arka planda işlemcinin desteklediği türlerden birine eşleştirilerek işlenen bu çeşit değerler, kaynak kod düzeyinde kalıtlanarak geliştirilemeyen—yani final—özel sınıfların nesneleri gibi ele alınır. Dolayısıyla, ilkel türden değerler de ileti alıcı konumunda kullanılabilir. Bunu akılda tutarak yukarıda verilen sıradüzenini açalım. Öncelikle, ister ilkel olsun isterse bileşke, tüm türlerin kökü ==, !=, equals, toString ve diğer temel iletileri içeren Any sınıfına gider. Scala nesnesi olduğunun anlaşılması için ScalaObject adlı bir gösterge çeşninin katıldığı bileşke türlü değerler, java.lang.Object'tekilere ek olarak eq ve ne gibi Scala nesnelerine özel iletiler içeren AnyRef sınıfında belirlenen sözleşmeye göre davranırlar. İlkel türlü değerler ise, ilişkin sınıfları işaretlemek için kullanılan AnyVal çeşnisi katılmış sınıflara aittir. Buna ek olarak, Predef tek-örneklisinde yapılan dönüşümler sayesinde, tüm ilkel tür değerler daha zengin bir arayüze sahipmiş gibi görünebilirler. Örnek olarak, 1'den 10'a tüm tamsayıları standart çıktıya yazan for (i <- 1 to 10) System.out.println(i) komutunun perde arkasına bir göz atalım. Unuttuysanız hatırlatalım, tek argümanlı ileti gönderileri işleç ortada sözdizimiyle de yazılabilir. Dolayısıyla, ileti alıcıdan başlayarak argümanındaki değere kadar olan tamsayıları içeren bir dilim döndüren to iletisini dönüştürerek döngümüzü for (i <- 1.to(10)) System.out.println(i) şeklinde yazmak da aynı işi görecektir. Yani, Int türlü 1'e to iletisi gönderilecek ve döndürülen dilim nesnesi gezilerek döngü işlenecektir. Ne var ki, Int sınıfının arayüzüne bakıldığında, to adında bir iletinin olmadığı görülecektir. O zaman, nasıl oluyor da Scala derleyicisi böyle bir durumda itiraz etmeden yukarıda anlatılan şeyi yapıyor? Bu sorunun yanıtı, Predef tek-örneklisinde sağlanan dönüştürücü metodun örtük çağrısında (İng., implicit call) yatıyor: derleyici, iletinin desteklenmediğini görmesinin ardından Int türlü hedef nesneyi scala.runtime.RichInt nesnesine çeviriyor ve iletiyi bu nesneye gönderiyor. Bir diğer deyişle, Int sınıfındaki işlevsellik RichInt sınıfında sağlanan işlevsellikle zenginleştiriliyor.

Bu haliyle çeşnilerin soyut sınıflardan çok da farklı olmadığını düşünebilirsiniz: tıpkı soyut sınıflarda olduğu gibi, çeşniler altalan tanımlarının yanısıra iletiler ve bu iletilerin bazıları veya tümü için gerçekleştirimler içerebilir. Buna karşılık, çeşniler yardımcı yapıcılara sahip olamaz; ek olarak, birincil yapıcılar argüman alamadığı gibi kalıtladıkları türlerin yapıcılarına argüman geçiremez. Ayrıca, (soyut) sınıflar için tekli kalıtlama geçerliyken çeşniler birden çok çeşniden kalıtlayabilir. Bundan dolayı bazılarınız, önceki paragraflardaki kategori sözcüğünün kullanımının da katkısıyla, çeşnilerin biraz da arayüzlere benzediğini düşünebilir. İki grubun da bir yere kadar haklı olduğu söylenebilir. Kimin haklı olduğunu ilan etmeden önce, Scala derleyicisinin çoklu sınıf kalıtlamanın desteklenmediği JSM üzerinde nasıl olur da soyut sınıf özellikleri sergileyen çeşnilerin çoklu kalıtımını olduruyor, ona bir bakalım. Bunun için sınıf dosyalarını tanımlanan türün üyelerini listeleyerek tersine bütünleştiren javap komutunu ve Scala derleyicisinin bir opsiyonunu kullanacağız.

Çeşni.scala
trait Çeşni { def ileti() { println("iletide ...") } }
$ scalac Çeşni.scala
$ ls Çeşni*.class
Çeşni.class Çeşni$class.class
$ javap Çeşni
Compiled from Çeşni.scala
public interface Çeşni extends scala.ScalaObject {
  public abstract void ileti();
}
Çeşni.class dosyasının incelenme sonucu, tercihini arayüzden yana kullananları haklı çıkarmış gibi gözüküyor; javap komutunun çıktısına göre, tanımlamakta olduğumuz iletinin imzası arayüze aynen taşınmış. Ancak, doğal olarak, arayüzlerin gerçekleştirim ayrıntısı içerememesi nedeniyle, metot gövdesi uçup gidivermiş. Bu sihirbazlığa açıklık getirilmesi gerekli. İşte bu noktada, Scala derleyicisine geçirilecek print opsiyonu yardım çağrımıza yanıt verecektir.
$ scalac -print Çeşni.scala
[[syntax trees at end of cleanup]]// Scala source: Çeşni.scala
package <empty> {
  abstract trait Çeşni extends java.lang.Object with ScalaObject {
    def ileti(): Unit
  };
  abstract trait Çeşni$class extends  {
    def ileti($this: Çeşni): Unit = scala.this.Predef.println("ileti içinde ...");
    def /*Çeşni$class*/$init$($this: Çeşni): Unit = {
      ()
    }
  }
}
Görünen o ki, metot gövdesi yukarıda yaptığımız sorgu sonrasında listelenen ikinci dosyaya, Çeşni$class.class, taşınmış. Yani, ortada kaybolup giden bir şey yok. Ama,çeşnimizin dönüşümünü Scala üstkavramları cinsinden veren bu çıktıda açıklama getirilmesi gereken bir nokta var: Çeşni$class çeşnisi, Java'ya nasıl çevrilecek? Çok bekletmeden yanıtını verelim: soyut sınıf olarak. Her şeyi bir arada gösteren aşağıdaki kod üzerinden anlamaya çalışalım.
trait Çeşni { def ileti() = println("ileti içinde...") }
⇓ Scala → Java
public interface Çeşni { public abstract void ileti(); }
+
public abstract class Çeşni$class {
  static void ileti1(Çeşni $this) = { ... }
  ...
}
class ÇeşniciBaşı extends Çeşni { ... }
⇓ Scala → Java
public class ÇeşniciBaşı implements Çeşni {
  ...
  void ileti() { Çeşni$class.ileti(this) }
  ...
}
Özetleyecek olursak; çeşni tanımının sözleşmesi bir arayüze, gerçekleştirim ayrıntıları ise bir soyut sınıfa konulur; çeşninin katıldığı sınıfta ise çeşninin arayüzündeki iletilere karşılık gelen metotlar, işlerini soyut sınıftaki gerçekleştirime havale ederek görürler.

Dikkatinizi çekeceğimiz son nokta, çeşni katılmış bir sınıfın nesnesine gönderilen iletilerin işlemesi sırasında super anahtar sözcüğünün anlamını ilgilendiriyor. Tekli sınıf kalıtlamanın geçerli olduğu ve arayüzlerin gerçekleştirim ayrıntısı içermediği Java'da üstsınıftaki yapıcı veya diğer metotlara atıfta bulunmak için kullanılan bu sözcük, çoklu çeşni kalıtlama, bir sınıfa birden çok çeşni katılabilmesi ve çeşnilerin gerçekleştirim ayrıntısı içermesi nedeniyle Scala'da anlaşılması daha zor bir anlama sahiptir. Programming in Scala kitabındaki örnek ile anlamaya çalışalım.
class Hayvan
trait Tüylü extends Hayvan
trait Bacaklı extends Hayvan
trait DörtBacaklı extends Bacaklı
class Kedi extends Hayvan with Tüylü with DörtBacaklı
super çağrılarının etkisini anlamak için, Scala'nın sıradüzenindeki türleri nasıl sıraya dizdiğini anlamak gerekir. Doğrusallaştırma adı verilen bu işlemin üç temel kuralı vardır:
  1. Bir sınıf üstsınıfları ve katılan çeşnilerinin öncesinde doğrusallaştırılır.
  2. Doğrusallaştırılmada daha önceden geçmiş bir tür bir daha sıraya konmaz.
  3. Bir sınıfın üstsınıfı ve birden çok çeşnisi olması durumunda, ilk olarak en son katılan çeşni doğrusallaştırılır.
Anlamanızı kolaylaştırmak için yalın bir tanımı olan Hayvan sınıfından başlayalım. Birinci madde gereği, doğrusallaştırma sonucu türler Hayvan, AnyRef, Any şeklinde sıralanacaktır. Yani, Hayvan sınıfının içinde geçen bir super çağrısı, AnyRef sınıfı içindeki ilişkin metoda atıfta bulunacaktır. Hayvan'ı geliştiren Tüylü çeşnisinin doğrusallaştırılması ise, kendisi ve Hayvan'ın doğrusallaştırılması ile elde edilen Tüylü, Hayvan, AnyRef, Any sırasını verecektir. Bacaklı ve DörtBacaklı için de benzer bir şekilde oluşturulan sıralama, sırasıyla, Bacaklı, Hayvan, AnyRef, Any ve DörtBacaklı, Bacaklı, Hayvan, AnyRef, Any olarak bulunacaktır. Hayvan sınıfına doğrudan ve Tüylü ve DörtBacaklı çeşnileri üzerinden olmak üzere üç değişik yoldan ulaşan Kedi sınıfı, ikinci ve üçüncü maddeler akılda bulundurularak doğrusallaştırılmalıdır. Yani, istediğimiz sıra Kedi sınıfının DörtBacaklı, Tüylü ve Hayvan'ın doğrusallaştırılması ile birleştirilmesi sonucunda bulunacaktır. DörtBacaklı'nın doğrusallaştırılması ile elde edilen sıranın öteki türlerin sıralamasındaki tüm türleri içermesi nedeniyle, sonuç Kedi, DörtBacaklı, Bacaklı, Tüylü, Hayvan, AnyRef, Any olarak ortaya çıkacaktır. Bu sıra, zincirleme super çağrılarının bulunduğu bir kodda bize denetim akışının izleyeceği yolu verir.


  1. Değinilecek farklılıklardan çoğunun kaynak kodu kısaltıp okumayı kolaylaştırdığına ve alana özel diller geliştirirken gerekli olacak akıcı arayüz yazımını olanaklı kıldığına dikkatinizi çekerim. ↑
  2. Yazıyı okurken Scala'nın kimi özelliklerini gösterebilmek adına, kodumuzu sınıf yazma reçetesinin anlatıldığı yazıda🔎 tavsiye edilenden farklı bir biçimde oluşturduğumuzu aklınızdan çıkarmayınız. ↑
  3. Hatırlayacak olursanız, protected niteleyicisi Java'da altsınıfların yanısıra, aynı paketteki türlere de erişim hakkı sağlar. ↑
  4. Tanımlayıcı adının oluşturulmasında işleçler ve diğer karakterlerin birbirine karıştırılarak kullanılması mümkün değildir. ↑