25 Eylül 2011 Pazar

Karakter Katarı Sınıfları

Java; ad, adres, makale içeriği gibi metinsel bilgilerin temsil edilmesinde kullanılmak üzere iki başlık altında incelenebilecek seçenekler sunar: dizi temelli çözümler metni eleman türü char olan diziler olarak ele alırken, karakter katarı kavramını soyutlayan sınıflar—üç tane—0 veya daha fazla sayıda karakter içeren katarların kullanımını kolaylaştıran iletiler sağlar. Bizim de bu yazıda yapacağımız, ikinci grupta geçen seçeneklere bakmak olacak.

İşimize neden üç sınıf bulunduğuna açıklık getirerek başlayalım. StringBuilder sınıfının varlık nedeni, içeriği değişmeyen karakter katarlarını temsil eden String sınıfının aşağıdaki kullanımının çalışma hızına dönük yorumla açıklanabilir.
public class SBuilder {
  ...
  public static String ekle(String katar, char[] ek) {
    String sonuç = katar;
    for (int i = 0; i < ek.length; i++)
      sonuç = sonuç + ek[i];

    return sonuç;
  } // String ekle(String, char[])
  ...
} // SBuilder sınıfının sonu
...
String ktr = "Ali";
char[] dizi = {'V', 'e', 'l', 'i'};
ktr = SBuilder.ekle(ktr, dizi);
İlk argümanındaki karakter katarına ikinci argümanındaki karakter dizisinin eklenmesi ile elde edilen sonucu döndüren ekle, işaretlenmiş satırdan dolayı işini yüksek maliyetli bir biçimde yapmaktadır. Benzer bir gözlem, son satırdaki atama için de geçerlidir. Bunun sebebi, String nesnelerinin değişmez içeriğe sahip olması ve içerik değişikliği etkisinin bitiştirme işleci (+) sonucunun aynı tutacağı güncellemesi yoluyla sağlanmasında yatar. Ne kastedildiğini döngü içindeki komutun işlemesi sırasında arka planda olup bitenleri açarak anlamaya çalışalım.
  1. sonuç tarafından gösterilen String nesnesine dizi'nin döngü değişkeni ile gösterilen indisteki elemanının eklenmesi ile elde edilen yeni bir String nesnesi yaratılır.
  2. Yapılan atama sonrasında sonuç yeni yaratılan nesneyi göstermeye başlar. Bu işlemin bir diğer sonucu olarak, sonuç tutacağının göstermekte olduğu eski nesne erişilmez olur ve çöp haline gelir.
Yukarıdaki maddelerin döngünün her dönüşünde icra edildiği düşünüldüğünde, dizi uzunluğu sayısında yeni nesnenin yaratılıp aynı sayıda eski nesnenin çöp haline dönüşeceği görülecektir. Nesne yaratma bağlamında belleğin ayrılması ve ilkleme için gereken zaman çöpe dönüşen eski nesneler ile birlikte düşünüldüğünde, yöntemimizin hem zaman hem de bellek kullanımı açısından tatminkar olmadığı kabul edilecektir. Aradığımız şey, değişken içerikli karakter katarlarının desteklenmesidir ki, bu özellik StringBuilder sınıfı tarafından sağlanır. Yani, özetlemek gerekirse, String içeriği değişmeyen karakter katarlarını soyutlarken, StringBuilder içeriği değişen, uzayıp kısalabilen karakter katarlarını soyutlar.

Bu noktada, sadece StringBuilder sınıfı ile yetinip içeriği değişmeyen karakter katarlarının kullanıcının disiplinli programlamasına bırakılmasıyla tek bir sınıfın yetebileceğini düşünenleriniz çıkabilir. Doğru ya, neden durup dururken karakter katarlarını içeriği değişen ve değişmeyen diye ayıralım ki? Bu haklı sorunun yanıtı, sabit içeriğin çok izleklili (İng., thread) programlarda sağladığı avantajda yatar. Birden çok denetim akışının aynı nesneyi kullanması durumunun ortaya çıktığı bu tür programlarda, nesne içeriğinin değişkenlik göstermesi halinde söz konusu nesneye değişik izlekler içinden eşgüdümlü ve sırasallaştırılmış bir biçimde erişilmesi gerekir. Her zaman doğru yerine getirilmeyen bu koşul, çalışma hızını olumsuz etkilediği gibi programlamayı da zorlaştırır. Sabit içerikli nesnelerde ise durum farklıdır; içeriğin güncellenmeyeceği bilindiği için, söz konusu nesne izlekler içinden herhangi bir sırada eşgüdüm kaygısı olmadan okunabilir. Dolayısıyla, hem programlama daha kolay olacaktır hem de ortaya çıkan program daha hızlı çalışacaktır. Bu nedenle, String ve StringBuilder sınıflarının her ikisine de gereksinim vardır. Peki, ya üçüncü sınıf StringBuffer? StringBuilder ile tamamıyla aynı işlevselliğe sahip olan bu sınıf, StringBuilder'ın performans kaygıları nedeniyle sağlamadığı birden çok izlek içinden güvenli kullanım garantisini ve String'in desteklemediği içerik değişkenliği özelliklerini birleştirir.

Değişken içerikÇok izleklilik
String
StringBuffer
StringBuilder

Not: Yazımızın devamında yukarıdaki farklılıkları dışında işlevsel olarak birbirlerinin aynısı olan StringBuilder ve StringBuffer sınıflarını ayrı ayrı incelemeyeceğiz; birisi için yapacağımız açıklamalar diğeri için de geçerli olacak.

Karakter katarlarını temsil etmek için neden üç tane sınıf sağlandığına açıklık getirdikten sonra, dışsallaştırma desteğine işaret eden java.io.Serializable arayüzüne ek olarak her üç sınıfa da ortak olan CharSequence arayüzündeki iletilerin açıklamaları ile devam edelim. Object sınıfındaki ilişkin metodun gerçekleştiren sınıflarda ezilmesini garanti etmek için toString'i içeren bu arayüz, katar içindeki karakter sayısını döndüren length ve argümanındaki indiste bulunan karakteri döndüren charAt iletilerine ilaveten hedef nesnenin argümanlarda belirtilen aralıktaki dilimini CharSequence tutacağı ile gösterilecek şekilde döndüren subSequence iletisini içerir.

Her üç karakter katarı sınıfında da bulunan diğer iletiler, hedef nesnenin içeriğini sorgulamaya yarayan iletilerdir. Bunlardan substring, hedef nesnenin belirtilen dilimi içindeki karakterlerden oluşan bir String döndürür ve iki uyarlamaya sahiptir. Uyarlamalardan ilki argümanlarda verilen sınırlar dahilindeki dilimi döndürürken, tek argümanlı olan ikinci uyarlama yegâne argümanda geçirilen indiste başlayıp katarın sonuna kadar tüm karakterleri kapsayan dilimi döndürür.

alıcı.substring(i, alıcı.length()) ≡ alıcı.substring(i)

Ortak olan bir diğer ileti grubu, karakterlerin Java temsilinde kullanılan UTF-16 kodlamasının bir özelliğini hatırlamamızı gerektiriyor: Her ne kadar modern dillere ait tüm simgeler iki sekizli genişliğindeki char değerlerle temsil ediliyorlarsa da, kimi özel ilgi gruplarının yararlandığı simgeler dört sekizliyle temsil edilir. (Ayrıntı için, buraya🔎 bakınız) Bir diğer deyişle, çoğu karakter (simge) tek bir char ile temsil edilirken kimileri ardışık iki char ile temsil edilebilir. Bu tür karakterleri içeren katarlara charAt iletisinin gönderilmesi, beklenen sonucu vermeyecektir; sorgulamanın başarıyla yapılması karaktere dair her iki char değerin birlikte okunmasıyla mümkün olur ki, incelediğimiz sınıflar bu amaçla charAt yerine kullanılmak üzere codePointAt iletisini sağlar. Argümanındaki indiste bulunan char değeri okuyan bu ileti, okuduğu değerin özel gösterimli karakterlerden birine ait olması durumunda ikinci bir char daha okur ve sonucu olarak okuduğu değeri—duruma göre bir veya iki char—bir int olarak döndürür. Benzer bir şekilde çalışan codePointBefore, kendisine geçirilen indisin öncesindeki karakteri döndürür. Bu noktada, bir hatırlatmanın yapılması yerinde olacaktır: geçirilen indisin değeri codePointAt için 0 ile katardaki char sayısının bir eksiği aralığında iken, codePointBefore için 1 ile katardaki char sayısı aralığında olmalıdır. İşini karakterlerin temsilindeki özel istisnayı göz önüne alarak gören bir diğer ileti, katar içinde kaç tane char olduğunu döndüren length yerine kullanılması gerekebilecek codePointCount iletisidir. Bu ileti, argümanlarında belirtilen indisler arasındaki karakterlerin, char değerlerin değil, sayısını döndürür. Dört sekizli ile temsil edilen karakterlerin dilimin başı ve/veya sonunda yarıda kesilmesi durumunda ise, karakter(ler)in varlığı döndürülen int değere yansıtılır. Herhangi bir indisin işaret ettiği konumdaki karakterin yarıda kesilip kesilmediği ise, offsetByCodePoints iletisi yardımıyla öğrenilebilir. Bu ileti, ilk argümanında verilen indis değerinden başlayarak ikinci argümanındaki sayı kadar sonraki karakterin hangi indiste başladığını döndürür.

indexOf ve lastIndexOf iletilerinin ilk argüman olarak String alanları da her üç sınıfa ortaktır. Bu iletilerden indexOf, ilk argümanında geçirilen String nesnenin hedef nesne içinde ilk hangi indiste geçtiğini döndürürken, lastIndexOf son geçişin indisini döndürür. Aranan katarın hedef nesnede bulunmaması durumu ise dönüş değerindeki -1 ile haber verilecektir. İstenecek olursa, her iki ileti de aramalarını aldıkları ikinci bir argümanın sonrasına/öncesine sınırlayabilir. Bu noktada, tüm iletilerin String sınıfına özel olmak üzere, String yerine char türünde ilk argüman alarak çalışan uyarlamaları da olduğu söylenmelidir.

Karakter katarı sınıflarına ait nesnelerin yaratılması için kullanabileceğimiz yapıcılara bakarak devam edelim. Her üç sınıf da varsayılan yapıcıya ek olarak argümanındaki String nesnenin kopyasını kullanarak ilkleme yapan yapıcılara sahiptir. Ancak, StringBuilder ve StringBuffer sınıflarının ortak bir özelliği, yapıcı kullanımı noktasında unutulmamalıdır: int argüman geçirilerek çağrılan yapıcı dışında, her iki sınıfta da nesnenin ilklenmesinde kullanılan karakterlerin ötesinde 16 char'lık bir bölge nesneye eklemeler yapılması durumunda kullanılmak üzere fazladan ayrılır. Bu gerçeği, aşağıdaki kod parçasının son satırındaki uzunluk ve sığayı döndüren length ve StringBuilder ile StringBuffer'a özel capacity iletilerinin ürettiği çıktıdan da gözlemleyebiliriz.
String ad = "Tevfik", soyad = new String("Aktuğlu");
String adSoyad1 = ad.concat(" " + soyad);
StringBuilder adSoyad2 = new StringBuilder(adSoyad1);
System.out.println(adsoyad2.length()); // ⇒ 14
System.out.println(adsoyad2.capacity()); // ⇒ 30
Yukarıdaki kod parçası incelendiğinde bazı noktalar dikkatinizi çekecektir. Öncelikle, String nesneleri new işleci ve sınıf adı olmaksızın, ilklemede kullanılacak katarın tırnak içinde yazılması ile de yaratılabilir. Bir diğer deyişle, String türlü tutacaklar derleyici tarafından String türlü olarak görülen karakter katarı sabitleri ile ilklenebilirler. Ancak, bunun özel bir durum olduğu ve sizi şaşırtacak sonuçlara neden olabileceği unutulmamalıdır. Ne demek istediğimizi aşağıdaki kod parçasını inceleyerek görelim: bir önceki kod parçası ile aynı biçimde yaratılan iki nesnenin eşitlik denetimleri beklediğimiz sonuçları verirken, aynılık denetimleri birbirleriyle farklı sonuçlar vermekte.
String ad2 = "Tevfik", soyad2 = new String("Aktuğlu");
System.out.println(soyad.equals(soyad2)); // ⇒ true
System.out.println(ad.equals(ad2)); // ⇒ true
System.out.println(soyad == soyad2); // ⇒ false
System.out.println(ad == ad2); // ⇒ true
Aynılık denetiminin beklenilenin aksine true döndürmesinin nedeni, karakter katarı sabitleri için yerin String sınıfının yönetimindeki bir içrek bellekten ayrılmasıdır. Yer ayrımı öncesinde söz konusu sabite eşit bir değer için bu özel bölgeden daha önce yer ayrılıp ayrılmadığına bakılır ve yanıtın olumlu olması durumunda yeni yaratılacak bir String nesnenin tutacağındansa, önceden yaratılmış olan nesnenin tutacağı döndürülür. Örneğimize dönecek olursak; ad tutacağının ilklenmesi noktasında içrek belleğin "Tevfik" değerine sahip bir katar içermemesi nedeniyle [içrek bellekte] yeni bir nesne yaratılarak döndürülmüş, ad2'nin ilklenmesi noktasında ise içrek bellekte aynı değerli bir katarın varlığı saptandığı için daha önceden yaratılan bu nesne kullanılmıştır. Yerden tasarruf sağlamasının yanısıra eşitlik denetimini aynılık denetimine indirgeyen bu özellik, iki karakter katarı sabitinin eşitlik denetiminde == işlecinin kullanılmasına olanak tanıyarak hızdan da kazandıracaktır.

Karakter katarı sabitleri için geçerli olan içrekleştirme—yani, yerin içrek bellekten ayrılması—intern iletisi vasıtasıyla diğer String nesnelerine de yaygınlaştırılabilir. Bu ileti, hedef nesnenin içeriğine sahip olan fakat yeri içrek bellekten ayrılmış bulunan bir nesnenin tutacağını döndürür. Ancak; intern iletisinin tüm String nesneler için kullanılmasının yığın belleğin bir bölgesi olan içrek belleği kolayca doldurup taşırabileceği unutulmamalıdır.
String soyad3 = soyad.intern();
String soyad4 = soyad2.intern();
System.out.println(soyad3 == soyad4); // ⇒ true
Yapıcılara dair ilk örneğimizde açıklama bekleyen ikinci nokta, bitiştirme işleci (+) ile benzer bir işlev gören concat iletisidir. Argümanlarından birisinin String olması halinde diğerini de String.valueOf metotlarından uyumlu türe sahip olanını çağırarak String'e dönüştürüp diğeri ile bitiştiren + işlecinin aksine, concat iletisinin yegâne argümanının String olmaması derleyici hatasına neden olur.1 Ayrıca, concat argümanındeki karakter katarının boş olması durumunda yeni bir nesnenin tutacağı yerine ileti alıcıyı döndürür.

String sınıfının yapıcıları arasında, StringBuffer ve StringBuilder nesnelerinden String nesnelerine dönüşümü sağlayan çeviren yapıcılara ek olarak, byte, char ve int dizilerini String nesnesine dönüştürenler de vardır. Bu yapıcılar, aynı veya farklı makinelerdeki süreçler arasında akan verilerin String olarak işlenmesini sağlamak için gereklidir. Örnek olarak, yazdığınız Java programının iletişimde bulunduğu bir C programından kendisine gönderilen karakter verileri işlemek istediğini düşünün. Karakter desteğinin ASCII tablosu üzerine inşa edilmesi nedeniyle tek sekizli büyüklüğündeki bir char türüne sahip C tarafından gönderilen karakterler, Java tarafında byte dizisi olarak görülecektir. Bu dizi içindeki karakterlerin ASCII kodlamasına uygun bir şekilde Java programında kullanılabilir bir karakter katarına çevrilmesi gereklidir. Bunun için yapılması gereken aşağıdaki gibi bir yapıcının kullanılması olacaktır.2 Yapıcının ilk argümanı dönüşüme kaynaklık eden diziyi sağlarken, ikinci argüman dönüşümde kullanılacak karakter kodlamasını belirtir. İstenecek olursa, dizinin tamamı yerine bir dilimi kaynaklık edebileceği gibi, kodlama bir java.nio.charset.Charset nesnesi vasıtasıyla da bildirilebilir veya sağlanmadığı durumlarda Java platformunun o anki kodlaması olarak varsayılabilir. Mesela, aşağıdaki kod parçasının son satırı, tampon'da biriktirilmiş byte'ların ilk indisten başlayarak 100 tanesini, o anki kodlamayı kullanarak String nesnesine çevirmektedir.
byte tampon[] = new byte[1024];
... // Karşı taraftan akan veriyle tampon'u doldur.
String katar = new String(tampon, "US-ASCII");
... // katar'ı kullan.
...// tampon'u yeniden doldur.
katar = new String(tampon, 0, 100);
Bu noktada, karşı taraftaki program C değil de Java programı olsaydı ve byte yerine char gönderiliyor olsaydı diye sorabilirsiniz. Hatta, ya gönderilen veri UTF-16'da iki char ile temsil edilebilen istisnai karakterleri de içeriyor olsaydı diye üsteliyebilirsiniz. Rahat olun, sorularınız yanıtlanacak ama ilk önce Java'dan C tarafına veri göndermek istediğimizde ne yapmamız gerektiğine bir göz atalım. Bu durumda, String nesnesi içindeki karakterlerin karşı taraftaki programın farkında olduğu bir kodlama ile byte dizisine dönüştüren getBytes işimizi görecektir. Hedef nesnedeki tüm karakterleri argümanındaki String veya Charset nesnesiyle belirtilen kodlamaya göre byte dizisine çeviren getBytes, argümansız kullanıldığı takdirde dönüşümü platformun o an varsayılan kodlamasını kullanarak yapar.
StringBuffer katar = new StringBuffer(...);
... // katar'ı güncelle.
String katar2 = new String(katar);
byte tampon[] = new byte[1024];
katar2.getBytes("US_ASCII")
... // katar2'nin içeriğini karşı tarafa akıt.
String nesnesinden char dizisine benzer bir dönüşüm ise, diğer karakter katarı sınıflarınca da desteklenen getChars ile yapılabilir. Hedef nesnenin ilk iki argümanında belirtilen dilimini son argümandaki indisten başlayarak üçüncü argümanda sağlanan diziye kopyalayan bu ileti, char değerlerin ve String içindeki karakterlerin aynı kodlamayı kullanması nedeniyle kodlama bilgisine gereksinim duymaz. Aynı işi çok daha basit bir imza ile halletmek isterseniz, hedef nesnenin bütün karakterlerini hedef nesnenin uzunluk özelliğine göre yeri ayrılmış bir char dizisine kopyalayan ve sonucu olarak bu diziyi döndüren toCharArray iletisini de kullanabilirsiniz.

Gelelim iki Java programı arasında gidip gelen char'ların String nesnesine dönüştürülmesi işine. Bunun için char dizisi veya dizi dilimi alan yapıcılardan biri kullanılabileceği gibi, aynı parametre listelerine sahip uyarlamaları bulunan String.copyValueOf metotları da kullanılabilir. Kimi zaman aynı iş için int dizisi veya dizi dilimi alan yapıcıların kullanılması gerekebilir. Bu gereklilik iki nedenden ötürü ortaya çıkar: i) veri kaynağında UTF-16'nın tek char'da temsil edemediği karakterlerin var olması, ii) okuduğu char değere ek olarak aykırı durumlar için de kullanılan ekstra dönüş değerlerinden dolayı int döndüren java.io.BufferedReader sınıfındaki read ve benzeri iletilerin doldurduğu dizilerin işlenmesi.

Alışageldiğimiz eşitlik denetimi (equals), hoş yazım (toString) ve kıyım fonksiyonunun (hashCode) ezilmesine ek olarak, Comparable arayüzünü gerçekleştiren String sınıfı, doğal olarak, compareTo iletisine karşılık bir metot sağlar. Ayrıca, büyük-küçük harf ayrımı gütmeksizin işini gören eşitlik denetimi (equalsIgnoreCase) ve karşılaştırma (compareToIgnoreCase) iletileri de sunulur. Bu iki iletiyi, toLowerCase iletisinden de yararlanarak şu şekilde tanımlamamız mümkündür.3

alıcı.equalsIgnoreCase(k)

alıcı.toLowerCase().equals(k.toLowerCase());
alıcı.compareToIgnoreCase(k)

alıcı.toLowerCase().compareTo(k.toLowerCase());

compareTo ve compareToIgnoreCase iletilerini Türkçe içerikli katarları karşılaştırmak için kullandığınızda beklemediğiniz bir sürprizle karşılaşabilirsiniz. Çünkü, bu iletilerin katarlardaki aynı indisli char'ların Unicode tablosundaki sıralarının farkını döndürerek işini gören String sınıfındaki gerçekleştirimleri, Türkçe'de olup İngilizce'de olmayan harflerin Unicode tablosunda diğerlerinden sonra gelmesi nedeniyle, aşağıdaki gibi alfabemizdekiyle çelişkili sonuçlar döndürebilir.
boolean önceMi = "a".compareTo("b") > 0; // önceMi ← true
önceMi = "ş".compareTo("t") > 0; // önceMi ← false
Bu can sıkıcı durumun giderilmesi, bir yörenin (Locale) özelliklerine bağlı kalarak karşılaştırma yapan karşılaştırıcı nesne (Collator) kullanımı ile olanaklıdır.
import java.text.Collator;
import java.util.Locale;
... 
Collator tr = Collator.getInstance(new Locale("tr"));
...
önceMi = tr.compare("ş", "t") < 0; // şÖncet ← true
Eşitlik denetimi amacıyla kullanılabilecek bir diğer ileti, StringBuffer ve CharSequence türlü iki uyarlaması bulunan tek argümanlı contentEquals iletisidir. equals iletisinin varlığında gereksiz gibi gözükebilecek bu ileti, derleyiciye sağladığı ekstra hata denetimi imkanı ve performans avantajından dolayı tercih edilmelidir. equals iletisinin Object türlü bir argümana sahip olmasının bir sonucu olarak, bu iletinin String sınıfı içindeki gerçekleştirimi olan metotta, ilk yapılan şey argümanın String türlü olup olmadığının kontroludur. contentEquals iletisinin gerçekleştirimlerinde ise, daha sıkı bir parametre türü bulunduğu için böylesine bir kontrola gerek yoktur.

Tabii, bazılarınız StringBuffer'a özel bir uyarlamanın bulunup da StringBuilder'a özel bir uyarlamanın neden bulunmadığını sorabilir. Bu haklı sorunun yanıtı, StringBuilder'ın değişken içerikli ve tek izlekli olmasında yatar. Bu sınıf, nesnelerinin aynı anda tek bir izlek içinden kullanılacağı varsayılarak gerçekleştirilmiş, çok izlekli kullanım durumlarında ise sorumluluğu programcıya bırakmıştır. Dolayısıyla, StringBuilder sınıfı çok izlekli bir programda eşitlik denetiminin başladığı noktadan bittiği noktaya kadar karşılaştırılmaya söz konusu olan nesnenin bir başka izlek içinden değiştirilmeyeceğini garanti edemez. Bu ise, sonucun döndürüldüğü noktada bile yanlış olabileceği anlamına gelir. Aşağıdaki maddeleri izleyerek böyle bir durumun nasıl ortaya çıkabileceğini görelim.
  1. (İzlek 1) Eşitlik denetimi başlar. İki uzunluklu katarlardaki (String ve StringBuilder) ilk char'ların eşit olduğu görülür.
  2. (İzlek 2) Argüman olarak geçirilen StringBuilder nesnesinin ilk indisindeki char değiştirilir.
  3. (İzlek 1) İkinci ve son char'ların eşit olduğu görülerek true döndürülür. Halbuki, diğer izlek tarafından yapılmış değişiklik nedeniyle, bu sonuç o anki durumu yansıtmamaktadır.
Bu noktada, StringBuilder nesnelerinin String nesneleri ile eşitlik denetiminin equals'a mahkum edilerek bu sınıfa haksızlık edildiğini düşünüyorsanız, size bir kere daha her üç karakter katarı sınıfının da CharSequence arayüzünü gerçekleştirdiğini hatırlatayım. İstenecek olursa, contentEquals iletisinin CharSequence tutacağı bekleyen uyarlaması kullanılarak equals'a göre hızlı bir karşılaştırma yapılabilir. Ancak, bu bağlamda şu uyarının yapılması yerinde olacaktır: StringBuilder ve StringBuffer sınıfları kısa süre içinde yoğun bir şekilde değişmesi beklenen karakter katarlarını modellemek için düşünülmüştür; daha sonra yararlanılacak gerekli bilgiyi biriktirmek için tampon olarak kullanılan ve tipik olarak kısa ömürlü olan bu sınıflara ait nesnelerin eşitlik denetimi ve karşılaştırma gibi işlemlere konu olması beklenmez. Bundan dolayıdır ki, her iki sınıf da Comparable arayüzünü gerçekleştirmez, Object sınıfından kalıtladıkları equals ve hashCode metotlarını ezmez. 

equals iletisinin bir diğer akrabası olan regionMatches, hedef nesnenin bir diliminin kendisine geçirilen bir diğer String nesnenin eşit uzunluklu dilimi ile eşit olup olmadığı sorusuna yanıt bulur. İstendiği takdirde, geçirilecek bir bayrak değeri ile eşitlik denetiminin büyük-küçük harf ayrımı yapıp yapmaması da kontrol edilebilir.

Eşitlik denetimini andıran matches yüklemi, hedef nesnenin argümanında sağlanan düzenli ifadenin (İng., regular expression) betimlediği karakter katarlarından biri olup olmadığına bakar. Örneğin, aşağıdaki kod parçasının son satırında "H" veya "Hayır" girilmesi durumunda programdan çıkılması isteniyor.
import java.util.Scanner;
...
System.out.println("Devam etmek istiyor musunuz? ");
String devamıMı = new Scanner(System.in).nextLine();
...
if (devamMı.matches("H(ayır)?")) System.exit(0);
Düzenli ifade kullanarak işini gören bir diğer ileti, hedef nesnenin argümandaki düzenli ifadeyle betimlenen karakter katarlarının bulunduğu konumlardan bölündüğü alt katarları String dizisi içinde döndüren split'tir. Örneğin, aşağıdaki kod parçasının son satırı, satır içinde birbirlerinden ";" veya ":" ile ayrılmış olan katarları döndürecektir. İstenecek olursa, split'e ikinci argümanında geçirilen bir int değerle parçalardan ilk kaçının kullanıcıyı ilgilendirdiği de belirtilebilir.
String satır;
... // satır'ın içine bir şeyler doldur.
String[] parçalar = satır.split(";|:");
Göz atacağımız son düzenli ifade kullanan String sınıfı iletileri, ilk argümanlarında betimlenen karakter katarlarının yerine ikinci argümanlarındaki String nesnenin içeriğini yerleştiren replaceAll ve replaceFirst iletileridir. Bunlardan ilki, istenen değişikliği tüm noktalarda uygularken, ikincisi sadece uyan ilk noktadaki parçayı değiştirir. Buna göre, aşağıdaki kod parçasının son satırı sonrasında, yeniSatır satır'ın başı ve sonu boşluklardan arındırılmış ve içindeki boşlukların "?" ile değiştirilmiş halini tutacaktır. Anlatım dilinin olası aldatıcılığını düşünerek String nesnelerinin değişmezlik özelliğini bir kez daha hatırlatmakta yarar var: hedef nesne—örneğimizde satır'ın gösterdiği nesne—kesinlikle değişmemektedir; dönüşümler döndürülen String nesneye—örneğimizde yeniSatır'ın gösterdiği nesne—yansıtılmaktadır.
String satır;
... // satır'ın içinde bir şeyler oku.
String yeniSatır = satır.trim().replaceAll(" ", "?");
İlk argümanda belirtilen değerin hedef nesne içinde geçtiği tüm konumlarda ikinci argümandaki ile değiştirildiği hedef nesne mutantını döndüren replace iletisi, char ve CharSequence argümanlı olmak üzere iki uyarlamaya sahiptir.

Biraz nefes alarak, yolumuza String nesnelerinin içerik denetiminde yararlanılabilecek yüklemlere göz atalım. Hedef nesnenin boş olup olmadığına yanıt veren isEmpty'ye ek olarak, endsWith ve startsWith, sırasıyla, hedef nesnenin argümandaki String nesne ile sonlanıp sonlanmadığı ve başlayıp başlamadığı sorusunu yanıtlarken, contains argümanındaki CharSequence kategorisindeki nesnenin hedef nesnede geçip geçmediğine yanıt verir. startsWith ile sorulan sorunun ikinci bir argüman geçirilmek suretiyle hangi indisten başlayarak geçerli olduğu da söylenebilir.

Programlamanın printf ve scanf'den ibaret olduğunu düşünen C gezegeni vatandaşları, eğer bu noktaya kadar sabredebildilerse, bayram edebilirler: String sınıfından göz atacağımız son şey, sprintf fonksiyonunun dengi olan format metodu. Bu metot, ilk argümanında sağlanan format katarının dönüştürülmesiyle elde edilen String nesnesinin tutacağını döndürür. java.util.Formatter sınıfında anlatılan kurallara göre oluşturulan format katarındaki dönüşüm, format'a geçirilen diğer argümanların format katarı içinde eşleştirildiği bildirime göre String'e dönüştürülüp format katarı içine yerleştirilmesi ile yapılır. Örneğin; aşağıdaki tanımların ardından, a'nın %d ile 10'lu tabana, b'nin %o ile 8'li tabana, a + b'nin %x ile 16'lı tabana ve %n'nin yeni satır karakterine dönüştürülerek format katarı içine yerleştirilmesiyle ktr değişkeni "a(37) + b(111) = 6e\n" ile ilklenecektir.
int a = 37, b = 73;
String ktr = String.format("a(%d) + b(%o) = %x%n", a, b, a + b);
Gelelim StringBuilder ve StringBuffer sınıflarına. Bu sınıflar, değişken içerik ve ekleme sonrasında ortaya çıkacak uzamanın maliyetini azaltmaya dönük olarak tutulan boş bölge nedeniyle, String sınıfından farklı olarak ekleme, silme ve değiştirme iletileriyle sığa işlemleme işlemlerini de destekler. Daha önce de söylendiği gibi, bu sınıfların nesnelerinin yaratılması sırasında ilklemede kullanılan katarın ihtiyacından on altı char daha geniş bir yer ayrılır. Nesnenin kullanımı esnasında, taşma olmadığı müddetçe eklemeler bu fazlalık alandan yararlanılarak yapılacak ve taşmanın söz konusu olduğu ilk noktada nesneyi tutan bellek bölgesi genişletilecektir.

İçeriği güncelleyen iletilerin değişiklikleri hedef nesnede yaptığı ve ileti alıcıyı döndürdüğünün bilinmesi zincirleme ileti gönderimini ve dolayısıyla daha akıcı okunabilen kod yazımını olanaklı kılacaktır. Örnek olarak aşağıdaki kod parçasının 7. satırını ele alalım. Bu satır, zincirleme ileti gönderme özelliği sayesinde önder.insert(); ve önder.trimToSize(); gönderilerinin arka arkaya gönderilmesiyle elde edilecek etkiyi yaratacaktır. Benzer bir durum 4. satır için de söz konusudur. Ayrıca, append ve insert iletilerinin tüm ilkel ve bileşke türlü değerleri alabilecek şekilde aşırı yüklenerek gerçekleştirildiğinin de bilinmesi gereksiz yere kullanılacak dönüşümleri ortadan kaldırmak suretiyle daha kısa kod yazılmasını olanaklı kılacaktır.
StringBuffer ad = new StringBuffer("Mustafa");
StringBuilder önder = new StringBuilder(ad);
System.out.println(önder.capacity()); // ⇒ 23
önder.append('_').append("Atatürk");
önder.setCharAt(7, ' ');
System.out.println(önder.capacity()); // ⇒ 23
önder.insert(8, "Kemal ").trimTosize();
System.out.println(önder.capacity()); // ⇒ 21
int i = önder.indexOf("Atatürk");
önder.replace(i, önder.length(), "ATATÜRK");
System.out.println(önder); // ⇒ Mustafa Kemal ATATÜRK
Yukarıdaki kod parçasında iki noktaya açıklık getirilmesinde yarar var. Öncelikle, yaratılmakta olan nesneyi argümanında geçirilen StringBuffer nesnenin içeriği ile ilkleyen ikinci satırdaki yapıcı çağrısının StringBuilder sınıfında desteklenmediğini düşünen arkadaşları yatıştıralım. StringBuilder sınıfında StringBuffer argüman alan bir yapıcının bulunmaması ile telaşlanan bu arkadaşlar her üç karakter katarı sınıfının da CharSequence arayüzünü desteklediğini ve arayüz tutacaklarının çokbiçimli kullanımını anımsarlarsa rahatlayacaklardır. Çünkü, StringBuilder—ve onun çok izlekli uyarlaması olan ikizi StringBufferCharSequence arayüzü türlü tutacak bekleyen bir yapıcıya sahiptir. Dolayısıyla, StringBuffer nesneler CharSequence kategorisinde oldukları için, 2. satırda CharSequence türlü tutacak bekleyen yapıcı çağrılacaktır. Açıklık getirilmesi gereken ikinci nokta, ekleme ve silme işlemlerinin etkiledikleri indis değerlerine bağlı olarak maliyetlerinin artabileceğidir. 6. satırdaki insert iletisini ele alalım. Bu gönderi, hali hazırda on beş karakter içeren bir katara 8. indisten başlayarak altı karakterli bir katar eklemektedir. Bu isteğin yerine getirilebilmesi için, sondaki yedi karakterin kaydırılarak eklenecek karakterler için yer açılması gerekir. Gönderilerin düzenlenerek
önder.append(' ').append("Kemal ").append("Atatürk")
şekline dönüştürülmesi daha iyi bir çözüm olacaktır. Çünkü, katar sonuna ekleme yaptığı için karakterlerın kaydırılmasına neden olmayan append'in maliyeti daha düşüktür. Hatta, gönderilerin birleştirilerek tek bir postada işin halledilmesi daha da iyi olacaktır. Son olarak; benzer maliyet kaygılarının argümanında sağlanan indisteki karakteri silen deleteCharAt ve argümanlarında belirlenen katar dilimini silen delete iletileri için de geçerli olduğu unutulmamalıdır.

append ve insert iletilerinin tamsayı türlü değerler geçirilerek kullanılan uyarlamalarında şu nokta unutulmamalıdır: hedef nesneye eklenen, sayının hoş yazımla elde edilen gösterimidir, karakter tablosunun sayı ile belirtilen indisindeki karakter değil. İstenenin ikinci seçenek olması durumunda int argüman alan appendCodePoint iletisinin kullanılması gerekir. Aşağıdaki kod parçasını takip ederek farkı anlamaya çalışalım. İkinci satırdaki append boş katara "65" eklerken, bir sonraki satırda gönderilen appendCodePoint iletisi katar sonuna Unicode tablosunun 65. konumundaki 'A' karakterini ekleyecektir.
StringBuilder katar = new StringBuilder("");
katar.append(65);
katar.appendCodePoint(65);
System.out.println(katar); // ⇒ 65A
capacity, ensureCapacity ve trimToSize Vector🔎 sınıfındaki adaşları ile benzer şekilde çalışırken, length iletisi katardaki char sayısını döndürür. setLength ise, geçirilen argümanın ve o anki uzunluk değerine göre katarı uzatıp kısaltabilir; argümanın o anki uzunluktan büyük olması durumunda hedef nesne uzatılarak yeni konumlar '\u0000' değeri ile doldurulurken, yeni uzunluk değerinin eskisinden küçük olması durumunda hedef nesne yeni uzunluk ile eskisi arasındaki değerlerin kırpılması ile küçültülecektir.

StringBuilder ve StringBuffer sınıflarındaki iletilerden değineceğimiz sonuncusu, hedef nesnenin içeriği yerinde tersine çeviren reverse iletisidir. Gelin bu iletiyi kullanan ufak bir örnekle yazımızı bitirelim.
import java.io.Console;
import java.util.Locale;

public class Palindromik {
  public static void main(String[] ksa) {
    System.out.print("Palindromikliği sınanacak katar: ");
    String sözcük = System.console().readLine().trim().toUpperCase(new Locale("TR"));
    String tersi = new StringBuilder(sözcük).reverse().toString();
    if (sözcük.equals(tersi)) System.out.println("palindromik");
      else System.out.println("palindromik değil");
  } // void main(String[]) sonu
} // Palindromik sınıfının sonu
Yukarıdaki kodu düzenleyerek bazı alternatif çözümler geliştirebilirsiniz. Mesela, küçük harften büyük harfe çeviren toUpperCase yerine ters yönde çeviri yapan toLowerCase iletisini kullanmanız aynı işi görecektir. Ya da, equals yerine compareTo iletisi kullanılarak dönen değerin 0 ile eşitlik denetiminin yapılması da aynı kapıya çıkacaktır. Ancak; String sınıfını işe bulaştırmadan kullanılacak bir equals Object sınıfındaki aynılık denetimi yapan aynı adlı metodu çağıracağı için beklediğinizden farklı sonuçlar verecektir. Çünkü, StringBuilder ve StringBuffer sınıfları, değişken içerikli ve genelde kısa ömürlü karakter katarı tamponlarını soyutlar; bu özellikteki nesnelerin eşitlik denetimi ve karşılaştırılmaları içeriklerinin değişkenliği nedeniyle pek anlamlı değildir. Yapılması gereken, ihtiyaç duyulan bilginin StringBuilder veya StringBuffer nesnesi içinde doldurulması sonrasında String nesnesine çevrilmesi ve yola öyle devam edilmesidir.


  1. Bitiştirme esnasındaki dönüşümün nesneye toString iletisi gönderilerek yapıldığını düşünenler yanılmıyorlar; dönüştürülecek değerin bileşke türlü olması durumunda işi kotaran gerçekten de hoş yazım iletisi olarak da bilinen toString'dir. Ancak, hoş yazımı istenen değerin ilkel türden olması halinde, ileti alan tutacakları olmaması nedeniyle, iş String.valueOf metotlarından uygun olanı çağrılarak yerine getirilir. Aslına bakacak olursanız, bir nesnenin hoş yazımı için gönderilen toString iletisi, Object türlü argüman bekleyen String.valueOf metodu içinden gönderilir.
  2. Doğruyu söylemek gerekirse, böyle bir durumda byte'tan char'a dönüşümü otomatikman yapan java.io.InputStreamReader gibi bir akak sınıfının kullanılması daha yerinde olacaktır.
  3. Benzer bir denklik, toUpperCase ile de kurulabilir.

14 Eylül 2011 Çarşamba

Değişken Uzunluklu Diziler: java.util.Vector

Dizi yapısının belki de en büyük artısı, eleman erişim ve eleman güncellemenin sabit maliyetli olmasıdır; hangi indisteki eleman söz konusu olursa olsun, doğrudan erişim sayesinde her iki işlem de sabit (ve yüksek) hızlı bir şekilde yapılabilmektedir. Ancak, bunu mümkün kılan "elemanların dizinin yaratıldığı noktada ayrılan ardışık bellek konumlarına yerleştirilmesi" özelliği, yapının statik olması sonucunu da doğurur. Yani, dizinin yaratılması sonrasında uzunluğunun değiştirilmesi olanaklı değildir. Dolayısıyla, kullanım sırasında bir taşmanın olmaması için, yer ayrımının olası en yüksek eleman sayısı düşünülerek yapılması gerekir. Bunun sonucu da kimi zaman, ayrılan yerin tümünden yararlanılmadığından, belleğin verimsiz kullanımı olacaktır.

Bu olumsuzluğu gidermek için, dizi tutacağını yeri yeni ayrılmış ve baş kısmında eski dizi nesnesinin kopyasına sahip daha büyük bir dizi nesnesini gösterecek şekilde güncellemeyi düşünebilirsiniz. Java karşılığı aşağıda sunulan bu yaklaşımın iki zaafı vardır: i) dizinin büyümesi için gerekli olan kod programcı tarafından yazılmak zorundadır ve ii) programcı dizinin ne zaman büyümesi gerektiğini bilmelidir.
int[] dizi = {0, 1, 4};
...
dizi = Arrays.copyOf(dizi, dizi.length * 2);
dizi[3] = 9;
İşte bu yazımızda, Java kitaplığı tarafından değişken uzunluklu dizileri desteklemek amacıyla sağlanan java.util.Vector sınıfına bakacağız. Vector nesnelerinin sahip olması gereken öznitelikler ile başlayalım: sığa, eleman sayısı ve sığa artımı. Sığa, bir Vector nesnesinin büyümeye gerek olmadan kaç değer tutabileceğini gösterirken; eleman sayısı, sorgulandığı anda Vector'de kaç değer tutulmakta olduğunu gösterir. Vector nesnesinin dolması sonrasında—yani eleman sayısı ile sığanın eşit olması halinde—yeni bir elemanın eklenmesi istenecek olursa, taşmayı önlemek adına Vector nesnesinin sığası, sığa artımı özelliğinde öngörüldüğü miktarda artırılarak yeni eleman için yer açılır ve ekleme yapılır.

Yapıcılar


Vector nesnelerine gönderilebilecek iletilere geçmeden önce, sağlanan yapıcılara bir göz atalım. Aşağıda verilen örneklerden de görülebileceği gibi, Vector sınıfı soysaldır; tutacaklarının tanımlandığı ve nesnelerinin yaratıldığı noktalarda ilişkin kapta tutulacak elemanların türü bilgisini tür argümanı olarak ister. Tür argümanının es geçilmesi, Vector sınıfının J2SE 5.0 öncesinde olduğu gibi tür güvenliği olmaksızın kullanılacağı anlamına gelir.
Vector<Integer> intVec1 = new Vector<Integer>(50, 10);
Vector<Integer> intVec2 = new Vector<Integer>(50);
Vector<Integer> intVec3 = new Vector<Integer>();
intVec1.add(1);
...
Vector<Integer> intVec4 = new Vector<Integer>(intVec1);
boolean aynıMı = intVec4 == intVec1; // aynıMı ← false
boolean eşitMi = intVec4.equals(intVec1); // eşitMi ← true
intVec4.set(0, 11);
eşitMi = intVec4.equals(intVec1); // eşitMi ← false
İlk yapıcı çağrısı, başlangıç sığası 50 olan ve taşma noktalarında 10 elemanlık artımla genişletilecek bir Vector nesnesi yaratır. Sığa artım argümanı olarak pozitif olmayan bir değer geçirilmesi veya ikinci satırdaki yapıcı çağrısında olduğu gibi artım değerinin sağlanmaması durumunda, Vector nesnesinin sığası her taşma noktasında ikiye katlanarak artırılacaktır. Buna göre, intVec2 tutacağı ile gösterilen nesne 51. elemanın eklenmesi istendiği noktada 100, 101. elemanın eklenmek istendiği noktada ise 200 eleman tutabilecek şekilde genişletilecektir. Benzer bir sığa artırım politikası izlenecek üçüncü satırdaki varsayılan yapıcı çağrısı, on elemanlık başlangıç sığasına sahip bir Vector nesnesi yaratacaktır. Son satırdaki çağrı ise, kopyalayan yapıcı görevini görecek ve kendisine geçirilen Collection<E> kategorisindeki bir kabın elemanlarını gezicinin döndürdüğü sırada yaratılmakta olan Vector nesnesine kopyalayacaktır. Kopyalamanın sonrasında eşit fakat birbirlerinden farklı (ve bağımsız) iki Vector nesnesinin var olacağı ve bunlardan birisine yapılacak değişikliğin diğerini etkilemeyeceği unutulmamalıdır. Ayrıca, yeni yaratılan Vector nesnesinin ilk sığası diğerinin eleman sayısı ile sınırlı olacaktır.

Desteklenen Arayüzler


Vector sınıfının dokümantasyonuna baktığınızda, desteklenen iletilerin çokluğu gözünüzü korkutabilir. Ancak, bunların bir bölümünün bir diğer ileti ile eşdeğer olduğu, bir bölümünün ise bildiğiniz sınıflardan tanıdık geleceği düşünüldüğünde, işimizin o kadar da zor olmadığı görülecektir. Ne de olsa, aşağıda verilen soy ağacına sahip olan Vector sınıfı, gerçekleştirilen ortak arayüzler ve kalıtlanılan ortak üstsınıflar nedeniyle Veri Kapları Çerçevesi'ndeki diğer sınıflarla büyük benzerlikler gösterir.
Iterable arayüzü ile başlayalım. Genelde programcı tarafından doğrudan kullanılmayacak olan iterator iletisini içeren bu arayüz, derleyiciye söz konusu sınıfın gezici for döngüsü ile [baştan sona] dolaşılabileceğini garanti eder. Mesela, aşağıdaki kod parçası, hiç gezici nesne yaratıp, bu nesneye ileti göndermek zahmetine katlanmadan intVec4'in gösterdiği kaptaki tüm elemanların kareköklerini standart çıktıya basar.
for (Integer eleman : intVec4)
  System.out.println(Math.sqrt(eleman));
Gerçekleştirilen arayüzler listesindeki Cloneable, Vector nesnelerinin kopyalarının çıkarılabileceği anlamına gelir. Object sınıfındaki clone metodunun gösterge arayüz olan Cloneable'ı gerçekleştiren sınıflarda özel bir biçimde ezilmesiyle yerine getirilen sözleşme sonucunda, clone iletisi Object türlü bir tutacakla dahi gönderilebilir ve yaratılan kopya Object tutacağı aracılığıyla döndürülür. Dolayısıyla, yeni kopyaya Vector sınıfına has iletiler gönderilmesi öncesinde sonucun biçimlendirilmesi gerekecektir. Ayrıca; işlem sırasında elemanları tutan kap kopyalandığı için hedef nesne ile çıkarılan kopya ayrı nesneler olacaktır. Bu, nesnelerin atıflarının (tutacak) geçirildiği Java'da, değer geçirme yönteminin benzetimi için kullanılabilir.
...
public void mtt(Vector<Integer> vec) {
  Vector<Integer> vecKopya = 
    (Vector<Integer>) vec.clone();
  // vecKopya'yı kullan.
  ...
} // void mtt(Vector<Integer>) sonu
java.util.RandomAccess de gösterge arayüz olup herhangi bir ileti içermez. İşaret görevini gören bu arayüzün varlığı, kimi zaman doğrudan erişimli kimi zamansa doğrudan erişimli olmayan kapları işleyerek işini gören soysal kod parçalarında, doğrudan erişimliliğin avantajlarından yararlanılabilecek noktalarda bu özelliğin kullanılmasıyla performansın artmasını olanaklı kılar. Bunun için, nesnenin RandomAccess arayüzünü destekleyip desteklemediğinin aşağıdaki gibi denetlenmesi yeterli olacaktır.
...
if (kap instanceof RandomAccess) {
  // Doğrudan erişimin avantajlarından yararlan.
  ...
} else { ... }
...
Desteklenen gösterge arayüzlerden bir diğeri olan java.io.Serializable, Vector nesnelerinin disk dosyası, ağ gibi çevre aygıtlara dışsallaştırılıp, aynı aygıtlardan içselleştirilebileceğini gösterir. Bu, Vector nesnelerinin java.io.ObjectOutputStream türünden akaklara writeObject ile yazılıp, java.io.ObjectInputStream türlü akaklardan readObject ile okunabileceği anlamını taşır.

Değineceğimiz son arayüzler, Vector nesnelerini ekleme, güncelleme, silme, sorgulama gibi işlemlerle manipüle eden java.util.Collection ve java.util.List arayüzleridir. Collection, Veri Kapları Çerçevesi'ndeki kapların pek çoğu için geçerli olan işlemlerin genel sözleşmelerini sağlar. Bu yapılırken, ne çeşit bir kabın ele alındığı konusunda—aynı değerden iki veya daha fazlası tutulabilir mi, uygulanan işlemlerin sıra ve sonucuna göre elemanların yapı içindeki sırası bilinebilir mi?—bir varsayımda bulunulmaz. Buna karşılık, List arayüzü, kaptaki değerlerin tekrarlanabileceğini ve uygulanan işlemlerin sonrasında yapı içindeki sıranın bilinebileceğini varsayar.

Desteklenen İletiler


İletilere öznitelikleri işlemlemek için yararlanabileceklerimizle başlayalım. Argümansız size ve capacity iletileri, sırasıyla, hedef nesnenin kaç eleman içerdiğini ve sığasını döndürürken, isEmpty boşluk yüklemi olarak görev görür. Hedef nesnenin içeriğini eleman sayısına eşit sığalı bir kaba koyarak bellekten tasarruf sağlayan trimToSize, eleman sayısının artmayacağından emin olduğumuz durumlarda kullanılabilir. Sığanın değiştirilmesine yarayan bir diğer ileti olan ensureCapacity, hedef nesneyi argümanda geçirilen değere eşit veya daha büyük bir sığaya sahip olacak şekilde değiştirir. Bu işlemin icra edilmesi esnasında, hedef nesne yaratılırken sağlanan/varsayılan sığa artım değerinden yararlanılacaktır. Son olarak, setSize, hedef nesnedeki eleman sayısı özniteliğini günceller. Bu iletinin kullanımında dikkat edilmesi gereken bir nokta, geçirilen argümana göre eleman sayısının artması gibi azalmasının da mümkün olduğudur: argümanın ileti gönderimi anındaki eleman sayısından büyük olması eleman sayısının null değerine sahip elemanlar eklenerek artırılmasına neden olurken, küçük olması kabın sonuna doğru bazı değerlerin kırpılmasına, yani eleman sayısının azaltılmasına neden olacaktır.

Sınıflarını yazarken titiz davrananlara tanıdık gelecek iletilerle devam edelim. equals ve toString, beklendiği gibi, sırasıyla, eşitlik denetimi ve hoş yazım işlemlerini karşılarken hashCode, hedef nesneyi kıyarak özet değeri görevini gören bir int döndürür. Bilmeyenler için, bunun kulağa geldiği kadar vahşi bir şey olmadığını, nesneyi değiştirerek zarar vermediğini söyleyelim ve ne işe yaradığını biraz açalım. Potansiyel olarak pek çok eleman içerebilecek kapların eşitlik denetimi oldukça uzun bir zaman alabilir; ilk elemandan başlayarak eşit olmayan eleman çiftine kadar kontrol edilen iki kabın aynı indislerdeki elemanları birbirleriyle karşılaştırılır. Bu, eleman sayısı ile doğru orantılı olarak artan oldukça pahalı bir işlemin söz konusu olduğu anlamına gelir. Maliyet kap içeriğinin özeti olarak bir değerin tutulması ile azaltılabilir; kap değiştikçe—bu, eleman ekleme, güncelleme ve eleman silme ile mümkün olabilir—özet değer yeniden hesaplanır. Bir diğer kap ile eşitlik denetiminin yapılması istendiği durumlarda ise, önce eleman sayıları sonra ise hashCode ile öğrenilebilecek özet değerler karşılaştırılır. Özet değerler eşit değilse, kaplar da eşit değildir; aksi takdirde, eşitsiz kapların özetlerinin eşit olması olanaklı olduğundan, yukarıda bahsettiğmiz pahalı yöntemle eşitlik denetimi yapılır.
public class Vector<E> extends AbstractList<E>
  implements List<E>, Cloneable, RandomAccess, Serializable {
  ...
  public boolean equals(Object sağ) {
    Vector<E> sağTaraf = (Vector<E>) sağ;
    if (size() != sağTaraf.size()) return false;
    if (hashCode() != sağTaraf.hashCode()) return false;
    // Kapları dolaşarak karşılıklı elemanları denetle.
    ...
  } // boolean equals(Object) sonu
  ...
} // Vector<E> sınıfının sonu
Vector sınıfı, içerik güncellenmesi ve sorgulanması için pek çok ileti sunar. Ancak, kimi iletilerin bir diğer Java sürümünde eklenen eşdeğerleri bulunduğu için, ileti listesini anlamlandırmak işinin altından kalkmak görünenden kolay olacaktır. Bu gruba giren iletilerden aşağıdaki tablonun ilk sütununda verilmiş olanları kullanmanız daha akıllıca olacaktır. Zira, Collection ve List arayüzlerinde tanımlanmış olan bu iletilerin kullanımı, Vector yerine Veri Kapları Çerçevesi'ndeki diğer sınıfların kodunuzda değişiklik yapılmadan kullanılmasını sağlayacak ve yeniden kullanımı olanaklı kılacaktır.

Vector sınıfındaki eşdeğer iletiler
add(e)addElement(e)e'yi kabın sonuna ekler
add(i, e)insertElementAt(e, i)e'yi i indisli konuma ekler
clear()removeAllElements()Hedef nesneyi boşaltır
get(i)elementAt(i)i indisli elemanı döndürür
remove(e)removeElement(e)e ile eşit ilk konumdaki değeri siler
remove(i)removeElementAt(i)i indisli elemanı siler
set(i, e)setElementAt(e, i)i indisli elemanı e ile değiştirir ve eski değeri döndürür.

İçerik sorgulama ve güncelleme iletilerinin anlatımına geçmeden önce, eleman ekleme ve silme iletilerinin kullanımı sırasında akılda tutulması yararlı olacak şu noktayı hatırlatalım: ekleme sırasında, eklemenin yapıldığı indis sonrasındaki elemanlar sona doğru, silme sırasında, silinen elemanın sonrasındaki elemanlar öne doğru kaydırılacaktır. Dolayısıyla, bu işlemlerin etkilediği indis kap önlerine yaklaştıkça maliyet artacaktır.

İki uyarlaması bulunan add, hedef nesneye yeni eleman eklemeye yarar. Bu iletilerden, tek argümanlı olanı, argümanındaki değeri Vector nesnesinin sonuna ekleyip true döndürürken, iki argümanlı olanı, ikinci argümandaki değeri ilk argümanda belirtilen indisteki konuma ekler. Eklemenin toptan yapılması istendiğinde, bir döngü veya pek çok add iletisi kullanmaktansa addAll iletisi kullanılabilir. add iletisine koşut iki uyarlaması bulunan bu ileti, kendisine geçirilen Collection arayüzünü destekleyen kabın içindeki elemanları gezicisinin döndürdüğü sırada hedef nesneye ekler.

get iletisi, yegâne argümanında belirtilen indisteki elemanı döndürür. Bu iletinin özel kullanımları için, sırasıyla ilk ve son elemanları döndüren firstElement ve lastElement iletileri tercih edilebilir. Ancak, kodun yeniden kullanım kaygıları ağır basıyorsa, List arayüzünde tanımlanmış get'i yeğlemek daha yerinde olacaktır. Bir grup ardışık elemanın döndürülmesi ise, List arayüzü türündeki tutacakla gösterilen bir Vector nesnesi döndüren subList iletisi ile olanaklıdır. Bu ileti, hedef nesnenin ilk argümandaki indisten başlayıp ikinci argümandaki indisin bir öncesinde sonlanan dilimini döndürür.

indexOf ve lastIndexOf iletileri de sorgulama amacıyla kullanılablir. get verilen bir indisteki elemanı döndürürken, indexOf ve lastIndexOf eleman türündeki bir değer alıp bu değerin hedef nesnede geçtiği indisi döndürür. İki uyarlaması var olan indexOf, tek argümanlı kullanılacak olursa argümanda geçirilen nesnenin bulunduğu ilk indisi döndürürken, iki argümanlı kullanılırsa, ilk argümanda geçirilen nesnenin ikinci argümanda belirtilen indisten sonra bulunduğu ilk yerin indisini döndürür. Benzer şekilde; lastIndexOf, tek argümanlı kullanılacak olursa argümanda geçirilen nesnenin bulunduğu son indisi döndürürken, iki argümanlı kullanılırsa, ilk argümanda geçirilen nesnenin ikinci argümanda belirtilen indisten önce bulunduğu ilk yerin indisini döndürür. Aranan nesnenin hedef nesnede bulunmaması kullanıcıya -1 döndürülerek bildirilecektir.

Sorgulama amacıyla yararlanabileceğimiz bir diğer ileti çifti, yegâne argümanlarında geçirilen nesnenin veya Collection arayüzünü destekleyen kaptaki elemanların hedef nesnede geçip geçmediğinin yanıtını veren contains ve containsAll yüklemleridir.

Hedef nesneden eleman silme işlemi, argüman olarak indis veya silinmesi istenen değere eşit bir nesne bekleyen iki remove iletisi ile karşılanır. İndis alan uyarlama istenen konumdaki elemanı silip sonucu olarak döndürürken, diğer ileti argümanındaki nesnenin hedef nesne içinde, varsa, geçtiği ilk noktadaki elemanı siler ve silmenin gerçekleşmesi durumunda true, aksi halde false döndürür.

Çoklu silme, removeAll, retainAll veya clear iletilerinden biri kullanılarak yapılabilir. removeAll, argümanındaki Collection arayüzünü destekleyen kap içindeki elemanların hedef nesnedeki bütün kopyalarını silerken, retainAll argümandaki kabın elemanlarının hedef nesnedeki tüm kopyalarının korunması ve geri kalan elemanların silinmesini sağlar. Her iki ileti de, işleyişleri sırasında hedef nesneyi değiştirecek olurlarsa true, aksi takdirde false döndürür. Son olarak, clear komutu, hedef nesnedeki tüm elemanları siler.

Eleman güncellemede kullanılan set iletisi, ilk argümanında sağlanan indisteki elemanı ikinci argümandaki değer ile değiştirir ve değişim öncesindeki eleman değerini sonucu olarak döndürür.

Değineceğimiz bir sonraki ileti grubu, gezici nesnesi döndürenler.1 listIterator adlı bu iletiler, hedef nesneyi çift yönlü dolaşıp, güncellememizi sağlayan ListIterator türlü bir gezici nesne döndürür.2 Bunlardan argümansız olanı, dolaşmaya hedef nesnemizin ilk indisinden başlarken, tek argümanlı olanı, dolaşmaya argümanda belirtilen indisten başlar. İstenen yere konuşlanılmasını takiben, gezici nesneye sonraki/önceki elemanı döndürme (next, previous), o anki konuma ekleme (add), o anki konumdaki elemanı güncelleme (set) ve silme (remove) imkanını veren iletiler gönderilebilir. Geziciye gönderilen iletiler esas etkilerini dolaşılmakta olan kap üzerinde gösterecektir. Bunu, Vector nesnesi içindeki 3 değerine sahip elemanları gezici vasıtasıyla silen aşağıdaki kod parçasından görebilirsiniz.
import java.util.ListIterator;
...
ListIterator<Integer> gezici = intVec4.listIterator();
while (gezici.hasNext())
  if (gezici.next() == 3) gezici.remove();
Göz atacağımız son ileti grubu, Vector yerine bir başka yapının gerektiği veya daha uygun olduğu zamanlarda ihtiyacını duyduğumuz dönüşümü sağlar. Öncelikle, Veri Kapları Çerçevesi'nde ArrayList, HashSet ve LinkedList'in de içinde olduğu pek çok sınıf, Collection arayüzünü gerçekleştiren bir kap bekleyen yapıcıya sahiptir. Bu yapıcılar, geçirilen kabı baştan sona dolaşarak elemanları yaratılmakta olan yeni kaba eklerler. Dolayısıyla, Vector ve diğer pek çok sınıfın Collection arayüzünü gerçekleştirdiği anımsanacak olursa, Vector nesnelerinin pek çok diğer türden kaba ve diğer türden kapların Vector nesnesine çevrilmesi kolaylıkla mümkün olacaktır.

Vector nesneleri dizi nesnelerine copyInto ve toArray iletileri kullanılarak dönüştürülebilir. copyInto hedef nesnenin içerdiği elemanları argümanında sağlanan dizinin içine doldururken dizinin yeterli uzunlukta olmamasını kullanıcıya IndexOutOfBoundsException ayrıksı durumu ile bildirir. Buna karşılık, benzer imzaya sahip toArray iletisi, argümanda geçirilen dizinin yetersiz kalması durumunda sızlanmaz ve gerekli uzunluğa sahip yeni bir dizi nesnesi yaratıp işini bu yeni dizi üstünde tamamladıktan sonra bu diziyi döndürür. toArray'in argümansız ikinci uyarlaması da, hedef nesnenin eleman sayısına sahip bir dizi döndürerek işini görür.3

  1. elements iletisi iterator ve listIterator iletilerinin eklenmesi ile kullanımdan düşmüştür. Dolayısıyla, anlatımımız bu iletiyi kapsamayacaktır.
  2. İletilerin adı olarak vectorIterator yerine listIterator (liste gezici) seçilmiş olması sizi şaşırtmasın. Ne de olsa baştan sona dolaşılmak tüm doğrusal veri yapıları için makul bir işlem ve bu yüzden bu işleme karşılık gelen iletiler List arayüzüne konulmuş.
  3. Bu iletinin Vector sınıfındaki gerçekleştirimi Arrays.asList metodu ile bir bütün olarak düşünülmelidir.

8 Eylül 2011 Perşembe

Kategorize Etmenin Yararları ve Arayüzler

Her ne kadar Bülent Ortaçgil, beni kategorize etme, demişse de, kategorizasyon çevremizi anlamamızı kolaylaştıran ve sıklıkla başvurduğumuz çok önemli bir keşif aracıdır. Birbirlerine benzesin ya da benzemesin, değişik türden varlıkların ortak yönlerini gruplamamızı olanaklı kılarak soyutlama yapmamızı sağlayan bu araç, keşfimizin paylaşılabilir olması durumunda, kendimizi daha kısa yoldan anlatmamızı da olanaklı kılar. Örneğin, üçüncü bir şahsın obez olduğu konusundaki gözlemimizi paylaştığımızda, karşımızdaki kişi, hangi ulustan olursa olsun, konuşmaya konu olan kişinin saçının, gözlerinin ne renk olduğunu bilemese de nasıl vücut hatlarına sahip olduğunu kafasında canlandırılabilir. Çünkü, ortak tanımı üzerinde anlaşılmış—yani, standart bir tanımı bulunan—obez olma özelliği kişilerin ait olduğu grupları aşan bir özelliktir; birbirleriyle hiç alakası bulunmayan grupların üyeleri arasında ortak payda olarak kullanılabilir. Yani, kafamızın içindeki gri materyalin tasnif etme donanımı, biz istesek de istemesek de, diğer kişileri müstahdem, öğretmen, yönetici, vb. şeklinde sınıflandırdığı gibi, obez, zeki, işbilir, vb. şeklinde de kategorize etmektedir. Yeri geldiğinde, sınıflarla kategoriler bitiştirilmekte ve obez öğretmen, işbilir yönetici gibi yeni sınıflamalar da yapılabilmektedir.1

İşte bu yazımızda, Java programlama dilinin nesneler alemini tasnif etmekte sınıf ile birlikte kullanılabilecek ikinci üstkavramı olan arayüz kavramına değineceğiz. Ama ilk önce pedagojik senaryomuz. Yer ülkemizdeki liselerden birinin müdüriyeti ve uyuşturucunun ilk paketinin bedava olduğunu bildiği için bazı şirketlerce öğretim ordusunun üyelerine "bedava" dağıtılan programlama dillerini kullanmayı reddederek Java'yı tercih eden müdürümüz, yönetimindeki öğrenci ve öğretmenlerin performansını özetleyecek bir programı nasıl yazabileceğini düşünmekte. Müdürümüzün amacı, öğrenci ve öğretmenleri kendi aralarında uygun bir ölçüte göre sıralayıp listelemek. Çiziktirmelerle dolu sayfalara baktığımızda, müdürümüzün öğrencileri önce ortalamaları sonra—ortalamaların eşit olması durumunda—numaraları karşılaştırırak sıraya dizmeyi düşünürken, öğretmenleri önce öğrenci anketlerindeki konu yeterliliği notunun ortalamasını sonra ders yükünü karşılaştırırak sıraya dizmek istediğini görüyoruz.

Yordamsal programlama alışkanlıkları olduğu anlaşılan müdürümüz, ilk çözüm girişiminde Performans adlı bir sınıftaki dört metot ile işini halletmeye çalışmış. Buna göre, sıralama metotları kendilerine geçirilen veri kümesinin elemanlarını ilişkin karşılaştırma metodunu çağırarak sıraya dizmekte.

Performans.java
public class Performans {
  ...
  public static int karşılaştır(Öğrenci sol, Öğrenci sağ) {
    ...
  } // int karşılaştır(Öğrenci, Öğrenci)

  public static int karşılaştır(Öğretmen sol, Öğretmen sağ) {
    ...
  } // int karşılaştır(Öğretmen, Öğretmen)

  public static void sırala(Öğrenci[] veri) { ... }

  public static void sırala(Öğretmen[] veri) { ... }
  ...
} // Performans sınıfının sonu

PerformansÖğrenci ve Öğretmen sınıflarına bağımlı kılması—Öğrenci ve Öğretmen sınıflarının değişmesi ilişkin karşılaştırma metotlarının değişmesine neden olabilir—ve sıralama metotlarının neredeyse aynı olmasından—ne de olsa, aynı algoritma kullanılıyor—huylanan müdürümüz, static niteleyicisinin nesne paradigmasının düşmanı olduğunu anımsaması ve, düşülen nota inanırsak, İnternet'teki bir yazıyı🔎 okuması sonrasında, sınıf ve kalıtlama kavramlarının kullanıldığı bir çözümü yeğleyerek bu çözümden vazgeçmiş.

Karşılaştırılabilir.java
public abstract class Karşılaştırılabilir {
  public abstract int karşılaştır(Karşılaştırılabilir sağ);
} // Karşılaştırılabilir sınıfının sonu

Öğrenci.java
public class Öğrenci extends Karşılaştırılabilir {
  ...
  public int karşılaştır(Karşılaştırılabilir sağ) {
    Öğrenci sağTaraf = (Öğrenci) sağ;
    ... // ileti alıcı ile sağTaraf'ı karşılaştır.
  } // int karşılaştır(Karşılaştırılabilir)
  ...
} // Öğrenci sınıfının sonu

Öğretmen.java
public class Öğretmen extends Karşılaştırılabilir {
  ...
  public int karşılaştır(Karşılaştırılabilir sağ) {
    Öğretmen sağTaraf = (Öğretmen) sağ;
    ... // ileti alıcı ile sağTaraf'ı karşılaştır.
  } // int karşılaştır(Karşılaştırılabilir)
  ...
} // Öğretmen sınıfının sonu

Performans.java
public class Performans {
  ...
  public static void sırala(Karşılaştırılabilir[] veri) {
    ...
  } // void sırala(Karşılaştırılabilir[]) sonu
  ...
} // Performans sınıfının sonu

Anlaşılan, yukarıda kaba hatlarıyla verilen çözümün, Java'nın çoklu sınıf kalıtlamayı desteklememesi nedeniyle, Öğrenci ve Öğretmen'in bir diğer sınıftan daha—mesela, iki sınıfın ortak noktalarını içeren Kişi gibi—türetilmesini engellediğini gören ve karşılaştırmanın genel bir işlem olduğunu düşünen müdürümüz, Karşılaştırılabilir'in aslında gereksiz olduğuna karar vermiş ve söz konusu işleme karşılık gelen iletiyi Object sınıfının içinde aramış. Buna gerekçe olarak da, equals (eşitlik denetimi) ve toString (hoş yazım) gibi tüm nesneler için geçerli olan iletilerin tüm sınıfların ortak noktalarını barındıran Object'te yer alması gerektiğini not düşmüş. Görünen o ki, bu varsayımın arama sonucunda yanlışlanması, müdürümüzü Java programlamayı iyi bilen bir matematik hocasının kapısını çalmaya zorlamış. Derdinin çaresinin Object sınıfında değil, arayüz kavramının kullanımında yattığını söyleyen matematik hocası, küme, matris, karmaşık sayı gibi matematiksel nesnelerin aslında karşılaştırılabilir olmadığı konusunda garanti vererek müdürümüzü ikna etmiş. Gelin bundan sonrasını birlikte görelim.2

Birbirleriyle ilintili veya ilintisiz olabilecek kimi kavramların ortak özelliklerini soyutlamaya yarayan kategoriler Java'da arayüz üstkavramı ile karşılık bulurlar; bir arayüzün tanımlanması, Java sınıf kitaplığını o arayüzü gerçekleştiren ve gerçekleştirmeyen sınıflar olmak üzere iki kategoriye ayırır. Bu tanımda altı çizilmesi gereken noktalar, aynı arayüzü gerçekleştiren sınıfların birbirleriyle ilintisiz olma ve kimi sınıfların arayüzü gerçekleştirmeme olasılığıdır. Örneğin, karşılaştırılabilir sınıflar, Öğretmen ve Müstahdem gibi ortak üstsınıfları (Çalışan) yoluyla ilintilendirilebilirken, tamsayılar ve vergi mükellefleri gibi alakasız da olabilirler. Aynı şekilde, tamsayılar karşılaştırılabilirlik özelliği taşırken, disk dosyalarını soyutlayan sınıfların aynı özelliği taşıması beklenmez.3

Karşılaştırılabilir.java
public interface Karşılaştırılabilir {
  public int karşılaştır(Object);
  /* public static final */ String
    BELİRLEYİCİ_KOMİTE = "Ağır Abiler, A.Ş.";
} // Karşılaştırılabilir arayüzünün sonu

Arayüz tanımı, destekleyen sınıfların gerçekleştirmesi zorunlu olan iletilerin imzalarını ve bu sınıfların üyesi olan nesnelerin tümü için değişmeyen özellikleri içerir. Örneğin, yukarıdaki arayüz karşılaştır adlı bir iletiyi gerçekleştiren sınıflara ait nesneleri soyutlarken, bu nesnelerin ele alınması esnasında yararlanılacak bir sabit tanımlamaktadır.

Arayüz tanımının kuru kuruya kullanıcılara sağlanması pek yararlı olmayacaktır; arayüzü belirleyen geliştiricilerin, arayüzün gerçekleştirimleri arasında tutarlılığı sağlamak adına, derleyicinin kullanım doğruluğu denetimleri için gerekli olan yukarıdaki sözdizimsel tanımın yanısıra, arayüzdeki öğelerin anlamlarına dair açıklamaları da sağlaması gerekir. Örneğin, karşılaştır iletisinin imzasının sağlanması yalnız başına bir işe yaramayacaktır; iletinin ne yaptığına, parametrelerin ve döndürülen değerin ne anlama geldiğine aşağıdaki gibi açıklık getirilmelidir. Yani, arayüzün sözdizimsel tanımına ek olarak anlambilimsel açıklamasının da sağlanması gereklidir.
karşılaştır iletisi, hedef nesne ile argümanda geçirilen nesneyi karşılaştırır. Hedef nesnenin büyük olması durumu döndürülen pozitif bir int ile bildirilirken, eşitlik durumu 0 ile, küçük olma durumu ise negatif bir int ile bildirilir. Hedef nesne ile argümandaki nesnenin uyumsuz olması, kullanıcıya ClassCastException ayrıksı durumuyla bildirilmelidir. Benzer şekilde, argümanda geçirilen tutacağın null olmasının, kullanıcıya NullPointerException ayrıksı durumuyla bildirilmesi beklenir.

Arayüzün gerçekleştirimci ile kullanıcıları arasında sözleşme görevi gören bir bağlantı noktası oluşturduğu unutulmamalıdır; hem gerçekleştirimci hem kullanıcı, arayüz tanımından değişik amaçlarla da olsa yararlanacaktır. Bu, arayüz tanımındaki bir değişikliğin, birbirlerinden çok farklı yerlerde bulunan pek çok kişinin kodunu değiştirmesi gibi maliyetli bir sonuca neden olabileceği anlamına gelir. Dolayısıyla, değişme ihtimali yüksek olan altalan tanımı ve metot gövdesi gibi gerçekleştirim ayrıntılarının arayüz tanımında yer almasına izin verilmez. Altalan ve metot gerçekleştirimlerinin olması gerektiği durumlarda, arayüz yerine soyut sınıf kullanımı düşünülmelidir.

Arayüz tanımındaki tüm öğelerin public erişimli ve tüm veri öğelerinin derleme anı sabiti—yani, static final ile nitelenmiş—olduğu varsayılır. Bunun nedeni, arayüzün, önceki paragrafta değindiğimiz gibi, birbirinden bağımsız gerçekleştirimci ve kullanıcılar arasında sözleşme görevini görmesidir; arayüzde değişmesi muhtemel ve kamuya açık olmayan gerçekleştirim ayrıntıları bulunamaz. [Müzik setinizin arayüzünde kullandırılmayan bir düğmenin bulunmasını mantıklı bulur musunuz?] Bu sayede kullanıcı, arayüzü hangi sınıf gerçekleştiriyor olursa olsun, arayüz tutacağını arkasındaki nesnenin türünden bağımsız bir şekilde ele alıp kodun yeniden kullanımını sağlayabilecektir. Bunu, aşağıdaki örnek üzerinden görelim.
public class Performans {
  ...
  public static void sırala(Karşılaştırılabilir[] veri) {
    if (veri.length <= 1) return;

    boolean takasYapıldı = false;
    int geçiş = 1;
    do {
      takasYapıldı = false;
      for (int i = 0; j < veri.length - geçiş; i++)
        if (veri[i].karşılaştır(veri[i + 1]) > 0) {
          Karşılaştırılabilir geçici = veri[i];
          veri[i] = veri[i + 1];
          veri[i + 1] = geçici;
          takasYapıldı = true;
        } // if sonu
      geçiş++;
    } while(geçiş < veri.length && takasYapıldı);
  } // void sırala(Karşılaştırılabilir[]) sonu
  ...
} // Performans sınıfının sonu
Yukarıdaki koda göre, sırala metodu Karşılaştırılabilir arayüzünü gerçekleştiren herhangi bir sınıfın nesnelerini gösteren tutacaklardan oluşan bir dizi alabilir. Metot içinde, geçirilen dizinin elemanlarının Karşılaştırılabilir arayüzünde listelenen özellikleri kullanılabilecektir. sırala metodunu nesnenin diğer özelliklerinden koparan bu kısıtlama, bir nesneye hangi iletilerin gönderilebileceğinin tutacak süzgecinden geçmesi gerektiği kuralı ile de uyumludur: arayüz tutacağı ile gösterilen bir nesneye, tutacağın izin verdiği—yani, ilişkin arayüzde listelenen—iletiler gönderilebilir; iletinin gönderilmesi sonrasında nesnenin ait olduğu sınıftaki aynı imzalı metot çağrılacaktır. Örneğin, programın çalıştırılması sonrasında yukarıda işaretlenmiş olan satırdaki gönderinin sonrasında hangi metodun çağrılacağına geçirilen dizinin eleman türüne göre çalışma anında dinamik iletimle karar verilecektir. Bir diğer deyişle, bir sınıftaki iletilerin söz konusu sınıfın kalıtlama [ilişkisi] ile ilişkilendirildiği altsınıfların nesnelerine gönderilmesi durumunda olduğu gibi, bir arayüzde listelenen iletiler, söz konusu arayüzün gerçekleştirme [ilişkisi] ile ilişkilendirildiği sınıfların nesnelerine gönderilmeleri durumunda çokbiçimli olarak davranır. Dolayısıyla, sırala Karşılaştırılabilir arayüzünü gerçekleştiren tüm sınıflar için işini sınıfın özelliklerine göre yaparak çalışacak ve bu sayede kodun yeniden kullanımını sağlayacaktır.

İlgilenenlerin bir arayüzü gerçekleştirmesi, ilişkin sınıf ile arayüz arasındaki gerçekleştirme ilişkisinin sınıf başlığında belirtilmesi ve arayüzde listelenen tüm iletilere karşılık metotların sağlanması ile mümkün olur. Sınıf ve arayüz arasındaki ilişki, sınıf adından sonra implements anahtar sözcüğü yoluyla belirtilir; kalıtlama ilişkisine dair bir bilginin bulunması durumunda, gerçekleştirme ilişkisine dair bilginin bunu takip etmesi gerekir.
public class Öğrenci implements Karşılaştırılabilir {
  ...
  public int karşılaştır(Object sağ) {
    if (this == sağ) return 0;

    Öğrenci sağTaraf = (Öğrenci) sağ;

    if (_ortalama > sağTaraf._ortalama) return 1;
    else if (_ortalama < sağTaraf._ortalama) return -1;
    if (_no > sağTaraf._no) return 1;
    else if (_no < sağTaraf._no) return -1;
      else return 0;
  } // int karşılaştır(Object) sonu
  ...
  private float _ortalama;
  private long _no;
} // Öğrenci sınıfının sonu
public class Öğretmen implements Karşılaştırılabilir {
  ...
  public int karşılaştır(Object sağ) {
    if (this == sağ) return 0;

    Öğretman sağTaraf = (Öğretmen) sağ;

    if (_ankNotu > sağTaraf._ankNotu) return 1;
    else if (_ankNotu < sağTaraf._ankNotu) return -1;
    if (_dersYükü > sağTaraf._dersYükü) return 1;
    else if (_dersYükü < sağTaraf._dersYükü) return -1;
      else return 0;
  } // int karşılaştır(Object) sonu
  ...
  private float _ankNotu;
  private int _dersYükü;
} // Öğretmen sınıfının sonu
Gerektiği durumlarda, bir sınıf birden çok sayıda arayüz gerçekleştirebilir; bu, gerçekleştirilen arayüzlerin birbirlerinden ',' ile ayrıldığı arayüz listesinin implements'ten sonra belirtilmesi ile mümkün olur. Böyle bir sınıfın kullanıcıları, bu sınıfa ait bir nesneyi sınıf ve üstsınıflarının tutacaklarının yanısıra herhangi bir arayüzünün tutacağı yoluyla da kullanabilir.

Kimi zaman, yeni bir arayüz daha önceden tanımlanmış olan bir arayüzün ileti listesine ekler yapılarak tanımlanabilir. Yazılımların sürüm değişikliği ile birlikte arayüzlerin yeni uyarlamalarının tanımlanması buna tipik bir örnektir. Bu gibi bir durumda, yeni arayüz sıfırdan tanımlanabileceği gibi, eski arayüzden kalıtlayacak şekilde tanımlanabilir. Bu yolu seçmenin getirisi, derleyicinin arayüzler arasındaki kalıtlama ilişkisini sınıflar arasındaki kalıtlamaya benzer bir şekilde yorumlayacak olmasıdır: yeni arayüz (altarayüz) eski arayüz (üstarayüz) gibidir; eski arayüzdeki iletilerin tümünü içeren yeni arayüzün tutacağı, eski arayüz türündeki tutacağın kullanıldığı her yerde kullanılabilir. Ne kastettiğimizi aşağıdaki tanımlar üzerinden görelim.
public interface A1 {
  void i1();
  void i2();
} // A1 arayüzünün sonu
public interface A2 extends A1 {
  void i1(int i);
  void i3();
} // A2 arayüzünün sonu
Sağlanan tanımlara göre, A2 arayüzünü destekleyen bir sınıfın A1'dekileri de içeren dört iletinin karşılığındaki metot gerçekleştirimlerini sağlaması gerekecektir: i1(), i2(), i1(int) ve i3().4 Bu noktada, arayüz kalıtlamanın, sınıf kalıtlamanın aksine, çoklu olduğu unutulmamalıdır. Yani, bir arayüz birden fazla üstarayüzden kalıtlayabilir. Çoklu arayüz kalıtlamadan yararlanıldığı durumlarda, üstarayüzlerden gelen öğelerin ad çakışmalarına dikkat edilmelidir. Aynı imzaya sahip olup değişik dönüş türüne sahip iletilerin kalıtlanması derleme hatasına neden olacağı gibi, aynı ada sahip olup değişik arayüzlerde farklı türlere ait tanımlanan sabitler, kullanıldıkları noktada derleme hatasına yol açacaklardır.

Son olarak, arayüzlerin önemli bir görevine değinerek yazımızı bitirelim: yazılım bileşenleri için standart oluşturmayı mümkün kılmaları. Bileşen standartları, bir bileşenin aynı arayüzleri destekleyen bir diğeri yerine tak-çıkar mantığıyla kodun güncelllenmesini gerektirmeden değiştirilmesine olanak tanır. [Yani, eskimesi nedeniyle doğramanızı değiştirmek istediğinizde duvarın yıkılmasına gerek olmayacaktır.] Bunu aşağıda verilen zamana göre sıraya dizilmiş çizelgeyi izleyerek görelim.

  1. Bir grup uzman, ortaya çıkan ihtiyaç üzerine A1 arayüzünü tanımlar.
  2. Ş1 şirketi, A1'i gerçekleştiren Ş1_A1 sınıfını piyasaya sürer.
  3. Ş2 şirketi, Ş1'den Ş1_A1'i satın alır ve kendi programlarında kullanmaya başlar.
  4. Ş3 şirketi, A1'i gerçekleştiren Ş3_A1 sınıfını piyasaya sürer.
  5. Ş1_A1'in performansından muzdarip Ş2 şirketi, Ş3'ten Ş3_A1'i alır ve Ş1_A1'in yerine kullanmaya başlar.

Dikkat edilecek olursa, Ş3 şirketi Ş3_A1 sınıfını Ş2 sınıfının geliştirdiği programlardan sonra geliştirmiştir. Bir diğer deyişle, Ş2 şirketinin geliştirdiği programlar, kendilerinden sonra geliştirilen bileşenleri kullanmaktadır. Zaman içinde geleceğe yapılan bu yolculuk, arayüzlerin değişmez özellikler barındırması ve sözdizimlerinin yanısıra anlamlarının da tanımlanmak suretiyle gerçekleştirimci kullanıcı arasında bir sözleşme oluşturması ile mümkün olmaktadır.


  1. Obezlik örneğini çok da ilerilere götürmeyiniz. Çünkü, obez olan birisinin zayıflayarak zaman içinde bu özelliğini yitirmesi olanak dahilindedir ki, böylesine bir özelliğin, nesne yönelimli modelleme dünyasının arayüze benzer bir üstkavramı olan rol ile karşılanması daha doğru olacaktır. Arayüzler, ilişkin nesnelerin kaybetmeyeceği özelliklerini tanımlayabilir.
  2. Sağladığımız çözümdeki türlerin soysal tanımlanarak daha güvenli bir çözümün mümkün olduğunun bilinmesinde yarar var.
  3. Aslına bakılacak olursa, karşılaştırılabilirlik kategorisi Java platformundaki Comparable arayüzü tarafından hali hazırda tanımlanmıştır. Dolayısıyla, Karşılaştırılabilir arayüzüne gerek yoktur.
  4. Yeri gelmişken tekrar hatırlatalım: dönüş türü Java'da aynı aduzayındeki iletileri (ve metotları) ayrıştırmak için kullanılmaz. Dolayısıyla, A2'ye int dönüş türlü i1() iletisinin eklenmesi, ortaya çıkan muğlaklık nedeniyle, derleme hatasına yol açacaktır.