Java String Concatenation, StringBuilder ve StringBuffer
Gece gece canım sıkıldı, yine yerimde duramadım,
bir benchmark yapıp yazılım camiasında sık sık dile getirilen bir noktaya bir de ben parmak basayım dedim.
Biliyorsunuz ki Java ile birşeyler geliştirirken sık sık String
‘leri uç uca eklememiz gerekiyor.
En basit okul ödevinden en komplike web projelerine kadar bu işlemi
hiç düşünmeden concatenation yöntemi ile yapanlarımız var.
Hiç düşünür müydünüz bunun bir performans sorunu olarak değerlendirilebileceğini
ve alternatif yaklaşımlarının bulunduğunu?
Öncelikle bu işin kökenine inelim. Ben ilk olarak Java’da tecrübeli abilerimizden tokat yememek için biraz araştırma yaptım. Yine de yanlışlarım olabilir. Sonuçta yaşım kaç başım kaç? :)
Java’da karakter dizilerini bizim için anlamlı hale getiren üç temel sınıf var. Bunlar String, StringBuilder ve StringBuffer. Bu üçü hemen hemen benzer işlerde kullanılıyorlar fakat ihtiyacınıza göre hangisini kullanmanız gerektiğine karar vermek iyi bir yazılımcı olarak size kalıyor. Şimdi birazcık farklarına değinelim bunların.
String
Java’da yarattığımız her string varsayılan olarak bu türde yaratılır.
Yani iki tırnak içine karakterleri yazıp bunu bir değişkene atayacak olursanız,
bu değişkenin türü String
‘dir.
String
literatürde immutable, yani değişmez olarak geçer.
Dökümantasyonunda da farkettiyseniz,
String
‘in değişmesi için çağırabileceğimiz bütün fonksiyonlar yeni bir String
objesi döner.
Yani orjinal String
objesine birşeycik olmaz.
Yani aşağıdaki kod parçacığını çalıştırdığınızda elinizde birbirinden farklı 3 adet String
objesi olur.
String s1 = "aBcD1";
String s2 = s1 + "a."; // s1 değişmeden kaldı
String s3 = s1.toLowerCase(); // s1 yine değişmedi.
StringBuilder
StringBuilder
sınıfı String
objelerinin birleştirme işlemindeki gereksiz bellek
ve işlemci kullanımını azaltmak için yaratıldı.
Yazının son kısmında göstereceğim benchmark ile de gösteriyor ki
döngü içerisinde yapılan String
concatenation işlemlerinde büyük ölçüde performans sağlıyor.
Normalde her String
birleştirme işleminde yeni bir String
objesi yaratılır
ve varolan String
objesi değişmeden kalır.
Bu işlemler StringBuilder
sınıfı ile yapıldığında ise tek bir obje ile
istediğimiz kadar String
‘i birbirine ekleyebiliriz.
Aşağıda da basitçe StringBuilder
sınıfı nasıl kullanılır ona bakalım:
StringBuilder builder = new StringBuilder();
// append methodu ile istediğimiz kadar
// String'i StringBuilder objesine ekleyebiliriz.
builder.append("abcdef");
builder.append("qwerty");
builder.append("zxcvbb");
String str = builder.toString();
// str = "abcdefqwertyzxcvbb";
StringBuffer
Bu sınıf ile StringBuilder
yaptıkları iş olarak baktığımızda neredeyse tamamen aynı.
String
objesinin concatenation işlemlerinde fazladan bellek
ve işlemci kullanımına karşı ikisi de aynı avantajı sağlar.
Aralarındaki tek fark, StringBuffer
bu işi thread safe olarak yapar.
Thread safety kavramını açıklamanın yeri bu yazı değil fakat kabaca değinmek gerekirse,
şu şekilde açıklayabilirim:
eğer bir nesne birden fazla thread tarafından rastgele olarak kullanılıyorsa
ve bu nesne bir thread kendisiyle işini bitirmeden diğerinin işine başlamıyorsa,
o nesneye thread safe diyoruz.
Bir de kaba bir örnek vereyim.
Mesela StringBuilder
objesine iki farklı thread aynı anda erişip birisi “abc”,
diğeri de “def” append etmeye çalışırsa,
en son elde ettiğimiz String
‘in “adefbc”, “deabfc” veya “adbecf” olma
veya sistemin Exception
fırlatma ihtimalleri var.
Bu durumda ya synchronized değişkenler kullanarak elle StringBuilder
objesini koruyacağız,
ya da kendi kendini koruyabilen StringBuffer
objesini kullanacağız.
StringBuffer
objesinin thread safe yapısı da kendisini StringBuilder
‘a göre biraz yavaş kılıyor.
Yani eğer sisteminizde birden fazla thread yoksa, StringBuilder
kullanmanızı tavsiye ederim.
Şimdi yaptığım benchmark uygulamasının kodlarını dökeyim ortaya. Yaptığım işlem basitçe şu:
- Lorem ipsum satırlarından oluşan dosyayı okuyup her satırı global bir
ArrayList
‘e kaydet. ArrayList
içindeki elemanları kopyalayıp tekrar ArrayList’e ekleyerek 1044480 adet String’den oluşan bir ArrayList elde et.- Toplamda 100 defa
String
concatenation,StringBuilder
veStringBuffer
kullanarak ArrayList içerisindekiString
‘leri birleştir ve sürelerin ortalamalarını hesapla. - Oluşan sonuçları her adımda ve programın sonunda konsola yaz.
Dürüst olmak gerekirse String
concatenation işlemi o kadar uzun sürdü ki,
ilk stepte programı durdurup String
ile yaptığım işlemi comment-out ettim
ve tekrar çalıştırdığımda sadece StringBuilder
ve StringBuffer
‘ı karşılaştırdım.
String
işin içine girdiğinde çok daha küçük bir ArrayList
(16320 cümle) ile çalıştım
ve döngülerin iterasyon sayılarını da düşük tuttum ki gece bitmeden bir sonuca varabileyim.
Aşağıda içinde String
objesinin concatenation operatörünün de kullanıldığı 16320 cümleyi birleştirme süreleri:
Size of WORD_LIST: 16320
Started 1. main iteration...
concatenation: 52424.0 ms.
string builder: 8.0 ms.
string buffer: 9.0 ms.
Finished 1. main iteration...
Buradan anlayacağımız üzere,
16320 tane String
objesini birleştirmek için kesinlikle String
concatenation kullanmıyoruz :).
String
concatenation işlemini uygulamadan kaldırdıktan sonra
ArrayList
‘in boyutunu arttırıp (1044480 cümle) uygulamamızı 10 sample üretecek şekilde çalıştırırsak
şu sonuçları elde ediyoruz:
Size of WORD_LIST: 1044480
Started 1. main iteration…
string builder: 302.2 ms.
string buffer: 230.49999999999997 ms.
Finished 1. main iteration…
Started 2. main iteration…
string builder: 143.3 ms.
string buffer: 149.30000000000004 ms.
Finished 2. main iteration…
Started 3. main iteration…
string builder: 155.50000000000003 ms.
string buffer: 150.5 ms.
Finished 3. main iteration…
Started 4. main iteration…
string builder: 143.09999999999997 ms.
string buffer: 160.4 ms.
Finished 4. main iteration…
Started 5. main iteration…
string builder: 159.60000000000002 ms.
string buffer: 198.8 ms.
Finished 5. main iteration…
Started 6. main iteration…
string builder: 161.8 ms.
string buffer: 143.2 ms.
Finished 6. main iteration…
Started 7. main iteration…
string builder: 125.39999999999999 ms.
string buffer: 142.4 ms.
Finished 7. main iteration…
Started 8. main iteration…
string builder: 134.2 ms.
string buffer: 148.2 ms.
Finished 8. main iteration…
Started 9. main iteration…
string builder: 189.5 ms.
string buffer: 170.4 ms.
Finished 9. main iteration…
Started 10. main iteration…
string builder: 150.4 ms.
string buffer: 292.5 ms.
Finished 10. main iteration…
Average time for concatenation: 0.0 ms.
Average time for string builder: 166.49999999999997 ms.
Average time for string buffer: 178.61999999999998 ms.
Raporun sonundaki ortalamadan anlayacağımız gibi StringBuilder
objesi
StringBuffer
objesine göre daha hızlı bir şekilde,
daha kısa sürede işini hallediyor.
Peki bazı ara steplerdeki anormal sonuçları nasıl açıklayabiliriz?
Nasıl oluyor da bazen StringBuffer
öne geçiyor?
Bunu 6 ay önce olsa açıklayamazdım fakat İşletim Sistemleri dersini aldığım için artık açıklayabilirim.
İşletim Sistemleri Multi-Threading/Multi-Processing yapılarından dolayı sürekli olarak bizim işlemimizi yapmıyor.
Büyük bir for döngüsü bitene kadar işlemcide çalışamaz mesela.
İşletim sistemi o işlemi ara sıra duraklatıp bilgisayardaki diğer işlerle de ilgilenmek zorunda kalır.
Hatta bu uygulamada yarattığımız yoğun işlemci kullanımı sebebiye de
işletim sistemi bizim uygulamamızın önceliğini düşürüyor olabilir.
StringBuilder
çalışırken bizim işlemimiz işletim sistemi tarafından 10 kere duraklatılmış
ve StringBuffer
çalışırken de 8 kere duraklatılmış olabilir.
Bu da StringBuilder
kullanırken ölçtüğümüz zamanın daha fazla çıktığı stepleri kabaca açıklayabilir.
Özet
Çok fazla yazı yazdım fakat hiçbir şey anlatamadım gibi bir his var içimde. Bir özet geçelim:
String
immutable(değişmez) bir sınıftır.String
üzerinden yapacağımız çoğu işlem yeni birString
objesi yaratır ve orjinalString
objesi değişmez.- Çok fazla
String
birleştireceksekString
concatenation kullanmıyoruz. Bu işlem çok uzun sürer, yüksek bellek ve CPU kullanımına sebep olabilir. - Birden fazla thread kullanıyorsak ve birden fazla thread aynı
StringBuilder
objesine erişiyorsa, istenmedik sonuçlar doğabilir. Bu durumdaStringBuffer
kullanıyoruz. - Birden fazla thread kullanmıyorsak, performans açısından
StringBuffer
değilStringBuilder
kullanıyoruz.
Programın kaynak kodlarını indirmek için buraya tıklayın.
Yada sadece programda kullandığım test.txt dosyasına ihtiyacınız varsa, burayı tıklayın.
İşte kodlar:
package com.asosyalbebe.blog;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Scanner;
public class Prep {
private static final String FILENAME = "test.txt";
private static final ArrayList<String> WORD_LIST = new ArrayList<String>(1000);
private static final int SUB_ITERATION = 10;
private static final int MAIN_ITERATION = 10;
public static void main(String[] args) throws FileNotFoundException {
readFile(FILENAME);
double averageTimeConcat = 0;
double averageTimeBuild = 0;
double averageTimeBuffer = 0;
for (int i = 0; i < MAIN_ITERATION; i++) {
System.out.println("Started " + (i + 1) + ". main iteration...");
// double concat = concatenateBenchmark(WORD_LIST, SUB_ITERATION);
// System.out.println("concatenation: " + concat + " ms.");
// averageTimeConcat += concat / MAIN_ITERATION;
double builderTime = stringBuildBenchmark(WORD_LIST, SUB_ITERATION);
System.out.println("string builder: " + builderTime + " ms.");
averageTimeBuild += builderTime / MAIN_ITERATION;
double bufferTime = stringBufferBenchmark(WORD_LIST, SUB_ITERATION);
System.out.println("string buffer: " + bufferTime + " ms.");
averageTimeBuffer += bufferTime / MAIN_ITERATION;
System.out.println("Finished " + (i + 1) + ". main iteration...");
}
System.out.println("Average time for concatenation: " + averageTimeConcat + " ms.");
System.out.println("Average time for string builder: " + averageTimeBuild + " ms.");
System.out.println("Average time for string buffer: " + averageTimeBuffer + " ms.");
}
private static double concatenateBenchmark(ArrayList<String> wordList, int iteration) {
double average = 0;
for (int i = 0; i < iteration; i++) {
long roundTime = concatenateWordList(wordList);
average += (double) roundTime / iteration;
}
return average;
}
private static double stringBuildBenchmark(ArrayList<String> wordList, int iteration) {
double average = 0;
for (int i = 0; i < iteration; i++) {
long roundTime = stringBuildWordList(wordList);
average += (double) roundTime / iteration;
}
return average;
}
private static double stringBufferBenchmark(ArrayList<String> wordList, int iteration) {
double average = 0;
for (int i = 0; i < iteration; i++) {
long roundTime = stringBufferWordList(wordList);
average += (double) roundTime / iteration;
}
return average;
}
private static long concatenateWordList(ArrayList<String> wordList) {
long startTime = System.currentTimeMillis();
String s = "";
for (String line : wordList) {
s += line;
}
long finishTime = System.currentTimeMillis();
s.charAt(0); // Just a dummy call
return finishTime - startTime;
}
private static long stringBuildWordList(ArrayList<String> wordList) {
long startTime = System.currentTimeMillis();
StringBuilder builder = new StringBuilder();
for (String line : wordList) {
builder.append(line);
}
String s = builder.toString();
long finishTime = System.currentTimeMillis();
s.charAt(0); // Just a dummy call
return finishTime - startTime;
}
private static long stringBufferWordList(ArrayList<String> wordList) {
long startTime = System.currentTimeMillis();
StringBuffer buffer = new StringBuffer();
for (String line : wordList) {
buffer.append(line);
}
String s = buffer.toString();
long finishTime = System.currentTimeMillis();
s.charAt(0); // Just a dummy call
return finishTime - startTime;
}
private static void readFile(String filename) throws FileNotFoundException {
Scanner scanner = new Scanner(new File(filename));
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
WORD_LIST.add(line);
}
scanner.close();
for (int j = 0; j < 11; j++) {
int size = WORD_LIST.size();
for (int i = 0; i < size; i++) {
String line = WORD_LIST.get(i);
WORD_LIST.add(line);
}
}
System.out.println("Size of WORD_LIST: " + WORD_LIST.size());
}
}
Buraya kadar katlandığınız için teşekkür ederim efenim, umarım faydalı bir içerik olmuştur. Sağlıcakla kalın.