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.

Hiç yorum yok:

Yorum Gönder

Not: Yalnızca bu blogun üyesi yorum gönderebilir.