Java’da Performans Arttırıcı Yollar - Bölüm 2
7 Ocak 2006 tarihli, Java, Programlama köşesine ait yazı.
Java’da performans arttırıcı yollar yazı dizisinin 2. bölümünde string konusunu ele alacağız. Ön bilgi olması açısından serinin ilk yazısını okumanızı tavsiye ederim. Ayrıca bu bölümün serinin en zevkli ve önemli bölümü olduğu kanaatindeyim.
4. String’ler
String java’da çok kullanılan bir veri tipidir. String nesnesi 16-bit Unicode karakter array’i ve string’in uzunluğunu içerir.
4.1 String’ler Değişmez
Java string’lerinin en önemli özelliği yaratıldıktan sonra değiştirilememeleri.Örnek olarak:
String str = “testing”;
str = str + “abc”;
“testing” string’i bir kere yaratılır ve değişmez. Sadece str‘nin point ettiği string adresi değişir. Yukarıdaki durumda “testing” string’ine “abc” eklenmiş, sonuçta yeni bir string objesi oluşturulmuş, bu objenin de adresi str‘ye atanmıştır. Bu işlem aşağıdaki şekilde de yapılabilir:
String str = “testing”;
StringBuffer tmp = new StringBuffer(str);
tmp.append(”abc”);
str = tmp.toString();
Başka bir değişle StringBuffer birleştirilecek iki string’i geçici bir alana kopyalayıp sonuçtan tekrar bir string oluşturur. Tabii ki bu daha verimsiz bir durum olarak görünebilir. Ancak StringBuffer ve string birleştirme operatörü olan +‘nın karşılaştırması bize StringBuffer kullanmamız yeri açıkça belirtiyor. Aşağıdaki program basitçe oluşturduğumuz string’e 10000 tane * ekliyor:
public class str_app {
public static void main(String args[]) {
final int N = 10000;
/* + kullanarak */
String s1 = “”;
for(int i=1;i<=N; i++)
s1 = s1 + “*”;
/* StringBuffer kullanarak */
StringBuffer sb = new StringBuffer();
for(int i=1;i<=N; i++)
sb.append(”*”);
}
}
Test sonucu bize StringBuffer kullanıldığında işlemin ~40 kat hızlı tamamlandığını gösteriyor. Sonuç olarak string birleştirme operatörleri olan + ve += basit string birleştirmelerinde, StringBuffer ise art arda gelen birleştirmelerde kullanılması mantıklı olacaktır.
4.2 char[] ile String Oluşturma
Java’da char[] ile direkt String yaratabilirsiniz, ancak gelen veri UTF-8 ile kodlanmış bir byte[] ise aşağıdaki kod hayli işinize yarayacaktır. Bildiğiniz gibi UTF-8 kodlamasında her karakter 1,2 veya 3 byte uzunluğunda olabiliyor. Bunun için yapmamız gereken gelen byte[] uzunluğunda bir char[] yaratıp, karakterleri tanıyıp char[]’e eklemek, ve sonuçta bu array’den string oluşturmak.
/* UTF-8′den String yaratma */
static String UTFtoString(byte b[]) {
int maxlen = b.length;
char str[] = new char[maxlen];
int outlen = 0;
for(int i=0;i<maxlen; i++) {
byte c = b[i];
if ((c & 0×80) == 0) {
str[outlen++] = (char)c;
}
else if ((c & 0xf0) == 0xe0) {
str[outlen++] = (char)(((c & 0xf) << 12) |
((b[i+1] & 0×3f) << 6) |
(b[i+2] & 0×3f));
i+=2;
}
else {
str[outlen++] = (char)(((c & 0×1f) << 6) |
(b[i+1] & 0×3f));
i++;
}
}
return new String(str, 0, outlen);
}
4.3 String Karşılaştırmasında == ve String.equals() Kullanımı
Belki daha önce C++ gibi, operator overloading destekleyen, bir dilde program yazdıysanız string’leri == ile karşılaştırmaya alışmış olabilirsiniz. Bu java’da da vardır, yalnız java == operatörü referanslara uygunlandığında referansların point ettiği objeleri değil, referansların aynı olup olmadığını karşılaştırır. Örnek olarak java şu iki string’i karşılaştırdığında sonuç:
String s1 = “abc”;
String s2 = “def”;
s1 == s2;
tabii ki false olacaktır, ancak bu string’ler aynı olmadığından değil, referansları farklı olduğundandır. Hal böyle iken string karşılaştırmasında equals() metodunu kullanıyoruz. Yazıda şöyle bir string karşılaştırma tekniğinin kullanılması öneriliyor:
if (s1 == s2 || s1.equals(s2))
…
Burada ilk olarak iki referansın aynı olup olmadığına bakılacak, aynı ise short-circuit yani kısa devre olacağından ikinci kısım evaluate edilmeyecektir. Tabii ki sadece referansların aynı olup olmadığını kontrol etmek equals() metodundan çok kısa bir sürede tamamlanacaktır.
4.4 String Interning
Önceki bölümdeki fikir interning kavramı ile bir adım daha ileri götürülebilir. String nesnesinin intern() metodu birbirinin aynı olmayan string’lerden oluşan bir string havuzu yaratmak için kullanılır.
str = str.intern();
string eğer bu havuzda yoksa ekler ve havuzdaki referansı return eder. Böylece string’ler == operatörüyle karşılaştırılabilir.
public static void main(String args[]) {
String str = new String(”testing”);
/* interning olmadan */
if (str == “testing”)
System.out.println(”equal”);
else
System.out.println(”unequal”);
/* interning yapıldığında */
str = str.intern();
if (str == “testing”)
System.out.println(”equal”);
else
System.out.println(”unequal”);
}
İlk örnek her iki string de “testing” olmasına karşın referanslar aynı olmadığından eşit olmadığını belirtirken ikinci örnekte her iki string de string havuzunda aynı string’i point ettiğinden eşit olarak belirtecektir. Sonuç olarak string interning, string havuzuna ekleme yapılması dışında, string karşılaştırması için çok hızlı bir yöntemdir.
4.5 String Uzunluğu
Hepimizin çoğunlukla gözü kapalı yazdığı for loop’lar için yazıda çok güzel bir tekniğe değinilmekte. İşin mantığı string’in length() metodunu her loop döndüğünde değil, sadece başta bir kez çağırmak. Aşağıdaki örnek daha açıklayıcı olacaktır:
final int N = 1000000;
StringBuffer sb = new StringBuffer();
for(int i=1;i<=N; i++)
sb.append(’*');
String s = sb.toString();
/* sürekli length() çağrılarak */
int cnt = 0;
for(int i=0;i<s.length(); i++) {
if (s.charAt(i) == ‘x’)
cnt++;
}
/* bir kez length() çağrılarak */
cnt=0;
for(int i=0,len=s.length(); i < len; i++) {
if (s.charAt(i) == ‘x’)
cnt++;
}
Sonuçlar gösteriyor ki her seferinde string’in uzunluğunu istemek performansı ~1,5 kat etkilemekte. Loop değişkenleri tanımlanırken sadece bir kez çağırılması ise hayli mantıklı.
4.6 toCharArray() Kullanımı
String içindeki charAt() metodu da aynı length() gibi bazı durumlarda performans kayıplarına yol açabiliyor. Alternatif yol olarak toCharArray() metodunun kullanılması öneriliyor. Bu metod string’i bir char[] olarak return ediyor.
final int N = 1000000;
StringBuffer sb = new StringBuffer();
for(int i=1;i<=N; i++)
sb.append(’*');
String s = sb.toString();
/* charAt() kullanılarak */
int cnt = 0;
for(inti=0,len= s.length(); i < len; i++) {
if (s.charAt(i) == ‘x’)
cnt++;
}
/* toCharArray() kullanılarak */
cnt=0;
char ss[] = s.toCharArray();
for(inti=0;i< ss.length; i++) {
if (ss[i] == ‘x’)
cnt++;
}
İlk kısım ikinci kısmın yaklaşık 3 katı zamanda tamamlanıyor. Başka bir değişle yeni char[] yaratılırken harcanan ekstra süre charAt() metodu her çağırıldığında kaybedilen sürenin yanında devede kulak kalıyor. Şu noktaya da dikkatinizi çekmek isterim: ilk kısımda string’in uzunluğu önceki bölümdeki teknikle hesaplanmasına karşın ikinci kısımda her loop döndüğünde .length ile alınıyor. Bunun performansı fazla düşürmemesinin nedeni .length‘in bir metod olmaması, uzunluğu veren özel bir değişken olmasından kaynaklanıyor.
4.7 String’leri Sayılara Dönüştürmek
Düşünün ki elinizde “12.34″ gibi bir string var ve siz bunu nümerik forma çevirmek istiyorsunuz. Peki bu işlem size ne kadara mal olacak? Test etmek için küçük bir program yazmak yeterli:
final int N = 100000;
Double d;
/* string’den double yaratma */
for(int i=1;i<=N; i++)
d = new Double(”12.34″);
/* sayıdan double yaratma */
for(int i=1;i<=N; i++)
d = new Double(12.34);
String’den double yaratma işleminin diğerine göre ~15 kat daha fazla zaman aldığı görülüyor. Sayıları bir dosyadan okuyorsanız genelde fazla seçim şansınız olamaz, ancak bu işlemin ağırlığını akıldan çıkarmamak gerekli.